Merge "Remove TrunkStable.bp" into main
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index c065cd6..a8c8408 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -33,7 +33,6 @@
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
-import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM;
import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
@@ -316,6 +315,7 @@
private final TetheringMetrics mTetheringMetrics;
private final Handler mHandler;
+ private final boolean mIsSyncSM;
// TODO: Add a dependency object to pass the data members or variables from the tethering
// object. It helps to reduce the arguments of the constructor.
@@ -325,7 +325,7 @@
@Nullable LateSdk<RoutingCoordinatorManager> routingCoordinator, Callback callback,
TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
TetheringMetrics tetheringMetrics, Dependencies deps) {
- super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
+ super(ifaceName, config.isSyncSM() ? null : handler.getLooper());
mHandler = handler;
mLog = log.forSubComponent(ifaceName);
mNetd = netd;
@@ -338,6 +338,7 @@
mLinkProperties = new LinkProperties();
mUsingLegacyDhcp = config.useLegacyDhcpServer();
mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
+ mIsSyncSM = config.isSyncSM();
mPrivateAddressCoordinator = addressCoordinator;
mDeps = deps;
mTetheringMetrics = tetheringMetrics;
@@ -515,7 +516,7 @@
private void handleError() {
mLastError = TETHER_ERROR_DHCPSERVER_ERROR;
- if (USE_SYNC_SM) {
+ if (mIsSyncSM) {
sendMessage(CMD_SERVICE_FAILED_TO_START, TETHER_ERROR_DHCPSERVER_ERROR);
} else {
sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START,
@@ -1170,7 +1171,7 @@
// in previous versions of the mainline module.
// TODO : remove sendMessageAtFrontOfQueueToAsyncSM after migrating to the Sync
// StateMachine.
- if (USE_SYNC_SM) {
+ if (mIsSyncSM) {
sendSelfMessageToSyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
} else {
sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 0678525..d09183a 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -132,15 +132,15 @@
public static final String TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION =
"tether_force_random_prefix_base_selection";
+
+ public static final String TETHER_ENABLE_SYNC_SM = "tether_enable_sync_sm";
+
/**
* Default value that used to periodic polls tether offload stats from tethering offload HAL
* to make the data warnings work.
*/
public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000;
- /** A flag for using synchronous or asynchronous state machine. */
- public static final boolean USE_SYNC_SM = false;
-
public final String[] tetherableUsbRegexs;
public final String[] tetherableWifiRegexs;
public final String[] tetherableWigigRegexs;
@@ -174,6 +174,7 @@
private final boolean mEnableWearTethering;
private final boolean mRandomPrefixBase;
+ private final boolean mEnableSyncSm;
private final int mUsbTetheringFunction;
protected final ContentResolver mContentResolver;
@@ -292,6 +293,7 @@
mEnableWearTethering = shouldEnableWearTethering(ctx);
mRandomPrefixBase = mDeps.isFeatureEnabled(ctx, TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION);
+ mEnableSyncSm = mDeps.isFeatureEnabled(ctx, TETHER_ENABLE_SYNC_SM);
configLog.log(toString());
}
@@ -385,6 +387,10 @@
return mRandomPrefixBase;
}
+ public boolean isSyncSM() {
+ return mEnableSyncSm;
+ }
+
/** Does the dumping.*/
public void dump(PrintWriter pw) {
pw.print("activeDataSubId: ");
@@ -438,6 +444,9 @@
pw.print("mRandomPrefixBase: ");
pw.println(mRandomPrefixBase);
+
+ pw.print("mEnableSyncSm: ");
+ pw.println(mEnableSyncSm);
}
/** Returns the string representation of this object.*/
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index aa322dc..19c6e5a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -754,4 +754,26 @@
new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertEquals(p2pLeasesSubnetPrefixLength, p2pCfg.getP2pLeasesSubnetPrefixLength());
}
+
+ private void setTetherEnableSyncSMFlagEnabled(Boolean enabled) {
+ mDeps.setFeatureEnabled(TetheringConfiguration.TETHER_ENABLE_SYNC_SM, enabled);
+ }
+
+ private void assertEnableSyncSMIs(boolean value) {
+ assertEquals(value, new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps).isSyncSM());
+ }
+
+ @Test
+ public void testEnableSyncSMFlag() throws Exception {
+ // Test default disabled
+ setTetherEnableSyncSMFlagEnabled(null);
+ assertEnableSyncSMIs(false);
+
+ setTetherEnableSyncSMFlagEnabled(true);
+ assertEnableSyncSMIs(true);
+
+ setTetherEnableSyncSMFlagEnabled(false);
+ assertEnableSyncSMIs(false);
+ }
}
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 86745d4..60a88c0 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -127,6 +127,7 @@
public final class IpSecTransform implements java.lang.AutoCloseable {
method public void close();
+ method @FlaggedApi("com.android.net.flags.ipsec_transform_state") public void getIpSecTransformState(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.IpSecTransformState,java.lang.RuntimeException>);
}
public static class IpSecTransform.Builder {
@@ -138,6 +139,29 @@
method @NonNull public android.net.IpSecTransform.Builder setIpv4Encapsulation(@NonNull android.net.IpSecManager.UdpEncapsulationSocket, int);
}
+ @FlaggedApi("com.android.net.flags.ipsec_transform_state") public final class IpSecTransformState implements android.os.Parcelable {
+ method public int describeContents();
+ method public long getByteCount();
+ method public long getPacketCount();
+ method @NonNull public byte[] getReplayBitmap();
+ method public long getRxHighestSequenceNumber();
+ method public long getTimestamp();
+ method public long getTxHighestSequenceNumber();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.IpSecTransformState> CREATOR;
+ }
+
+ @FlaggedApi("com.android.net.flags.ipsec_transform_state") public static final class IpSecTransformState.Builder {
+ ctor public IpSecTransformState.Builder();
+ method @NonNull public android.net.IpSecTransformState build();
+ method @NonNull public android.net.IpSecTransformState.Builder setByteCount(long);
+ method @NonNull public android.net.IpSecTransformState.Builder setPacketCount(long);
+ method @NonNull public android.net.IpSecTransformState.Builder setReplayBitmap(@NonNull byte[]);
+ method @NonNull public android.net.IpSecTransformState.Builder setRxHighestSequenceNumber(long);
+ method @NonNull public android.net.IpSecTransformState.Builder setTimestamp(long);
+ method @NonNull public android.net.IpSecTransformState.Builder setTxHighestSequenceNumber(long);
+ }
+
public class TrafficStats {
ctor public TrafficStats();
method public static void clearThreadStatsTag();
@@ -251,6 +275,7 @@
method public int getPort();
method public String getServiceName();
method public String getServiceType();
+ method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") @NonNull public java.util.Set<java.lang.String> getSubtypes();
method public void removeAttribute(String);
method public void setAttribute(String, String);
method @Deprecated public void setHost(java.net.InetAddress);
@@ -259,6 +284,7 @@
method public void setPort(int);
method public void setServiceName(String);
method public void setServiceType(String);
+ method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public void setSubtypes(@NonNull java.util.Set<java.lang.String>);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.NsdServiceInfo> CREATOR;
}
diff --git a/framework-t/src/android/net/IIpSecService.aidl b/framework-t/src/android/net/IIpSecService.aidl
index 88ffd0e..f972ab9 100644
--- a/framework-t/src/android/net/IIpSecService.aidl
+++ b/framework-t/src/android/net/IIpSecService.aidl
@@ -22,6 +22,7 @@
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 @@
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 3afa6ef..3f74e1c 100644
--- a/framework-t/src/android/net/IpSecManager.java
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -65,6 +65,13 @@
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 @@
}
}
+ /** @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 c236b6c..246a2dd 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.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.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.
@@ -201,6 +207,43 @@
}
/**
+ * 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
+ */
+ @FlaggedApi(IPSEC_TRANSFORM_STATE)
+ public void getIpSecTransformState(
+ @CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> 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
*
* <p>Use this callback to receive 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 0000000..69cce28
--- /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 0000000..b575dd5
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTransformState.java
@@ -0,0 +1,282 @@
+/*
+ * 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
+ *
+ * <p>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.
+ */
+@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<IpSecTransformState> CREATOR =
+ new Parcelable.Creator<IpSecTransformState>() {
+ @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)
+ */
+ public long getTimestamp() {
+ return mTimeStamp;
+ }
+
+ /**
+ * Retrieve the highest sequence number sent so far
+ *
+ * @see Builder#setTxHighestSequenceNumber(long)
+ */
+ public long getTxHighestSequenceNumber() {
+ return mTxHighestSequenceNumber;
+ }
+
+ /**
+ * Retrieve the highest sequence number received so far
+ *
+ * @see Builder#setRxHighestSequenceNumber(long)
+ */
+ public long getRxHighestSequenceNumber() {
+ return mRxHighestSequenceNumber;
+ }
+
+ /**
+ * Retrieve the number of packets received AND sent so far
+ *
+ * @see Builder#setPacketCount(long)
+ */
+ public long getPacketCount() {
+ return mPacketCount;
+ }
+
+ /**
+ * Retrieve the number of bytes received AND sent so far
+ *
+ * @see Builder#setByteCount(long)
+ */
+ public long getByteCount() {
+ return mByteCount;
+ }
+
+ /**
+ * Retrieve the replay bitmap
+ *
+ * <p>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.
+ *
+ * <p>The size of a replay bitmap will never change over the lifetime of an IpSecTransform
+ *
+ * <p>The replay bitmap is solely useful for inbound IpSecTransforms. For outbound
+ * IpSecTransforms, all bits will be unchecked.
+ *
+ * @see Builder#setReplayBitmap(byte[])
+ */
+ @NonNull
+ public byte[] getReplayBitmap() {
+ return mReplayBitmap.clone();
+ }
+
+ /** Builder class for testing purposes */
+ @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;
+
+ public Builder() {
+ mTimeStamp = System.currentTimeMillis();
+ }
+
+ /**
+ * Set the epoch timestamp (milliseconds) for when this state was created
+ *
+ * @see IpSecTransformState#getTimestamp()
+ */
+ @NonNull
+ public Builder setTimestamp(long timeStamp) {
+ mTimeStamp = timeStamp;
+ return this;
+ }
+
+ /**
+ * Set the highest sequence number sent so far
+ *
+ * @see IpSecTransformState#getTxHighestSequenceNumber()
+ */
+ @NonNull
+ public Builder setTxHighestSequenceNumber(long seqNum) {
+ mTxHighestSequenceNumber = seqNum;
+ return this;
+ }
+
+ /**
+ * Set the highest sequence number received so far
+ *
+ * @see IpSecTransformState#getRxHighestSequenceNumber()
+ */
+ @NonNull
+ public Builder setRxHighestSequenceNumber(long seqNum) {
+ mRxHighestSequenceNumber = seqNum;
+ return this;
+ }
+
+ /**
+ * Set the number of packets received AND sent so far
+ *
+ * @see IpSecTransformState#getPacketCount()
+ */
+ @NonNull
+ public Builder setPacketCount(long packetCount) {
+ mPacketCount = packetCount;
+ return this;
+ }
+
+ /**
+ * Set the number of bytes received AND sent so far
+ *
+ * @see IpSecTransformState#getByteCount()
+ */
+ @NonNull
+ public Builder setByteCount(long byteCount) {
+ mByteCount = byteCount;
+ return this;
+ }
+
+ /**
+ * Set the replay bitmap
+ *
+ * @see IpSecTransformState#getReplayBitmap()
+ */
+ @NonNull
+ public Builder setReplayBitmap(@NonNull byte[] bitMap) {
+ mReplayBitmap = bitMap.clone();
+ return this;
+ }
+
+ /**
+ * Build and validate the IpSecTransformState
+ *
+ * @return an immutable IpSecTransformState instance
+ */
+ @NonNull
+ public IpSecTransformState build() {
+ return new IpSecTransformState(
+ mTimeStamp,
+ mTxHighestSequenceNumber,
+ mRxHighestSequenceNumber,
+ mPacketCount,
+ mByteCount,
+ mReplayBitmap);
+ }
+ }
+}
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index bf01a9d..fcf79eb 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -150,6 +150,8 @@
public static class Flags {
static final String REGISTER_NSD_OFFLOAD_ENGINE_API =
"com.android.net.flags.register_nsd_offload_engine_api";
+ static final String NSD_SUBTYPES_SUPPORT_ENABLED =
+ "com.android.net.flags.nsd_subtypes_support_enabled";
}
/**
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index caeecdd..ac4ea23 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -16,6 +16,9 @@
package android.net.nsd;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
@@ -24,6 +27,7 @@
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Log;
import com.android.net.module.util.InetAddressUtils;
@@ -35,6 +39,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* A class representing service information for network service discovery
@@ -48,9 +53,11 @@
private String mServiceType;
- private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
+ private final Set<String> mSubtypes;
- private final List<InetAddress> mHostAddresses = new ArrayList<>();
+ private final ArrayMap<String, byte[]> mTxtRecord;
+
+ private final List<InetAddress> mHostAddresses;
private int mPort;
@@ -60,14 +67,34 @@
private int mInterfaceIndex;
public NsdServiceInfo() {
+ mSubtypes = new ArraySet<>();
+ mTxtRecord = new ArrayMap<>();
+ mHostAddresses = new ArrayList<>();
}
/** @hide */
public NsdServiceInfo(String sn, String rt) {
+ this();
mServiceName = sn;
mServiceType = rt;
}
+ /**
+ * Creates a copy of {@code other}.
+ *
+ * @hide
+ */
+ public NsdServiceInfo(@NonNull NsdServiceInfo other) {
+ mServiceName = other.getServiceName();
+ mServiceType = other.getServiceType();
+ mSubtypes = new ArraySet<>(other.getSubtypes());
+ mTxtRecord = new ArrayMap<>(other.mTxtRecord);
+ mHostAddresses = new ArrayList<>(other.getHostAddresses());
+ mPort = other.getPort();
+ mNetwork = other.getNetwork();
+ mInterfaceIndex = other.getInterfaceIndex();
+ }
+
/** Get the service name */
public String getServiceName() {
return mServiceName;
@@ -391,11 +418,41 @@
mInterfaceIndex = interfaceIndex;
}
+ /**
+ * Sets the subtypes to be advertised for this service instance.
+ *
+ * The elements in {@code subtypes} should be the subtype identifiers which have the trailing
+ * "._sub" removed. For example, the subtype should be "_printer" for
+ * "_printer._sub._http._tcp.local".
+ *
+ * Only one subtype will be registered if multiple elements of {@code subtypes} have the same
+ * case-insensitive value.
+ */
+ @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+ public void setSubtypes(@NonNull Set<String> subtypes) {
+ mSubtypes.clear();
+ mSubtypes.addAll(subtypes);
+ }
+
+ /**
+ * Returns subtypes of this service instance.
+ *
+ * When this object is returned by the service discovery/browse APIs (etc. {@link
+ * NsdManager.DiscoveryListener}), the return value may or may not include the subtypes of this
+ * service.
+ */
+ @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+ @NonNull
+ public Set<String> getSubtypes() {
+ return Collections.unmodifiableSet(mSubtypes);
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("name: ").append(mServiceName)
.append(", type: ").append(mServiceType)
+ .append(", subtypes: ").append(TextUtils.join(", ", mSubtypes))
.append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
.append(", port: ").append(mPort)
.append(", network: ").append(mNetwork);
@@ -414,6 +471,7 @@
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mServiceName);
dest.writeString(mServiceType);
+ dest.writeStringList(new ArrayList<>(mSubtypes));
dest.writeInt(mPort);
// TXT record key/value pairs.
@@ -445,6 +503,7 @@
NsdServiceInfo info = new NsdServiceInfo();
info.mServiceName = in.readString();
info.mServiceType = in.readString();
+ info.setSubtypes(new ArraySet<>(in.createStringArrayList()));
info.mPort = in.readInt();
// TXT record key/value pairs.
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index a934ddb..fa27d0e 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -16,8 +16,6 @@
package android.net;
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
-import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
-import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST;
import static android.net.NetworkRequest.Type.LISTEN;
@@ -27,8 +25,6 @@
import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT;
import static android.net.QosCallback.QosCallbackRegistrationException;
-import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
-
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
@@ -41,16 +37,12 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
-import android.annotation.TargetApi;
-import android.app.Application;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
import android.compat.annotation.UnsupportedAppUsage;
-import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.net.ConnectivityDiagnosticsManager.DataStallReport.DetectionMethod;
import android.net.IpSecManager.UdpEncapsulationSocket;
import android.net.SocketKeepalive.Callback;
@@ -82,8 +74,6 @@
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.modules.utils.build.SdkLevel;
import libcore.net.event.NetworkEventDispatcher;
@@ -105,7 +95,6 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
/**
* Class that answers queries about the state of network connectivity. It also
@@ -6262,90 +6251,6 @@
}
/**
- * Helper class to track data saver status.
- *
- * The class will fetch current data saver status from {@link NetworkPolicyManager} when
- * initialized, and listening for status changed intent to cache the latest status.
- *
- * @hide
- */
- @TargetApi(Build.VERSION_CODES.TIRAMISU) // RECEIVER_NOT_EXPORTED requires T.
- @VisibleForTesting(visibility = PRIVATE)
- public static class DataSaverStatusTracker extends BroadcastReceiver {
- private static final Object sDataSaverStatusTrackerLock = new Object();
-
- private static volatile DataSaverStatusTracker sInstance;
-
- /**
- * Gets a static instance of the class.
- *
- * @param context A {@link Context} for initialization. Note that since the data saver
- * status is global on a device, passing any context is equivalent.
- * @return The static instance of a {@link DataSaverStatusTracker}.
- */
- public static DataSaverStatusTracker getInstance(@NonNull Context context) {
- if (sInstance == null) {
- synchronized (sDataSaverStatusTrackerLock) {
- if (sInstance == null) {
- sInstance = new DataSaverStatusTracker(context);
- }
- }
- }
- return sInstance;
- }
-
- private final NetworkPolicyManager mNpm;
- // The value updates on the caller's binder thread or UI thread.
- private final AtomicBoolean mIsDataSaverEnabled;
-
- @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
- public DataSaverStatusTracker(final Context context) {
- // To avoid leaks, take the application context.
- final Context appContext;
- if (context instanceof Application) {
- appContext = context;
- } else {
- appContext = context.getApplicationContext();
- }
-
- if ((appContext.getApplicationInfo().flags & FLAG_PERSISTENT) == 0
- && (appContext.getApplicationInfo().flags & FLAG_SYSTEM) == 0) {
- throw new IllegalStateException("Unexpected caller: "
- + appContext.getApplicationInfo().packageName);
- }
-
- mNpm = appContext.getSystemService(NetworkPolicyManager.class);
- final IntentFilter filter = new IntentFilter(
- ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED);
- // The receiver should not receive broadcasts from other Apps.
- appContext.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED);
- mIsDataSaverEnabled = new AtomicBoolean();
- updateDataSaverEnabled();
- }
-
- // Runs on caller's UI thread.
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED)) {
- updateDataSaverEnabled();
- } else {
- throw new IllegalStateException("Unexpected intent " + intent);
- }
- }
-
- public boolean getDataSaverEnabled() {
- return mIsDataSaverEnabled.get();
- }
-
- private void updateDataSaverEnabled() {
- // Uid doesn't really matter, but use a fixed UID to make things clearer.
- final int dataSaverForCallerUid = mNpm.getRestrictBackgroundStatus(Process.SYSTEM_UID);
- mIsDataSaverEnabled.set(dataSaverForCallerUid
- != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED);
- }
- }
-
- /**
* Return whether the network is blocked for the given uid and metered condition.
*
* Similar to {@link NetworkPolicyManager#isUidNetworkingBlocked}, but directly reads the BPF
@@ -6370,17 +6275,12 @@
@RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
-
- final boolean isDataSaverEnabled;
- if (SdkLevel.isAtLeastV()) {
- isDataSaverEnabled = reader.getDataSaverEnabled();
- } else {
- final DataSaverStatusTracker dataSaverStatusTracker =
- DataSaverStatusTracker.getInstance(mContext);
- isDataSaverEnabled = dataSaverStatusTracker.getDataSaverEnabled();
- }
-
- return reader.isUidNetworkingBlocked(uid, isNetworkMetered, isDataSaverEnabled);
+ // Note that before V, the data saver status in bpf is written by ConnectivityService
+ // when receiving {@link #ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
+ // the status is not synchronized.
+ // On V+, the data saver status is set by platform code when enabling/disabling
+ // data saver, which is synchronized.
+ return reader.isUidNetworkingBlocked(uid, isNetworkMetered, reader.getDataSaverEnabled());
}
/** @hide */
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 9824faa..653e41d 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -290,7 +290,10 @@
* Therefore these capabilities are only in NetworkRequest.
*/
private static final int[] DEFAULT_FORBIDDEN_CAPABILITIES = new int[] {
- NET_CAPABILITY_LOCAL_NETWORK
+ // TODO(b/313030307): this should contain NET_CAPABILITY_LOCAL_NETWORK.
+ // We cannot currently add it because doing so would crash if the module rolls back,
+ // because JobScheduler persists NetworkRequests to disk, and existing production code
+ // does not consider LOCAL_NETWORK to be a valid capability.
};
private final NetworkCapabilities mNetworkCapabilities;
diff --git a/nearby/README.md b/nearby/README.md
index 8451882..0d26563 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -47,12 +47,20 @@
## Build and Install
```sh
-$ source build/envsetup.sh && lunch <TARGET>
-$ m com.google.android.tethering.next deapexer
-$ $ANDROID_BUILD_TOP/out/host/linux-x86/bin/deapexer decompress --input \
- ${ANDROID_PRODUCT_OUT}/system/apex/com.google.android.tethering.next.capex \
- --output /tmp/tethering.apex
-$ adb install -r /tmp/tethering.apex
+Build unbundled module using banchan
+
+$ source build/envsetup.sh
+$ banchan com.google.android.tethering mainline_modules_arm64
+$ m apps_only dist
+$ adb install out/dist/com.google.android.tethering.apex
+$ adb reboot
+Ensure that the module you are installing is compatible with the module currently preloaded on the phone (in /system/apex/com.google.android.tethering.apex). Compatible means:
+
+1. Same package name
+2. Same keys used to sign the apex and the payload
+3. Higher version
+
+See go/mainline-local-build#build-install-local-module for more information
```
## Build and Install from tm-mainline-prod branch
@@ -63,7 +71,7 @@
This is because the device is flashed with AOSP built from master or other branches, which has
prebuilt APEX with higher version. We can use root access to replace the prebuilt APEX with the APEX
built from tm-mainline-prod as below.
-1. adb root && adb remount; adb shell mount -orw,remount /system/apex
+1. adb root && adb remount -R
2. cp tethering.next.apex com.google.android.tethering.apex
3. adb push com.google.android.tethering.apex /system/apex/
4. adb reboot
diff --git a/nearby/framework/java/android/nearby/DataElement.java b/nearby/framework/java/android/nearby/DataElement.java
index 10c1132..8f032bf 100644
--- a/nearby/framework/java/android/nearby/DataElement.java
+++ b/nearby/framework/java/android/nearby/DataElement.java
@@ -39,10 +39,15 @@
private final int mKey;
private final byte[] mValue;
- /** @hide */
+ /**
+ * Note this interface is used for internal implementation only.
+ * We only keep those data element types used for encoding and decoding from the specs.
+ * Please read the nearby specs for learning more about each data type and use it as the only
+ * source.
+ *
+ * @hide
+ */
@IntDef({
- DataType.BLE_SERVICE_DATA,
- DataType.BLE_ADDRESS,
DataType.SALT,
DataType.PRIVATE_IDENTITY,
DataType.TRUSTED_IDENTITY,
@@ -50,20 +55,17 @@
DataType.PROVISIONED_IDENTITY,
DataType.TX_POWER,
DataType.ACTION,
- DataType.MODEL_ID,
- DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER,
DataType.ACCOUNT_KEY_DATA,
DataType.CONNECTION_STATUS,
DataType.BATTERY,
+ DataType.ENCRYPTION_INFO,
+ DataType.BLE_SERVICE_DATA,
+ DataType.BLE_ADDRESS,
DataType.SCAN_MODE,
DataType.TEST_DE_BEGIN,
DataType.TEST_DE_END
})
public @interface DataType {
- int BLE_SERVICE_DATA = 100;
- int BLE_ADDRESS = 101;
- // This is to indicate if the scan is offload only
- int SCAN_MODE = 102;
int SALT = 0;
int PRIVATE_IDENTITY = 1;
int TRUSTED_IDENTITY = 2;
@@ -71,11 +73,19 @@
int PROVISIONED_IDENTITY = 4;
int TX_POWER = 5;
int ACTION = 6;
- int MODEL_ID = 7;
- int EDDYSTONE_EPHEMERAL_IDENTIFIER = 8;
int ACCOUNT_KEY_DATA = 9;
int CONNECTION_STATUS = 10;
int BATTERY = 11;
+
+ int ENCRYPTION_INFO = 16;
+
+ // Not defined in the spec. Reserved for internal use only.
+ int BLE_SERVICE_DATA = 100;
+ int BLE_ADDRESS = 101;
+ // This is to indicate if the scan is offload only
+ int SCAN_MODE = 102;
+
+ int DEVICE_TYPE = 22;
// Reserves test DE ranges from {@link DataElement.DataType#TEST_DE_BEGIN}
// to {@link DataElement.DataType#TEST_DE_END}, inclusive.
// Reserves 128 Test DEs.
@@ -84,27 +94,6 @@
}
/**
- * @hide
- */
- public static boolean isValidType(int type) {
- return type == DataType.BLE_SERVICE_DATA
- || type == DataType.ACCOUNT_KEY_DATA
- || type == DataType.BLE_ADDRESS
- || type == DataType.SCAN_MODE
- || type == DataType.SALT
- || type == DataType.PRIVATE_IDENTITY
- || type == DataType.TRUSTED_IDENTITY
- || type == DataType.PUBLIC_IDENTITY
- || type == DataType.PROVISIONED_IDENTITY
- || type == DataType.TX_POWER
- || type == DataType.ACTION
- || type == DataType.MODEL_ID
- || type == DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER
- || type == DataType.CONNECTION_STATUS
- || type == DataType.BATTERY;
- }
-
- /**
* @return {@code true} if this is identity type.
* @hide
*/
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 070a2b6..00f1c38 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -284,6 +284,8 @@
*/
public void queryOffloadCapability(@NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<OffloadCapability> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
try {
mService.queryOffloadCapability(new OffloadTransport(executor, callback));
} catch (RemoteException e) {
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
index 9b32d69..9ef905d 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -54,6 +54,11 @@
public static final String NEARBY_REFACTOR_DISCOVERY_MANAGER =
"nearby_refactor_discovery_manager";
+ /**
+ * Flag to guard enable BLE during Nearby Service init time.
+ */
+ public static final String NEARBY_ENABLE_BLE_IN_INIT = "nearby_enable_ble_in_init";
+
private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();
@@ -67,6 +72,8 @@
private boolean mSupportTestApp;
@GuardedBy("mDeviceConfigLock")
private boolean mRefactorDiscoveryManager;
+ @GuardedBy("mDeviceConfigLock")
+ private boolean mEnableBleInInit;
public NearbyConfiguration() {
mDeviceConfigListener.start();
@@ -131,6 +138,15 @@
}
}
+ /**
+ * @return {@code true} if enableBLE() is called during NearbyService init time.
+ */
+ public boolean enableBleInInit() {
+ synchronized (mDeviceConfigLock) {
+ return mEnableBleInInit;
+ }
+ }
+
private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
public void start() {
DeviceConfig.addOnPropertiesChangedListener(getNamespace(),
@@ -149,6 +165,8 @@
NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
mRefactorDiscoveryManager = getDeviceConfigBoolean(
NEARBY_REFACTOR_DISCOVERY_MANAGER, false /* defaultValue */);
+ mEnableBleInInit = getDeviceConfigBoolean(
+ NEARBY_ENABLE_BLE_IN_INIT, true /* defaultValue */);
}
}
}
diff --git a/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
index 024bff8..28a33fa 100644
--- a/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
@@ -22,6 +22,7 @@
import android.nearby.BroadcastRequest;
import android.nearby.IBroadcastListener;
import android.nearby.PresenceBroadcastRequest;
+import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
@@ -49,6 +50,10 @@
private final NearbyConfiguration mNearbyConfiguration;
private IBroadcastListener mBroadcastListener;
+ // Used with mBroadcastListener. Now we only support single client, for multi clients, a map
+ // between live binder to the information over the binder is needed.
+ // TODO: Finish multi-client logic for broadcast.
+ private BroadcastListenerDeathRecipient mDeathRecipient;
public BroadcastProviderManager(Context context, Injector injector) {
this(ForegroundThread.getExecutor(),
@@ -62,6 +67,7 @@
mLock = new Object();
mNearbyConfiguration = new NearbyConfiguration();
mBroadcastListener = null;
+ mDeathRecipient = null;
}
/**
@@ -70,6 +76,15 @@
public void startBroadcast(BroadcastRequest broadcastRequest, IBroadcastListener listener) {
synchronized (mLock) {
mExecutor.execute(() -> {
+ if (listener == null) {
+ return;
+ }
+ if (mBroadcastListener != null) {
+ Log.i(TAG, "We do not support multi clients yet,"
+ + " please stop previous broadcast first.");
+ reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+ return;
+ }
if (!mNearbyConfiguration.isTestAppSupported()) {
NearbyConfiguration configuration = new NearbyConfiguration();
if (!configuration.isPresenceBroadcastLegacyEnabled()) {
@@ -89,7 +104,17 @@
reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
return;
}
+ BroadcastListenerDeathRecipient deathRecipient =
+ new BroadcastListenerDeathRecipient(listener);
+ try {
+ listener.asBinder().linkToDeath(deathRecipient, 0);
+ } catch (RemoteException e) {
+ // This binder has already died, so call the DeathRecipient as if we had
+ // called linkToDeath in time.
+ deathRecipient.binderDied();
+ }
mBroadcastListener = listener;
+ mDeathRecipient = deathRecipient;
mBleBroadcastProvider.start(presenceBroadcastRequest.getVersion(),
advertisement.toBytes(), this);
});
@@ -113,13 +138,19 @@
*/
public void stopBroadcast(IBroadcastListener listener) {
synchronized (mLock) {
- if (!mNearbyConfiguration.isTestAppSupported()
- && !mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
- reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
- return;
+ if (listener != null) {
+ if (!mNearbyConfiguration.isTestAppSupported()
+ && !mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
+ reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+ return;
+ }
+ if (mDeathRecipient != null) {
+ listener.asBinder().unlinkToDeath(mDeathRecipient, 0);
+ }
}
mBroadcastListener = null;
- mExecutor.execute(() -> mBleBroadcastProvider.stop());
+ mDeathRecipient = null;
+ mExecutor.execute(mBleBroadcastProvider::stop);
}
}
@@ -142,4 +173,21 @@
Log.e(TAG, "remote exception when reporting status");
}
}
+
+ /**
+ * Class to make listener unregister after the binder is dead.
+ */
+ public class BroadcastListenerDeathRecipient implements IBinder.DeathRecipient {
+ public IBroadcastListener mListener;
+
+ BroadcastListenerDeathRecipient(IBroadcastListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void binderDied() {
+ Log.d(TAG, "Binder is dead - stop broadcast listener");
+ stopBroadcast(mListener);
+ }
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
index 0c41426..8995232 100644
--- a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
@@ -21,6 +21,7 @@
import static com.android.server.nearby.NearbyService.TAG;
import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.nearby.DataElement;
import android.nearby.IScanListener;
@@ -35,6 +36,7 @@
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.managers.registration.DiscoveryRegistration;
import com.android.server.nearby.provider.AbstractDiscoveryProvider;
@@ -66,6 +68,7 @@
private final BleDiscoveryProvider mBleDiscoveryProvider;
private final Injector mInjector;
private final Executor mExecutor;
+ private final NearbyConfiguration mNearbyConfiguration;
public DiscoveryProviderManager(Context context, Injector injector) {
Log.v(TAG, "DiscoveryProviderManager: ");
@@ -75,6 +78,7 @@
mChreDiscoveryProvider = new ChreDiscoveryProvider(mContext,
new ChreCommunication(injector, mContext, mExecutor), mExecutor);
mInjector = injector;
+ mNearbyConfiguration = new NearbyConfiguration();
}
@VisibleForTesting
@@ -86,6 +90,7 @@
mInjector = injector;
mBleDiscoveryProvider = bleDiscoveryProvider;
mChreDiscoveryProvider = chreDiscoveryProvider;
+ mNearbyConfiguration = new NearbyConfiguration();
}
private static boolean isChreOnly(Set<ScanFilter> scanFilters) {
@@ -141,6 +146,10 @@
/** Called after boot completed. */
public void init() {
+ // Register BLE only scan when Bluetooth is turned off
+ if (mNearbyConfiguration.enableBleInInit()) {
+ setBleScanEnabled();
+ }
if (mInjector.getContextHubManager() != null) {
mChreDiscoveryProvider.init();
}
@@ -242,7 +251,7 @@
@GuardedBy("mMultiplexerLock")
private void startBleProvider(Set<ScanFilter> scanFilters) {
if (!mBleDiscoveryProvider.getController().isStarted()) {
- Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+ Log.d(TAG, "DiscoveryProviderManager starts BLE scanning.");
mBleDiscoveryProvider.getController().setListener(this);
mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
mBleDiscoveryProvider.getController().setProviderScanFilters(
@@ -313,4 +322,29 @@
public void onMergedRegistrationsUpdated() {
invalidateProviderScanMode();
}
+
+ /**
+ * Registers Nearby service to Ble scan if Bluetooth is off. (Even when Bluetooth is off)
+ * @return {@code true} when Nearby currently can scan through Bluetooth or Ble or successfully
+ * registers Nearby service to Ble scan when Blutooth is off.
+ */
+ public boolean setBleScanEnabled() {
+ BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+ if (adapter == null) {
+ Log.e(TAG, "BluetoothAdapter is null.");
+ return false;
+ }
+ if (adapter.isEnabled() || adapter.isLeEnabled()) {
+ return true;
+ }
+ if (!adapter.isBleScanAlwaysAvailable()) {
+ Log.v(TAG, "Ble always on scan is disabled.");
+ return false;
+ }
+ if (!adapter.enableBLE()) {
+ Log.e(TAG, "Failed to register Ble scan.");
+ return false;
+ }
+ return true;
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
index 4b76eba..3ef8fb7 100644
--- a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
@@ -22,6 +22,7 @@
import android.annotation.Nullable;
import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.nearby.DataElement;
import android.nearby.IScanListener;
@@ -38,6 +39,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.metrics.NearbyMetrics;
import com.android.server.nearby.presence.PresenceDiscoveryResult;
@@ -69,6 +71,7 @@
private final Context mContext;
private final BleDiscoveryProvider mBleDiscoveryProvider;
private final Injector mInjector;
+ private final NearbyConfiguration mNearbyConfiguration;
@ScanRequest.ScanMode
private int mScanMode;
@GuardedBy("mLock")
@@ -83,6 +86,7 @@
mContext, new ChreCommunication(injector, mContext, executor), executor);
mScanTypeScanListenerRecordMap = new HashMap<>();
mInjector = injector;
+ mNearbyConfiguration = new NearbyConfiguration();
Log.v(TAG, "DiscoveryProviderManagerLegacy: ");
}
@@ -96,6 +100,7 @@
mBleDiscoveryProvider = bleDiscoveryProvider;
mChreDiscoveryProvider = chreDiscoveryProvider;
mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
+ mNearbyConfiguration = new NearbyConfiguration();
}
private static boolean isChreOnly(List<ScanFilter> scanFilters) {
@@ -142,18 +147,18 @@
for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
if (record == null) {
- Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+ Log.w(TAG, "DiscoveryProviderManagerLegacy cannot find the scan record.");
continue;
}
CallerIdentity callerIdentity = record.getCallerIdentity();
if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
appOpsManager, callerIdentity)) {
- Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ Log.w(TAG, "[DiscoveryProviderManagerLegacy] scan permission revoked "
+ "- not forwarding results");
try {
record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
} catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report error.", e);
}
return;
}
@@ -180,7 +185,7 @@
NearbyMetrics.logScanDeviceDiscovered(
record.hashCode(), record.getScanRequest(), nearbyDevice);
} catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
+ Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report onDiscovered.", e);
}
}
}
@@ -193,18 +198,18 @@
for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
if (record == null) {
- Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+ Log.w(TAG, "DiscoveryProviderManagerLegacy cannot find the scan record.");
continue;
}
CallerIdentity callerIdentity = record.getCallerIdentity();
if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
appOpsManager, callerIdentity)) {
- Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ Log.w(TAG, "[DiscoveryProviderManagerLegacy] scan permission revoked "
+ "- not forwarding results");
try {
record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
} catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report error.", e);
}
return;
}
@@ -212,7 +217,7 @@
try {
record.getScanListener().onError(errorCode);
} catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report onError.", e);
+ Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report onError.", e);
}
}
}
@@ -220,6 +225,10 @@
/** Called after boot completed. */
public void init() {
+ // Register BLE only scan when Bluetooth is turned off
+ if (mNearbyConfiguration.enableBleInInit()) {
+ setBleScanEnabled();
+ }
if (mInjector.getContextHubManager() != null) {
mChreDiscoveryProvider.init();
}
@@ -293,10 +302,10 @@
if (listenerBinder != null && deathRecipient != null) {
listenerBinder.unlinkToDeath(removedRecord.getDeathRecipient(), 0);
}
- Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
+ Log.v(TAG, "DiscoveryProviderManagerLegacy unregistered scan listener.");
NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
if (mScanTypeScanListenerRecordMap.isEmpty()) {
- Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
+ Log.v(TAG, "DiscoveryProviderManagerLegacy stops provider because there is no "
+ "scan listener registered.");
stopProviders();
return;
@@ -306,8 +315,8 @@
// Removes current highest scan mode requested and sets the next highest scan mode.
if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
- Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
- + "because the highest scan mode listener was unregistered.");
+ Log.v(TAG, "DiscoveryProviderManagerLegacy starts to find the new highest "
+ + "scan mode because the highest scan mode listener was unregistered.");
@ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
// find the next highest scan mode;
for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
@@ -377,7 +386,7 @@
private void startBleProvider(List<ScanFilter> scanFilters) {
if (!mBleDiscoveryProvider.getController().isStarted()) {
- Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+ Log.d(TAG, "DiscoveryProviderManagerLegacy starts BLE scanning.");
mBleDiscoveryProvider.getController().setListener(this);
mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
@@ -387,7 +396,7 @@
@VisibleForTesting
void startChreProvider(List<ScanFilter> scanFilters) {
- Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
+ Log.d(TAG, "DiscoveryProviderManagerLegacy starts CHRE scanning.");
mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
mChreDiscoveryProvider.getController().start();
@@ -503,4 +512,29 @@
unregisterScanListener(listener);
}
}
+
+ /**
+ * Registers Nearby service to Ble scan if Bluetooth is off. (Even when Bluetooth is off)
+ * @return {@code true} when Nearby currently can scan through Bluetooth or Ble or successfully
+ * registers Nearby service to Ble scan when Blutooth is off.
+ */
+ public boolean setBleScanEnabled() {
+ BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+ if (adapter == null) {
+ Log.e(TAG, "BluetoothAdapter is null.");
+ return false;
+ }
+ if (adapter.isEnabled() || adapter.isLeEnabled()) {
+ return true;
+ }
+ if (!adapter.isBleScanAlwaysAvailable()) {
+ Log.v(TAG, "Ble always on scan is disabled.");
+ return false;
+ }
+ if (!adapter.enableBLE()) {
+ Log.e(TAG, "Failed to register Ble scan.");
+ return false;
+ }
+ return true;
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java b/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
new file mode 100644
index 0000000..ac1e18f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
@@ -0,0 +1,90 @@
+/*
+ * 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 com.android.server.nearby.presence;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.nearby.DataElement;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.nearby.util.ArrayUtils;
+
+import java.util.Arrays;
+
+/**
+ * Data Element that indicates the presence of extended 16 bytes salt instead of 2 byte salt and to
+ * indicate which encoding scheme (MIC tag or Signature) is used for a section. If present, it must
+ * be the first DE in a section.
+ */
+public class EncryptionInfo {
+
+ @IntDef({EncodingScheme.MIC, EncodingScheme.SIGNATURE})
+ public @interface EncodingScheme {
+ int MIC = 0;
+ int SIGNATURE = 1;
+ }
+
+ // 1st byte : encryption scheme
+ // 2nd to 17th bytes: salt
+ public static final int ENCRYPTION_INFO_LENGTH = 17;
+ private static final int ENCODING_SCHEME_MASK = 0b01111000;
+ private static final int ENCODING_SCHEME_OFFSET = 3;
+ @EncodingScheme
+ private final int mEncodingScheme;
+ private final byte[] mSalt;
+
+ /**
+ * Constructs a {@link DataElement}.
+ */
+ public EncryptionInfo(@NonNull byte[] value) {
+ Preconditions.checkArgument(value.length == ENCRYPTION_INFO_LENGTH);
+ mEncodingScheme = (value[0] & ENCODING_SCHEME_MASK) >> ENCODING_SCHEME_OFFSET;
+ Preconditions.checkArgument(isValidEncodingScheme(mEncodingScheme));
+ mSalt = Arrays.copyOfRange(value, 1, ENCRYPTION_INFO_LENGTH);
+ }
+
+ private boolean isValidEncodingScheme(int scheme) {
+ return scheme == EncodingScheme.MIC || scheme == EncodingScheme.SIGNATURE;
+ }
+
+ @EncodingScheme
+ public int getEncodingScheme() {
+ return mEncodingScheme;
+ }
+
+ public byte[] getSalt() {
+ return mSalt;
+ }
+
+ /** Combines the encoding scheme and salt to a byte array
+ * that represents an {@link EncryptionInfo}.
+ */
+ @Nullable
+ public static byte[] toByte(@EncodingScheme int encodingScheme, byte[] salt) {
+ if (ArrayUtils.isEmpty(salt)) {
+ return null;
+ }
+ if (salt.length != ENCRYPTION_INFO_LENGTH - 1) {
+ return null;
+ }
+ byte schemeByte =
+ (byte) ((encodingScheme << ENCODING_SCHEME_OFFSET) & ENCODING_SCHEME_MASK);
+ return ArrayUtils.append(schemeByte, salt);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
index 34a7514..c2304cc 100644
--- a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
@@ -16,22 +16,27 @@
package com.android.server.nearby.presence;
+import static android.nearby.BroadcastRequest.PRESENCE_VERSION_V1;
+
import static com.android.server.nearby.NearbyService.TAG;
+import static com.android.server.nearby.presence.EncryptionInfo.ENCRYPTION_INFO_LENGTH;
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID_BYTES;
import android.annotation.Nullable;
-import android.nearby.BroadcastRequest;
+import android.nearby.BroadcastRequest.BroadcastVersion;
import android.nearby.DataElement;
+import android.nearby.DataElement.DataType;
import android.nearby.PresenceBroadcastRequest;
import android.nearby.PresenceCredential;
import android.nearby.PublicCredential;
import android.util.Log;
+import com.android.server.nearby.util.ArrayUtils;
import com.android.server.nearby.util.encryption.Cryptor;
-import com.android.server.nearby.util.encryption.CryptorImpFake;
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
-import com.android.server.nearby.util.encryption.CryptorImpV1;
+import com.android.server.nearby.util.encryption.CryptorMicImp;
import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -51,37 +56,51 @@
* The header contains:
* version (3 bits) | 5 bit reserved for future use (RFU)
*/
-public class ExtendedAdvertisement extends Advertisement{
+public class ExtendedAdvertisement extends Advertisement {
public static final int SALT_DATA_LENGTH = 2;
-
static final int HEADER_LENGTH = 1;
static final int IDENTITY_DATA_LENGTH = 16;
-
+ // Identity Index is always 2 .
+ // 0 is reserved, 1 is Salt or Credential Element.
+ private static final int CIPHER_START_INDEX = 2;
private final List<DataElement> mDataElements;
+ private final byte[] mKeySeed;
- private final byte[] mAuthenticityKey;
+ private final byte[] mData;
- // All Data Elements including salt and identity.
- // Each list item (byte array) is a Data Element (with its header).
- private final List<byte[]> mCompleteDataElementsBytes;
- // Signature generated from data elements.
- private final byte[] mHmacTag;
+ private ExtendedAdvertisement(
+ @PresenceCredential.IdentityType int identityType,
+ byte[] identity,
+ byte[] salt,
+ byte[] keySeed,
+ List<Integer> actions,
+ List<DataElement> dataElements) {
+ this.mVersion = PRESENCE_VERSION_V1;
+ this.mIdentityType = identityType;
+ this.mIdentity = identity;
+ this.mSalt = salt;
+ this.mKeySeed = keySeed;
+ this.mDataElements = dataElements;
+ this.mActions = actions;
+ mData = toBytesInternal();
+ }
/**
* Creates an {@link ExtendedAdvertisement} from a Presence Broadcast Request.
+ *
* @return {@link ExtendedAdvertisement} object. {@code null} when the request is illegal.
*/
@Nullable
public static ExtendedAdvertisement createFromRequest(PresenceBroadcastRequest request) {
- if (request.getVersion() != BroadcastRequest.PRESENCE_VERSION_V1) {
+ if (request.getVersion() != PRESENCE_VERSION_V1) {
Log.v(TAG, "ExtendedAdvertisement only supports V1 now.");
return null;
}
byte[] salt = request.getSalt();
- if (salt.length != SALT_DATA_LENGTH) {
+ if (salt.length != SALT_DATA_LENGTH && salt.length != ENCRYPTION_INFO_LENGTH - 1) {
Log.v(TAG, "Salt does not match correct length");
return null;
}
@@ -94,12 +113,12 @@
}
List<Integer> actions = request.getActions();
- if (actions.isEmpty()) {
- Log.v(TAG, "ExtendedAdvertisement must contain at least one action");
- return null;
- }
-
List<DataElement> dataElements = request.getExtendedProperties();
+ // DataElements should include actions.
+ for (int action : actions) {
+ dataElements.add(
+ new DataElement(DataType.ACTION, new byte[]{(byte) action}));
+ }
return new ExtendedAdvertisement(
request.getCredential().getIdentityType(),
identity,
@@ -109,149 +128,252 @@
dataElements);
}
- /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
- @Nullable
- public byte[] toBytes() {
- ByteBuffer buffer = ByteBuffer.allocate(getLength());
-
- // Header
- buffer.put(ExtendedAdvertisementUtils.constructHeader(getVersion()));
-
- // Salt
- buffer.put(mCompleteDataElementsBytes.get(0));
-
- // Identity
- buffer.put(mCompleteDataElementsBytes.get(1));
-
- List<Byte> rawDataBytes = new ArrayList<>();
- // Data Elements (Already includes salt and identity)
- for (int i = 2; i < mCompleteDataElementsBytes.size(); i++) {
- byte[] dataElementBytes = mCompleteDataElementsBytes.get(i);
- for (Byte b : dataElementBytes) {
- rawDataBytes.add(b);
- }
- }
-
- byte[] dataElements = new byte[rawDataBytes.size()];
- for (int i = 0; i < rawDataBytes.size(); i++) {
- dataElements[i] = rawDataBytes.get(i);
- }
-
- buffer.put(
- getCryptor(/* encrypt= */ true).encrypt(dataElements, getSalt(), mAuthenticityKey));
-
- buffer.put(mHmacTag);
-
- return buffer.array();
- }
-
- /** Deserialize from bytes into an {@link ExtendedAdvertisement} object.
- * {@code null} when there is something when parsing.
+ /**
+ * Deserialize from bytes into an {@link ExtendedAdvertisement} object.
+ * Return {@code null} when there is an error in parsing.
*/
@Nullable
- public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential publicCredential) {
- @BroadcastRequest.BroadcastVersion
+ public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential sharedCredential) {
+ @BroadcastVersion
int version = ExtendedAdvertisementUtils.getVersion(bytes);
- if (version != PresenceBroadcastRequest.PRESENCE_VERSION_V1) {
+ if (version != PRESENCE_VERSION_V1) {
Log.v(TAG, "ExtendedAdvertisement is used in V1 only and version is " + version);
return null;
}
- byte[] authenticityKey = publicCredential.getAuthenticityKey();
-
- int index = HEADER_LENGTH;
- // Salt
- byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
- DataElementHeader saltHeader = DataElementHeader.fromBytes(version, saltHeaderArray);
- if (saltHeader == null || saltHeader.getDataType() != DataElement.DataType.SALT) {
- Log.v(TAG, "First data element has to be salt.");
+ byte[] keySeed = sharedCredential.getAuthenticityKey();
+ byte[] metadataEncryptionKeyUnsignedAdvTag = sharedCredential.getEncryptedMetadataKeyTag();
+ if (keySeed == null || metadataEncryptionKeyUnsignedAdvTag == null) {
return null;
}
- index += saltHeaderArray.length;
- byte[] salt = new byte[saltHeader.getDataLength()];
- for (int i = 0; i < saltHeader.getDataLength(); i++) {
- salt[i] = bytes[index++];
- }
- // Identity
+ int index = 0;
+ // Header
+ byte[] header = new byte[]{bytes[index]};
+ index += HEADER_LENGTH;
+ // Section header
+ byte[] sectionHeader = new byte[]{bytes[index]};
+ index += HEADER_LENGTH;
+ // Salt or Encryption Info
+ byte[] firstHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+ DataElementHeader firstHeader = DataElementHeader.fromBytes(version, firstHeaderArray);
+ if (firstHeader == null) {
+ Log.v(TAG, "Cannot find salt.");
+ return null;
+ }
+ @DataType int firstType = firstHeader.getDataType();
+ if (firstType != DataType.SALT && firstType != DataType.ENCRYPTION_INFO) {
+ Log.v(TAG, "First data element has to be Salt or Encryption Info.");
+ return null;
+ }
+ index += firstHeaderArray.length;
+ byte[] firstDeBytes = new byte[firstHeader.getDataLength()];
+ for (int i = 0; i < firstHeader.getDataLength(); i++) {
+ firstDeBytes[i] = bytes[index++];
+ }
+ byte[] nonce = getNonce(firstType, firstDeBytes);
+ if (nonce == null) {
+ return null;
+ }
+ byte[] saltBytes = firstType == DataType.SALT
+ ? firstDeBytes : (new EncryptionInfo(firstDeBytes)).getSalt();
+
+ // Identity header
byte[] identityHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
DataElementHeader identityHeader =
DataElementHeader.fromBytes(version, identityHeaderArray);
- if (identityHeader == null) {
- Log.v(TAG, "The second element has to be identity.");
+ if (identityHeader == null || identityHeader.getDataLength() != IDENTITY_DATA_LENGTH) {
+ Log.v(TAG, "The second element has to be a 16-bytes identity.");
return null;
}
index += identityHeaderArray.length;
@PresenceCredential.IdentityType int identityType =
toPresenceCredentialIdentityType(identityHeader.getDataType());
- if (identityType == PresenceCredential.IDENTITY_TYPE_UNKNOWN) {
- Log.v(TAG, "The identity type is unknown.");
+ if (identityType != PresenceCredential.IDENTITY_TYPE_PRIVATE
+ && identityType != PresenceCredential.IDENTITY_TYPE_TRUSTED) {
+ Log.v(TAG, "Only supports encrypted advertisement.");
return null;
}
- byte[] encryptedIdentity = new byte[identityHeader.getDataLength()];
- for (int i = 0; i < identityHeader.getDataLength(); i++) {
- encryptedIdentity[i] = bytes[index++];
- }
- byte[] identity =
- CryptorImpIdentityV1
- .getInstance().decrypt(encryptedIdentity, salt, authenticityKey);
-
- Cryptor cryptor = getCryptor(/* encrypt= */ true);
- byte[] encryptedDataElements =
- new byte[bytes.length - index - cryptor.getSignatureLength()];
- // Decrypt other data elements
- System.arraycopy(bytes, index, encryptedDataElements, 0, encryptedDataElements.length);
- byte[] decryptedDataElements =
- cryptor.decrypt(encryptedDataElements, salt, authenticityKey);
- if (decryptedDataElements == null) {
+ // Ciphertext
+ Cryptor cryptor = CryptorMicImp.getInstance();
+ byte[] ciphertext = new byte[bytes.length - index - cryptor.getSignatureLength()];
+ System.arraycopy(bytes, index, ciphertext, 0, ciphertext.length);
+ byte[] plaintext = cryptor.decrypt(ciphertext, nonce, keySeed);
+ if (plaintext == null) {
return null;
}
+ // Verification
+ // Verify the computed metadata encryption key tag
+ // First 16 bytes is metadata encryption key data
+ byte[] metadataEncryptionKey = new byte[IDENTITY_DATA_LENGTH];
+ System.arraycopy(plaintext, 0, metadataEncryptionKey, 0, IDENTITY_DATA_LENGTH);
+ // Verify metadata encryption key tag
+ byte[] computedMetadataEncryptionKeyTag =
+ CryptorMicImp.generateMetadataEncryptionKeyTag(metadataEncryptionKey,
+ keySeed);
+ if (!Arrays.equals(computedMetadataEncryptionKeyTag, metadataEncryptionKeyUnsignedAdvTag)) {
+ Log.w(TAG,
+ "The calculated metadata encryption key tag is different from the metadata "
+ + "encryption key unsigned adv tag in the SharedCredential.");
+ return null;
+ }
// Verify the computed HMAC tag is equal to HMAC tag in advertisement
- if (cryptor.getSignatureLength() > 0) {
- byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
- System.arraycopy(
- bytes, bytes.length - cryptor.getSignatureLength(),
- expectedHmacTag, 0, cryptor.getSignatureLength());
- if (!cryptor.verify(decryptedDataElements, authenticityKey, expectedHmacTag)) {
- Log.e(TAG, "HMAC tags not match.");
- return null;
- }
+ byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
+ System.arraycopy(
+ bytes, bytes.length - cryptor.getSignatureLength(),
+ expectedHmacTag, 0, cryptor.getSignatureLength());
+ byte[] micInput = ArrayUtils.concatByteArrays(
+ PRESENCE_UUID_BYTES, header, sectionHeader,
+ firstHeaderArray, firstDeBytes,
+ nonce, identityHeaderArray, ciphertext);
+ if (!cryptor.verify(micInput, keySeed, expectedHmacTag)) {
+ Log.e(TAG, "HMAC tag not match.");
+ return null;
}
- int dataElementArrayIndex = 0;
- // Other Data Elements
- List<Integer> actions = new ArrayList<>();
- List<DataElement> dataElements = new ArrayList<>();
- while (dataElementArrayIndex < decryptedDataElements.length) {
- byte[] deHeaderArray = ExtendedAdvertisementUtils
- .getDataElementHeader(decryptedDataElements, dataElementArrayIndex);
- DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
- dataElementArrayIndex += deHeaderArray.length;
+ byte[] otherDataElements = new byte[plaintext.length - IDENTITY_DATA_LENGTH];
+ System.arraycopy(plaintext, IDENTITY_DATA_LENGTH,
+ otherDataElements, 0, otherDataElements.length);
+ List<DataElement> dataElements = getDataElementsFromBytes(version, otherDataElements);
+ if (dataElements.isEmpty()) {
+ return null;
+ }
+ List<Integer> actions = getActionsFromDataElements(dataElements);
+ if (actions == null) {
+ return null;
+ }
+ return new ExtendedAdvertisement(identityType, metadataEncryptionKey, saltBytes, keySeed,
+ actions, dataElements);
+ }
- @DataElement.DataType int type = Objects.requireNonNull(deHeader).getDataType();
- if (type == DataElement.DataType.ACTION) {
- if (deHeader.getDataLength() != 1) {
- Log.v(TAG, "Action id should only 1 byte.");
+ @PresenceCredential.IdentityType
+ private static int toPresenceCredentialIdentityType(@DataType int type) {
+ switch (type) {
+ case DataType.PRIVATE_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_PRIVATE;
+ case DataType.PROVISIONED_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
+ case DataType.TRUSTED_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_TRUSTED;
+ case DataType.PUBLIC_IDENTITY:
+ default:
+ return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
+ }
+ }
+
+ @DataType
+ private static int toDataType(@PresenceCredential.IdentityType int identityType) {
+ switch (identityType) {
+ case PresenceCredential.IDENTITY_TYPE_PRIVATE:
+ return DataType.PRIVATE_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
+ return DataType.PROVISIONED_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_TRUSTED:
+ return DataType.TRUSTED_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
+ default:
+ return DataType.PUBLIC_IDENTITY;
+ }
+ }
+
+ /**
+ * Returns {@code true} if the given {@link DataType} is salt, or one of the
+ * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
+ */
+ private static boolean isSaltOrIdentity(@DataType int type) {
+ return type == DataType.SALT || type == DataType.ENCRYPTION_INFO
+ || type == DataType.PRIVATE_IDENTITY
+ || type == DataType.TRUSTED_IDENTITY
+ || type == DataType.PROVISIONED_IDENTITY
+ || type == DataType.PUBLIC_IDENTITY;
+ }
+
+ /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+ @Nullable
+ public byte[] toBytes() {
+ return mData.clone();
+ }
+
+ /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+ @Nullable
+ public byte[] toBytesInternal() {
+ int sectionLength = 0;
+ // Salt
+ DataElement saltDe;
+ byte[] nonce;
+ try {
+ switch (mSalt.length) {
+ case SALT_DATA_LENGTH:
+ saltDe = new DataElement(DataType.SALT, mSalt);
+ nonce = CryptorMicImp.generateAdvNonce(mSalt);
+ break;
+ case ENCRYPTION_INFO_LENGTH - 1:
+ saltDe = new DataElement(DataType.ENCRYPTION_INFO,
+ EncryptionInfo.toByte(EncryptionInfo.EncodingScheme.MIC, mSalt));
+ nonce = CryptorMicImp.generateAdvNonce(mSalt, CIPHER_START_INDEX);
+ break;
+ default:
+ Log.w(TAG, "Invalid salt size.");
return null;
- }
- actions.add((int) decryptedDataElements[dataElementArrayIndex++]);
- } else {
- if (isSaltOrIdentity(type)) {
- Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
- + " and one identity in the advertisement.");
- return null;
- }
- byte[] deData = new byte[deHeader.getDataLength()];
- for (int i = 0; i < deHeader.getDataLength(); i++) {
- deData[i] = decryptedDataElements[dataElementArrayIndex++];
- }
- dataElements.add(new DataElement(type, deData));
}
+ } catch (GeneralSecurityException e) {
+ Log.w(TAG, "Failed to generate the IV for encryption.", e);
+ return null;
}
- return new ExtendedAdvertisement(identityType, identity, salt, authenticityKey, actions,
- dataElements);
+ byte[] saltOrEncryptionInfoBytes =
+ ExtendedAdvertisementUtils.convertDataElementToBytes(saltDe);
+ sectionLength += saltOrEncryptionInfoBytes.length;
+ // 16 bytes encrypted identity
+ @DataType int identityDataType = toDataType(getIdentityType());
+ byte[] identityHeaderBytes = new DataElementHeader(PRESENCE_VERSION_V1,
+ identityDataType, mIdentity.length).toBytes();
+ sectionLength += identityHeaderBytes.length;
+ final List<DataElement> dataElementList = getDataElements();
+ byte[] ciphertext = getCiphertext(nonce, dataElementList);
+ if (ciphertext == null) {
+ return null;
+ }
+ sectionLength += ciphertext.length;
+ // mic
+ sectionLength += CryptorMicImp.MIC_LENGTH;
+ mLength = sectionLength;
+ // header
+ byte header = ExtendedAdvertisementUtils.constructHeader(getVersion());
+ mLength += HEADER_LENGTH;
+ // section header
+ if (sectionLength > 255) {
+ Log.e(TAG, "A section should be shorter than 255 bytes.");
+ return null;
+ }
+ byte sectionHeader = (byte) sectionLength;
+ mLength += HEADER_LENGTH;
+
+ // generates mic
+ ByteBuffer micInputBuffer = ByteBuffer.allocate(
+ mLength + PRESENCE_UUID_BYTES.length + nonce.length - CryptorMicImp.MIC_LENGTH);
+ micInputBuffer.put(PRESENCE_UUID_BYTES);
+ micInputBuffer.put(header);
+ micInputBuffer.put(sectionHeader);
+ micInputBuffer.put(saltOrEncryptionInfoBytes);
+ micInputBuffer.put(nonce);
+ micInputBuffer.put(identityHeaderBytes);
+ micInputBuffer.put(ciphertext);
+ byte[] micInput = micInputBuffer.array();
+ byte[] mic = CryptorMicImp.getInstance().sign(micInput, mKeySeed);
+ if (mic == null) {
+ return null;
+ }
+
+ ByteBuffer buffer = ByteBuffer.allocate(mLength);
+ buffer.put(header);
+ buffer.put(sectionHeader);
+ buffer.put(saltOrEncryptionInfoBytes);
+ buffer.put(identityHeaderBytes);
+ buffer.put(ciphertext);
+ buffer.put(mic);
+ return buffer.array();
}
/** Returns the {@link DataElement}s in the advertisement. */
@@ -260,7 +382,7 @@
}
/** Returns the {@link DataElement}s in the advertisement according to the key. */
- public List<DataElement> getDataElements(@DataElement.DataType int key) {
+ public List<DataElement> getDataElements(@DataType int key) {
List<DataElement> res = new ArrayList<>();
for (DataElement dataElement : mDataElements) {
if (key == dataElement.getKey()) {
@@ -285,125 +407,86 @@
getActions());
}
- ExtendedAdvertisement(
- @PresenceCredential.IdentityType int identityType,
- byte[] identity,
- byte[] salt,
- byte[] authenticityKey,
- List<Integer> actions,
- List<DataElement> dataElements) {
- this.mVersion = BroadcastRequest.PRESENCE_VERSION_V1;
- this.mIdentityType = identityType;
- this.mIdentity = identity;
- this.mSalt = salt;
- this.mAuthenticityKey = authenticityKey;
- this.mActions = actions;
- this.mDataElements = dataElements;
- this.mCompleteDataElementsBytes = new ArrayList<>();
+ @Nullable
+ private byte[] getCiphertext(byte[] nonce, List<DataElement> dataElements) {
+ Cryptor cryptor = CryptorMicImp.getInstance();
+ byte[] rawDeBytes = mIdentity;
+ for (DataElement dataElement : dataElements) {
+ rawDeBytes = ArrayUtils.concatByteArrays(rawDeBytes,
+ ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement));
+ }
+ return cryptor.encrypt(rawDeBytes, nonce, mKeySeed);
+ }
- int length = HEADER_LENGTH; // header
+ private static List<DataElement> getDataElementsFromBytes(
+ @BroadcastVersion int version, byte[] bytes) {
+ List<DataElement> res = new ArrayList<>();
+ if (ArrayUtils.isEmpty(bytes)) {
+ return res;
+ }
+ int index = 0;
+ while (index < bytes.length) {
+ byte[] deHeaderArray = ExtendedAdvertisementUtils
+ .getDataElementHeader(bytes, index);
+ DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
+ index += deHeaderArray.length;
+ @DataType int type = Objects.requireNonNull(deHeader).getDataType();
+ if (isSaltOrIdentity(type)) {
+ Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
+ + " and one identity in the advertisement.");
+ return new ArrayList<>();
+ }
+ byte[] deData = new byte[deHeader.getDataLength()];
+ for (int i = 0; i < deHeader.getDataLength(); i++) {
+ deData[i] = bytes[index++];
+ }
+ res.add(new DataElement(type, deData));
+ }
+ return res;
+ }
- // Salt
- DataElement saltElement = new DataElement(DataElement.DataType.SALT, salt);
- byte[] saltByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(saltElement);
- mCompleteDataElementsBytes.add(saltByteArray);
- length += saltByteArray.length;
+ @Nullable
+ private static byte[] getNonce(@DataType int type, byte[] data) {
+ try {
+ if (type == DataType.SALT) {
+ if (data.length != SALT_DATA_LENGTH) {
+ Log.v(TAG, "Salt DataElement needs to be 2 bytes.");
+ return null;
+ }
+ return CryptorMicImp.generateAdvNonce(data);
+ } else if (type == DataType.ENCRYPTION_INFO) {
+ try {
+ EncryptionInfo info = new EncryptionInfo(data);
+ if (info.getEncodingScheme() != EncryptionInfo.EncodingScheme.MIC) {
+ Log.v(TAG, "Not support Signature yet.");
+ return null;
+ }
+ return CryptorMicImp.generateAdvNonce(info.getSalt(), CIPHER_START_INDEX);
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Salt DataElement needs to be 17 bytes.", e);
+ return null;
+ }
+ }
+ } catch (GeneralSecurityException e) {
+ Log.w(TAG, "Failed to decrypt metadata encryption key.", e);
+ return null;
+ }
+ return null;
+ }
- // Identity
- byte[] encryptedIdentity =
- CryptorImpIdentityV1.getInstance().encrypt(identity, salt, authenticityKey);
- DataElement identityElement = new DataElement(toDataType(identityType), encryptedIdentity);
- byte[] identityByteArray =
- ExtendedAdvertisementUtils.convertDataElementToBytes(identityElement);
- mCompleteDataElementsBytes.add(identityByteArray);
- length += identityByteArray.length;
-
- List<Byte> dataElementBytes = new ArrayList<>();
- // Intents
- for (int action : mActions) {
- DataElement actionElement = new DataElement(DataElement.DataType.ACTION,
- new byte[] {(byte) action});
- byte[] intentByteArray =
- ExtendedAdvertisementUtils.convertDataElementToBytes(actionElement);
- mCompleteDataElementsBytes.add(intentByteArray);
- for (Byte b : intentByteArray) {
- dataElementBytes.add(b);
+ @Nullable
+ private static List<Integer> getActionsFromDataElements(List<DataElement> dataElements) {
+ List<Integer> actions = new ArrayList<>();
+ for (DataElement dataElement : dataElements) {
+ if (dataElement.getKey() == DataElement.DataType.ACTION) {
+ byte[] value = dataElement.getValue();
+ if (value.length != 1) {
+ Log.w(TAG, "Action should be only 1 byte.");
+ return null;
+ }
+ actions.add(Byte.toUnsignedInt(value[0]));
}
}
-
- // Data Elements (Extended properties)
- for (DataElement dataElement : mDataElements) {
- byte[] deByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement);
- mCompleteDataElementsBytes.add(deByteArray);
- for (Byte b : deByteArray) {
- dataElementBytes.add(b);
- }
- }
-
- byte[] data = new byte[dataElementBytes.size()];
- for (int i = 0; i < dataElementBytes.size(); i++) {
- data[i] = dataElementBytes.get(i);
- }
- Cryptor cryptor = getCryptor(/* encrypt= */ true);
- byte[] encryptedDeBytes = cryptor.encrypt(data, salt, authenticityKey);
-
- length += encryptedDeBytes.length;
-
- // Signature
- byte[] hmacTag = Objects.requireNonNull(cryptor.sign(data, authenticityKey));
- mHmacTag = hmacTag;
- length += hmacTag.length;
-
- this.mLength = length;
- }
-
- @PresenceCredential.IdentityType
- private static int toPresenceCredentialIdentityType(@DataElement.DataType int type) {
- switch (type) {
- case DataElement.DataType.PRIVATE_IDENTITY:
- return PresenceCredential.IDENTITY_TYPE_PRIVATE;
- case DataElement.DataType.PROVISIONED_IDENTITY:
- return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
- case DataElement.DataType.TRUSTED_IDENTITY:
- return PresenceCredential.IDENTITY_TYPE_TRUSTED;
- case DataElement.DataType.PUBLIC_IDENTITY:
- default:
- return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
- }
- }
-
- @DataElement.DataType
- private static int toDataType(@PresenceCredential.IdentityType int identityType) {
- switch (identityType) {
- case PresenceCredential.IDENTITY_TYPE_PRIVATE:
- return DataElement.DataType.PRIVATE_IDENTITY;
- case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
- return DataElement.DataType.PROVISIONED_IDENTITY;
- case PresenceCredential.IDENTITY_TYPE_TRUSTED:
- return DataElement.DataType.TRUSTED_IDENTITY;
- case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
- default:
- return DataElement.DataType.PUBLIC_IDENTITY;
- }
- }
-
- /**
- * Returns {@code true} if the given {@link DataElement.DataType} is salt, or one of the
- * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
- */
- private static boolean isSaltOrIdentity(@DataElement.DataType int type) {
- return type == DataElement.DataType.SALT || type == DataElement.DataType.PRIVATE_IDENTITY
- || type == DataElement.DataType.TRUSTED_IDENTITY
- || type == DataElement.DataType.PROVISIONED_IDENTITY
- || type == DataElement.DataType.PUBLIC_IDENTITY;
- }
-
- private static Cryptor getCryptor(boolean encrypt) {
- if (encrypt) {
- Log.d(TAG, "get V1 Cryptor");
- return CryptorImpV1.getInstance();
- }
- Log.d(TAG, "get fake Cryptor");
- return CryptorImpFake.getInstance();
+ return actions;
}
}
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
index 54264f7..50dada2 100644
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
@@ -18,10 +18,15 @@
import android.os.ParcelUuid;
+import com.android.server.nearby.util.ArrayUtils;
+
/**
* Constants for Nearby Presence operations.
*/
public class PresenceConstants {
+ /** The Presence UUID value in byte array format. */
+ public static final byte[] PRESENCE_UUID_BYTES = ArrayUtils.intToByteArray(0xFCF1);
+
/** Presence advertisement service data uuid. */
public static final ParcelUuid PRESENCE_UUID =
ParcelUuid.fromString("0000fcf1-0000-1000-8000-00805f9b34fb");
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 6829fba..66ae79c 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -85,6 +85,7 @@
case BroadcastRequest.PRESENCE_VERSION_V0:
bluetoothLeAdvertiser.startAdvertising(getAdvertiseSettings(),
advertiseData, this);
+ Log.v(TAG, "Start to broadcast V0 advertisement.");
break;
case BroadcastRequest.PRESENCE_VERSION_V1:
if (adapter.isLeExtendedAdvertisingSupported()) {
@@ -92,6 +93,7 @@
getAdvertisingSetParameters(),
advertiseData,
null, null, null, mAdvertisingSetCallback);
+ Log.v(TAG, "Start to broadcast V1 advertisement.");
} else {
Log.w(TAG, "Failed to start advertising set because the chipset"
+ " does not supports LE Extended Advertising feature.");
@@ -103,7 +105,8 @@
+ " is wrong.");
advertiseStarted = false;
}
- } catch (NullPointerException | IllegalStateException | SecurityException e) {
+ } catch (NullPointerException | IllegalStateException | SecurityException
+ | IllegalArgumentException e) {
Log.w(TAG, "Failed to start advertising.", e);
advertiseStarted = false;
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index 355f7cf..e4651b7 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -45,7 +45,6 @@
import com.android.server.nearby.presence.ExtendedAdvertisement;
import com.android.server.nearby.util.ArrayUtils;
import com.android.server.nearby.util.ForegroundThread;
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
import com.google.common.annotations.VisibleForTesting;
@@ -69,15 +68,6 @@
@GuardedBy("mLock")
@Nullable
private List<android.nearby.ScanFilter> mScanFilters;
- private android.bluetooth.le.ScanCallback mScanCallbackLegacy =
- new android.bluetooth.le.ScanCallback() {
- @Override
- public void onScanResult(int callbackType, ScanResult scanResult) {
- }
- @Override
- public void onScanFailed(int errorCode) {
- }
- };
private android.bluetooth.le.ScanCallback mScanCallback =
new android.bluetooth.le.ScanCallback() {
@Override
@@ -133,10 +123,6 @@
.addMedium(NearbyDevice.Medium.BLE)
.setName(deviceName)
.setRssi(rssi);
- for (int i : advertisement.getActions()) {
- builder.addExtendedProperty(new DataElement(DataElement.DataType.ACTION,
- new byte[]{(byte) i}));
- }
for (DataElement dataElement : advertisement.getDataElements()) {
builder.addExtendedProperty(dataElement);
}
@@ -175,7 +161,6 @@
if (isBleAvailable()) {
Log.d(TAG, "BleDiscoveryProvider started");
startScan(getScanFilters(), getScanSettings(/* legacy= */ false), mScanCallback);
- startScan(getScanFilters(), getScanSettings(/* legacy= */ true), mScanCallbackLegacy);
return;
}
Log.w(TAG, "Cannot start BleDiscoveryProvider because Ble is not available.");
@@ -192,7 +177,6 @@
}
Log.v(TAG, "Ble scan stopped.");
bluetoothLeScanner.stopScan(mScanCallback);
- bluetoothLeScanner.stopScan(mScanCallbackLegacy);
synchronized (mLock) {
if (mScanFilters != null) {
mScanFilters = null;
@@ -284,17 +268,13 @@
if (advertisement == null) {
continue;
}
- if (CryptorImpIdentityV1.getInstance().verify(
- advertisement.getIdentity(),
- credential.getEncryptedMetadataKeyTag())) {
- builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
- rssi));
- builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
- if (!ArrayUtils.isEmpty(credential.getSecretId())) {
- builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
- }
- return;
+ builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
+ rssi));
+ builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
+ if (!ArrayUtils.isEmpty(credential.getSecretId())) {
+ builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
}
+ return;
}
}
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index d69d42d..21ec252 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -20,6 +20,9 @@
import static com.android.server.nearby.NearbyService.TAG;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -155,7 +158,7 @@
builder.setFastPairSupported(version != ChreCommunication.INVALID_NANO_APP_VERSION);
try {
callback.onQueryComplete(builder.build());
- } catch (RemoteException e) {
+ } catch (RemoteException | NullPointerException e) {
e.printStackTrace();
}
});
@@ -350,7 +353,11 @@
DataElement.DataType.ACTION,
new byte[]{(byte) filterResult.getIntent()}));
}
-
+ if (filterResult.hasTimestampNs()) {
+ presenceDeviceBuilder
+ .setDiscoveryTimestampMillis(MILLISECONDS.convert(
+ filterResult.getTimestampNs(), NANOSECONDS));
+ }
PublicCredential publicCredential =
new PublicCredential.Builder(
secretId,
diff --git a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
index 35251d8..e69d004 100644
--- a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
+++ b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
@@ -22,6 +22,10 @@
* ArrayUtils class that help manipulate array.
*/
public class ArrayUtils {
+ private static final char[] HEX_UPPERCASE = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+ };
+
/** Concatenate N arrays of bytes into a single array. */
public static byte[] concatByteArrays(byte[]... arrays) {
// Degenerate case - no input provided.
@@ -52,4 +56,67 @@
public static boolean isEmpty(byte[] bytes) {
return bytes == null || bytes.length == 0;
}
+
+ /** Appends a byte array to a byte. */
+ public static byte[] append(byte a, byte[] b) {
+ if (b == null) {
+ return new byte[]{a};
+ }
+
+ int length = b.length;
+ byte[] result = new byte[length + 1];
+ result[0] = a;
+ System.arraycopy(b, 0, result, 1, length);
+ return result;
+ }
+
+ /**
+ * Converts an Integer to a 2-byte array.
+ */
+ public static byte[] intToByteArray(int value) {
+ return new byte[] {(byte) (value >> 8), (byte) value};
+ }
+
+ /** Appends a byte to a byte array. */
+ public static byte[] append(byte[] a, byte b) {
+ if (a == null) {
+ return new byte[]{b};
+ }
+
+ int length = a.length;
+ byte[] result = new byte[length + 1];
+ System.arraycopy(a, 0, result, 0, length);
+ result[length] = b;
+ return result;
+ }
+
+ /** Convert an hex string to a byte array. */
+
+ public static byte[] stringToBytes(String hex) throws IllegalArgumentException {
+ int length = hex.length();
+ if (length % 2 != 0) {
+ throw new IllegalArgumentException("Hex string has odd number of characters");
+ }
+ byte[] out = new byte[length / 2];
+ for (int i = 0; i < length; i += 2) {
+ // Byte.parseByte() doesn't work here because it expects a hex value in -128, 127, and
+ // our hex values are in 0, 255.
+ out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
+ }
+ return out;
+ }
+
+ /** Encodes a byte array as a hexadecimal representation of bytes. */
+ public static String bytesToStringUppercase(byte[] bytes) {
+ int length = bytes.length;
+ StringBuilder out = new StringBuilder(length * 2);
+ for (int i = 0; i < length; i++) {
+ if (i == length - 1 && (bytes[i] & 0xff) == 0) {
+ break;
+ }
+ out.append(HEX_UPPERCASE[(bytes[i] & 0xf0) >>> 4]);
+ out.append(HEX_UPPERCASE[bytes[i] & 0x0f]);
+ }
+ return out.toString();
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
index 3c5132d..ba9ca41 100644
--- a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
@@ -22,6 +22,10 @@
import androidx.annotation.Nullable;
+import com.google.common.hash.Hashing;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@@ -30,22 +34,28 @@
/** Class for encryption/decryption functionality. */
public abstract class Cryptor {
+ /**
+ * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+ */
+ public static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+ public static final byte[] NP_HKDF_SALT = "Google Nearby".getBytes(StandardCharsets.US_ASCII);
/** AES only supports key sizes of 16, 24 or 32 bytes. */
static final int AUTHENTICITY_KEY_BYTE_SIZE = 16;
- private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+ public static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
/**
* Encrypt the provided data blob.
*
* @param data data blob to be encrypted.
- * @param salt used for IV
+ * @param iv advertisement nonce
* @param secretKeyBytes secrete key accessed from credentials
* @return encrypted data, {@code null} if failed to encrypt.
*/
@Nullable
- public byte[] encrypt(byte[] data, byte[] salt, byte[] secretKeyBytes) {
+ public byte[] encrypt(byte[] data, byte[] iv, byte[] secretKeyBytes) {
return data;
}
@@ -53,12 +63,12 @@
* Decrypt the original data blob from the provided byte array.
*
* @param encryptedData data blob to be decrypted.
- * @param salt used for IV
+ * @param iv advertisement nonce
* @param secretKeyBytes secrete key accessed from credentials
* @return decrypted data, {@code null} if failed to decrypt.
*/
@Nullable
- public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] secretKeyBytes) {
+ public byte[] decrypt(byte[] encryptedData, byte[] iv, byte[] secretKeyBytes) {
return encryptedData;
}
@@ -90,7 +100,7 @@
* A HAMC sha256 based HKDF algorithm to pseudo randomly hash data and salt into a byte array of
* given size.
*/
- // Based on google3/third_party/tink/java/src/main/java/com/google/crypto/tink/subtle/Hkdf.java
+ // Based on crypto/tink/subtle/Hkdf.java
@Nullable
static byte[] computeHkdf(byte[] ikm, byte[] salt, int size) {
Mac mac;
@@ -146,4 +156,66 @@
return result;
}
+
+ /**
+ * Computes an HKDF.
+ *
+ * @param macAlgorithm the MAC algorithm used for computing the Hkdf. I.e., "HMACSHA1" or
+ * "HMACSHA256".
+ * @param ikm the input keying material.
+ * @param salt optional salt. A possibly non-secret random value.
+ * (If no salt is provided i.e. if salt has length 0)
+ * then an array of 0s of the same size as the hash
+ * digest is used as salt.
+ * @param info optional context and application specific information.
+ * @param size The length of the generated pseudorandom string in bytes.
+ * @throws GeneralSecurityException if the {@code macAlgorithm} is not supported or if {@code
+ * size} is too large or if {@code salt} is not a valid key for
+ * macAlgorithm (which should not
+ * happen since HMAC allows key sizes up to 2^64).
+ */
+ public static byte[] computeHkdf(
+ String macAlgorithm, final byte[] ikm, final byte[] salt, final byte[] info, int size)
+ throws GeneralSecurityException {
+ Mac mac = Mac.getInstance(macAlgorithm);
+ if (size > 255 * mac.getMacLength()) {
+ throw new GeneralSecurityException("size too large");
+ }
+ if (salt == null || salt.length == 0) {
+ // According to RFC 5869, Section 2.2 the salt is optional. If no salt is provided
+ // then HKDF uses a salt that is an array of zeros of the same length as the hash
+ // digest.
+ mac.init(new SecretKeySpec(new byte[mac.getMacLength()], macAlgorithm));
+ } else {
+ mac.init(new SecretKeySpec(salt, macAlgorithm));
+ }
+ byte[] prk = mac.doFinal(ikm);
+ byte[] result = new byte[size];
+ int ctr = 1;
+ int pos = 0;
+ mac.init(new SecretKeySpec(prk, macAlgorithm));
+ byte[] digest = new byte[0];
+ while (true) {
+ mac.update(digest);
+ mac.update(info);
+ mac.update((byte) ctr);
+ digest = mac.doFinal();
+ if (pos + digest.length < size) {
+ System.arraycopy(digest, 0, result, pos, digest.length);
+ pos += digest.length;
+ ctr++;
+ } else {
+ System.arraycopy(digest, 0, result, pos, size - pos);
+ break;
+ }
+ }
+ return result;
+ }
+
+ /** Generates the HMAC bytes. */
+ public static byte[] generateHmac(String algorithm, byte[] input, byte[] key) {
+ return Hashing.hmacSha256(new SecretKeySpec(key, algorithm))
+ .hashBytes(input)
+ .asBytes();
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
deleted file mode 100644
index 1c0ec9e..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2022 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.server.nearby.util.encryption;
-
-import androidx.annotation.Nullable;
-
-/**
- * A Cryptor that returns the original data without actual encryption
- */
-public class CryptorImpFake extends Cryptor {
- // Lazily instantiated when {@link #getInstance()} is called.
- @Nullable
- private static CryptorImpFake sCryptor;
-
- /** Returns an instance of CryptorImpFake. */
- public static CryptorImpFake getInstance() {
- if (sCryptor == null) {
- sCryptor = new CryptorImpFake();
- }
- return sCryptor;
- }
-
- private CryptorImpFake() {
- }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
deleted file mode 100644
index b0e19b4..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright (C) 2022 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.server.nearby.util.encryption;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.security.keystore.KeyProperties;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for identity
- * encryption and decryption.
- */
-public class CryptorImpIdentityV1 extends Cryptor {
-
- // 3 16 byte arrays known by both the encryptor and decryptor.
- private static final byte[] EK_IV =
- new byte[] {14, -123, -39, 42, 109, 127, 83, 27, 27, 11, 91, -38, 92, 17, -84, 66};
- private static final byte[] ESALT_IV =
- new byte[] {46, 83, -19, 10, -127, -31, -31, 12, 31, 76, 63, -9, 33, -66, 15, -10};
- private static final byte[] KTAG_IV =
- {-22, -83, -6, 67, 16, -99, -13, -9, 8, -3, -16, 37, -75, 47, 1, -56};
-
- /** Length of encryption key required by AES/GCM encryption. */
- private static final int ENCRYPTION_KEY_SIZE = 32;
-
- /** Length of salt required by AES/GCM encryption. */
- private static final int AES_CTR_IV_SIZE = 16;
-
- /** Length HMAC tag */
- public static final int HMAC_TAG_SIZE = 8;
-
- /**
- * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
- */
- private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
-
- @VisibleForTesting
- static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
-
- // Lazily instantiated when {@link #getInstance()} is called.
- @Nullable private static CryptorImpIdentityV1 sCryptor;
-
- /** Returns an instance of CryptorImpIdentityV1. */
- public static CryptorImpIdentityV1 getInstance() {
- if (sCryptor == null) {
- sCryptor = new CryptorImpIdentityV1();
- }
- return sCryptor;
- }
-
- @Nullable
- @Override
- public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
- if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
- Log.w(TAG, "Illegal authenticity key size");
- return null;
- }
-
- // Generates a 32 bytes encryption key from authenticity_key
- byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
- if (encryptionKey == null) {
- Log.e(TAG, "Failed to generate encryption key.");
- return null;
- }
-
- // Encrypts the data using the encryption key
- SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
- Cipher cipher;
- try {
- cipher = Cipher.getInstance(CIPHER_ALGORITHM);
- } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
- Log.e(TAG, "Failed to encrypt with secret key.", e);
- return null;
- }
- byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
- if (esalt == null) {
- Log.e(TAG, "Failed to generate salt.");
- return null;
- }
- try {
- cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(esalt));
- } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
- Log.e(TAG, "Failed to initialize cipher.", e);
- return null;
- }
- try {
- return cipher.doFinal(data);
- } catch (IllegalBlockSizeException | BadPaddingException e) {
- Log.e(TAG, "Failed to encrypt with secret key.", e);
- return null;
- }
- }
-
- @Nullable
- @Override
- public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
- if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
- Log.w(TAG, "Illegal authenticity key size");
- return null;
- }
-
- // Generates a 32 bytes encryption key from authenticity_key
- byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
- if (encryptionKey == null) {
- Log.e(TAG, "Failed to generate encryption key.");
- return null;
- }
-
- // Decrypts the data using the encryption key
- SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
- Cipher cipher;
- try {
- cipher = Cipher.getInstance(CIPHER_ALGORITHM);
- } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
- Log.e(TAG, "Failed to get cipher instance.", e);
- return null;
- }
- byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
- if (esalt == null) {
- return null;
- }
- try {
- cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(esalt));
- } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
- Log.e(TAG, "Failed to initialize cipher.", e);
- return null;
- }
-
- try {
- return cipher.doFinal(encryptedData);
- } catch (IllegalBlockSizeException | BadPaddingException e) {
- Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
- return null;
- }
- }
-
- /**
- * Generates a digital signature for the data.
- *
- * @return signature {@code null} if failed to sign
- */
- @Nullable
- @Override
- public byte[] sign(byte[] data, byte[] salt) {
- if (data == null) {
- Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
- return null;
- }
-
- // Generates a 8 bytes HMAC tag
- return Cryptor.computeHkdf(data, salt, HMAC_TAG_SIZE);
- }
-
- /**
- * Generates a digital signature for the data.
- * Uses KTAG_IV as salt value.
- */
- @Nullable
- public byte[] sign(byte[] data) {
- // Generates a 8 bytes HMAC tag
- return sign(data, KTAG_IV);
- }
-
- @Override
- public boolean verify(byte[] data, byte[] key, byte[] signature) {
- return Arrays.equals(sign(data, key), signature);
- }
-
- /**
- * Verifies the signature generated by data and key, with the original signed data. Uses
- * KTAG_IV as salt value.
- */
- public boolean verify(byte[] data, byte[] signature) {
- return verify(data, KTAG_IV, signature);
- }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
deleted file mode 100644
index 15073fb..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright (C) 2022 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.server.nearby.util.encryption;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.security.keystore.KeyProperties;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for encryption and decryption.
- */
-public class CryptorImpV1 extends Cryptor {
-
- /**
- * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
- */
- private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
-
- @VisibleForTesting
- static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
-
- /** Length of encryption key required by AES/GCM encryption. */
- private static final int ENCRYPTION_KEY_SIZE = 32;
-
- /** Length of salt required by AES/GCM encryption. */
- private static final int AES_CTR_IV_SIZE = 16;
-
- /** Length HMAC tag */
- public static final int HMAC_TAG_SIZE = 16;
-
- // 3 16 byte arrays known by both the encryptor and decryptor.
- private static final byte[] AK_IV =
- new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
- private static final byte[] ASALT_IV =
- new byte[] {111, 48, -83, -79, -10, -102, -16, 73, 43, 55, 102, -127, 58, -19, -113, 4};
- private static final byte[] HK_IV =
- new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
-
- // Lazily instantiated when {@link #getInstance()} is called.
- @Nullable private static CryptorImpV1 sCryptor;
-
- /** Returns an instance of CryptorImpV1. */
- public static CryptorImpV1 getInstance() {
- if (sCryptor == null) {
- sCryptor = new CryptorImpV1();
- }
- return sCryptor;
- }
-
- private CryptorImpV1() {
- }
-
- @Nullable
- @Override
- public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
- if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
- Log.w(TAG, "Illegal authenticity key size");
- return null;
- }
-
- // Generates a 32 bytes encryption key from authenticity_key
- byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
- if (encryptionKey == null) {
- Log.e(TAG, "Failed to generate encryption key.");
- return null;
- }
-
- // Encrypts the data using the encryption key
- SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
- Cipher cipher;
- try {
- cipher = Cipher.getInstance(CIPHER_ALGORITHM);
- } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
- Log.e(TAG, "Failed to encrypt with secret key.", e);
- return null;
- }
- byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
- if (asalt == null) {
- Log.e(TAG, "Failed to generate salt.");
- return null;
- }
- try {
- cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(asalt));
- } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
- Log.e(TAG, "Failed to initialize cipher.", e);
- return null;
- }
- try {
- return cipher.doFinal(data);
- } catch (IllegalBlockSizeException | BadPaddingException e) {
- Log.e(TAG, "Failed to encrypt with secret key.", e);
- return null;
- }
- }
-
- @Nullable
- @Override
- public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
- if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
- Log.w(TAG, "Illegal authenticity key size");
- return null;
- }
-
- // Generates a 32 bytes encryption key from authenticity_key
- byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
- if (encryptionKey == null) {
- Log.e(TAG, "Failed to generate encryption key.");
- return null;
- }
-
- // Decrypts the data using the encryption key
- SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
- Cipher cipher;
- try {
- cipher = Cipher.getInstance(CIPHER_ALGORITHM);
- } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
- Log.e(TAG, "Failed to get cipher instance.", e);
- return null;
- }
- byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
- if (asalt == null) {
- return null;
- }
- try {
- cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(asalt));
- } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
- Log.e(TAG, "Failed to initialize cipher.", e);
- return null;
- }
-
- try {
- return cipher.doFinal(encryptedData);
- } catch (IllegalBlockSizeException | BadPaddingException e) {
- Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
- return null;
- }
- }
-
- @Override
- @Nullable
- public byte[] sign(byte[] data, byte[] key) {
- return generateHmacTag(data, key);
- }
-
- @Override
- public int getSignatureLength() {
- return HMAC_TAG_SIZE;
- }
-
- @Override
- public boolean verify(byte[] data, byte[] key, byte[] signature) {
- return Arrays.equals(sign(data, key), signature);
- }
-
- /** Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
- * is equal to HMAC tag in advertisement to see data integrity. */
- @Nullable
- @VisibleForTesting
- byte[] generateHmacTag(byte[] data, byte[] authenticityKey) {
- if (data == null || authenticityKey == null) {
- Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
- return null;
- }
-
- if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
- Log.e(TAG, "Illegal authenticity key size");
- return null;
- }
-
- // Generates a 32 bytes HMAC key from authenticity_key
- byte[] hmacKey = Cryptor.computeHkdf(authenticityKey, HK_IV, AES_CTR_IV_SIZE);
- if (hmacKey == null) {
- Log.e(TAG, "Failed to generate HMAC key.");
- return null;
- }
-
- // Generates a 16 bytes HMAC tag from authenticity_key
- return Cryptor.computeHkdf(data, hmacKey, HMAC_TAG_SIZE);
- }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java
new file mode 100644
index 0000000..3dbf85c
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2022 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.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.util.ArrayUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * MIC encryption and decryption for {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1}
+ * advertisement
+ */
+public class CryptorMicImp extends Cryptor {
+
+ public static final int MIC_LENGTH = 16;
+
+ private static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+ private static final byte[] AES_KEY_INFO_BYTES = "Unsigned Section AES key".getBytes(
+ StandardCharsets.US_ASCII);
+ private static final byte[] ADV_NONCE_INFO_BYTES_SALT_DE = "Unsigned Section IV".getBytes(
+ StandardCharsets.US_ASCII);
+ private static final byte[] ADV_NONCE_INFO_BYTES_ENCRYPTION_INFO_DE =
+ "V1 derived salt".getBytes(StandardCharsets.US_ASCII);
+ private static final byte[] METADATA_KEY_HMAC_KEY_INFO_BYTES =
+ "Unsigned Section metadata key HMAC key".getBytes(StandardCharsets.US_ASCII);
+ private static final byte[] MIC_HMAC_KEY_INFO_BYTES = "Unsigned Section HMAC key".getBytes(
+ StandardCharsets.US_ASCII);
+ private static final int AES_KEY_SIZE = 16;
+ private static final int ADV_NONCE_SIZE_SALT_DE = 16;
+ private static final int ADV_NONCE_SIZE_ENCRYPTION_INFO_DE = 12;
+ private static final int HMAC_KEY_SIZE = 32;
+
+ // Lazily instantiated when {@link #getInstance()} is called.
+ @Nullable
+ private static CryptorMicImp sCryptor;
+
+ private CryptorMicImp() {
+ }
+
+ /** Returns an instance of CryptorImpV1. */
+ public static CryptorMicImp getInstance() {
+ if (sCryptor == null) {
+ sCryptor = new CryptorMicImp();
+ }
+ return sCryptor;
+ }
+
+ /**
+ * Generate the meta data encryption key tag
+ * @param metadataEncryptionKey used as identity
+ * @param keySeed authenticity key saved in local and shared credential
+ * @return bytes generated by hmac or {@code null} when there is an error
+ */
+ @Nullable
+ public static byte[] generateMetadataEncryptionKeyTag(byte[] metadataEncryptionKey,
+ byte[] keySeed) {
+ try {
+ byte[] metadataKeyHmacKey = generateMetadataKeyHmacKey(keySeed);
+ return Cryptor.generateHmac(/* algorithm= */ HMAC_SHA256_ALGORITHM, /* input= */
+ metadataEncryptionKey, /* key= */ metadataKeyHmacKey);
+ } catch (GeneralSecurityException e) {
+ Log.e(TAG, "Failed to generate Metadata encryption key tag.", e);
+ return null;
+ }
+ }
+
+ /**
+ * @param salt from the 2 bytes Salt Data Element
+ */
+ @Nullable
+ public static byte[] generateAdvNonce(byte[] salt) throws GeneralSecurityException {
+ return Cryptor.computeHkdf(
+ /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+ /* ikm = */ salt,
+ /* salt= */ NP_HKDF_SALT,
+ /* info= */ ADV_NONCE_INFO_BYTES_SALT_DE,
+ /* size= */ ADV_NONCE_SIZE_SALT_DE);
+ }
+
+ /** Generates the 12 bytes nonce with salt from the 2 bytes Salt Data Element */
+ @Nullable
+ public static byte[] generateAdvNonce(byte[] salt, int deIndex)
+ throws GeneralSecurityException {
+ // go/nearby-specs-working-doc
+ // Indices are encoded as big-endian unsigned 32-bit integers, starting at 1.
+ // Index 0 is reserved
+ byte[] indexBytes = new byte[4];
+ indexBytes[3] = (byte) deIndex;
+ byte[] info =
+ ArrayUtils.concatByteArrays(ADV_NONCE_INFO_BYTES_ENCRYPTION_INFO_DE, indexBytes);
+ return Cryptor.computeHkdf(
+ /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+ /* ikm = */ salt,
+ /* salt= */ NP_HKDF_SALT,
+ /* info= */ info,
+ /* size= */ ADV_NONCE_SIZE_ENCRYPTION_INFO_DE);
+ }
+
+ @Nullable
+ @Override
+ public byte[] encrypt(byte[] input, byte[] iv, byte[] keySeed) {
+ if (input == null || iv == null || keySeed == null) {
+ return null;
+ }
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+
+ byte[] aesKey;
+ try {
+ aesKey = generateAesKey(keySeed);
+ } catch (GeneralSecurityException e) {
+ Log.e(TAG, "Encryption failed because failed to generate the AES key.", e);
+ return null;
+ }
+ if (aesKey == null) {
+ Log.i(TAG, "Failed to generate the AES key.");
+ return null;
+ }
+ SecretKey secretKey = new SecretKeySpec(aesKey, ENCRYPT_ALGORITHM);
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+
+ }
+ try {
+ return cipher.doFinal(input);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public byte[] decrypt(byte[] encryptedData, byte[] iv, byte[] keySeed) {
+ if (encryptedData == null || iv == null || keySeed == null) {
+ return null;
+ }
+
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to get cipher instance.", e);
+ return null;
+ }
+ byte[] aesKey;
+ try {
+ aesKey = generateAesKey(keySeed);
+ } catch (GeneralSecurityException e) {
+ Log.e(TAG, "Decryption failed because failed to generate the AES key.", e);
+ return null;
+ }
+ SecretKey secretKey = new SecretKeySpec(aesKey, ENCRYPT_ALGORITHM);
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+
+ try {
+ return cipher.doFinal(encryptedData);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+ return null;
+ }
+ }
+
+ @Override
+ @Nullable
+ public byte[] sign(byte[] data, byte[] key) {
+ byte[] res = generateHmacTag(data, key);
+ return res;
+ }
+
+ @Override
+ public int getSignatureLength() {
+ return MIC_LENGTH;
+ }
+
+ @Override
+ public boolean verify(byte[] data, byte[] key, byte[] signature) {
+ return Arrays.equals(sign(data, key), signature);
+ }
+
+ /**
+ * Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
+ * is equal to HMAC tag in advertisement to see data integrity.
+ *
+ * @param input concatenated advertisement UUID, header, section header, derived salt, and
+ * section content
+ * @param keySeed the MIC HMAC key is calculated using the derived key
+ * @return the first 16 bytes of HMAC-SHA256 result
+ */
+ @Nullable
+ @VisibleForTesting
+ byte[] generateHmacTag(byte[] input, byte[] keySeed) {
+ try {
+ if (input == null || keySeed == null) {
+ return null;
+ }
+ byte[] micHmacKey = generateMicHmacKey(keySeed);
+ byte[] hmac = Cryptor.generateHmac(/* algorithm= */ HMAC_SHA256_ALGORITHM, /* input= */
+ input, /* key= */ micHmacKey);
+ if (ArrayUtils.isEmpty(hmac)) {
+ return null;
+ }
+ return Arrays.copyOf(hmac, MIC_LENGTH);
+ } catch (GeneralSecurityException e) {
+ Log.e(TAG, "Failed to generate mic hmac key.", e);
+ return null;
+ }
+ }
+
+ @Nullable
+ private static byte[] generateAesKey(byte[] keySeed) throws GeneralSecurityException {
+ return Cryptor.computeHkdf(
+ /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+ /* ikm = */ keySeed,
+ /* salt= */ NP_HKDF_SALT,
+ /* info= */ AES_KEY_INFO_BYTES,
+ /* size= */ AES_KEY_SIZE);
+ }
+
+ private static byte[] generateMetadataKeyHmacKey(byte[] keySeed)
+ throws GeneralSecurityException {
+ return generateHmacKey(keySeed, METADATA_KEY_HMAC_KEY_INFO_BYTES);
+ }
+
+ private static byte[] generateMicHmacKey(byte[] keySeed) throws GeneralSecurityException {
+ return generateHmacKey(keySeed, MIC_HMAC_KEY_INFO_BYTES);
+ }
+
+ private static byte[] generateHmacKey(byte[] keySeed, byte[] info)
+ throws GeneralSecurityException {
+ return Cryptor.computeHkdf(
+ /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+ /* ikm = */ keySeed,
+ /* salt= */ NP_HKDF_SALT,
+ /* info= */ info,
+ /* size= */ HMAC_KEY_SIZE);
+ }
+}
diff --git a/nearby/service/proto/src/presence/blefilter.proto b/nearby/service/proto/src/presence/blefilter.proto
index e1bf455..bf9357b 100644
--- a/nearby/service/proto/src/presence/blefilter.proto
+++ b/nearby/service/proto/src/presence/blefilter.proto
@@ -115,6 +115,9 @@
repeated DataElement data_element = 7;
optional bytes ble_service_data = 8;
optional ResultType result_type = 9;
+ // Timestamp when the device is discovered, in nanoseconds,
+ // relative to Android SystemClock.elapsedRealtimeNanos().
+ optional uint64 timestamp_ns = 10;
}
message BleFilterResults {
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
index 5ddfed3..644e178 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
@@ -25,6 +25,7 @@
import static com.google.common.truth.Truth.assertThat;
+import android.os.Build;
import android.provider.DeviceConfig;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -68,7 +69,12 @@
"3", false);
Thread.sleep(500);
- assertThat(mNearbyConfiguration.isTestAppSupported()).isTrue();
+ // TestAppSupported Flag can only be set to true in user-debug devices.
+ if (Build.isDebuggable()) {
+ assertThat(mNearbyConfiguration.isTestAppSupported()).isTrue();
+ } else {
+ assertThat(mNearbyConfiguration.isTestAppSupported()).isFalse();
+ }
assertThat(mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()).isTrue();
assertThat(mNearbyConfiguration.getNanoAppMinVersion()).isEqualTo(3);
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
index bc38210..7ff7b13 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
@@ -24,7 +24,9 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.UiAutomation;
import android.content.Context;
@@ -34,6 +36,7 @@
import android.nearby.PresenceBroadcastRequest;
import android.nearby.PresenceCredential;
import android.nearby.PrivateCredential;
+import android.os.IBinder;
import android.provider.DeviceConfig;
import androidx.test.core.app.ApplicationProvider;
@@ -74,6 +77,8 @@
IBroadcastListener mBroadcastListener;
@Mock
BleBroadcastProvider mBleBroadcastProvider;
+ @Mock
+ IBinder mBinder;
private Context mContext;
private BroadcastProviderManager mBroadcastProviderManager;
private BroadcastRequest mBroadcastRequest;
@@ -82,9 +87,12 @@
@Before
public void setUp() {
+ when(mBroadcastListener.asBinder()).thenReturn(mBinder);
mUiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
DeviceConfig.setProperty(
NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, "true", false);
+ DeviceConfig.setProperty(
+ NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
mContext = ApplicationProvider.getApplicationContext();
mBroadcastProviderManager = new BroadcastProviderManager(
@@ -104,15 +112,28 @@
}
@Test
- public void testStartAdvertising() {
+ public void testStartAdvertising() throws Exception {
mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
verify(mBleBroadcastProvider).start(eq(BroadcastRequest.PRESENCE_VERSION_V0),
any(byte[].class), any(BleBroadcastProvider.BroadcastListener.class));
+ verify(mBinder).linkToDeath(any(), eq(0));
}
@Test
public void testStopAdvertising() {
+ mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
mBroadcastProviderManager.stopBroadcast(mBroadcastListener);
+ verify(mBinder).unlinkToDeath(any(), eq(0));
+ }
+
+ @Test
+ public void testRegisterAdvertising_twoTimes_fail() throws Exception {
+ IBroadcastListener newListener = mock(IBroadcastListener.class);
+ IBinder newBinder = mock(IBinder.class);
+ when(newListener.asBinder()).thenReturn(newBinder);
+ mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
+ mBroadcastProviderManager.startBroadcast(mBroadcastRequest, newListener);
+ verify(newListener).onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
}
@Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
index aa0dad3..0ca571a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
@@ -16,6 +16,7 @@
package com.android.server.nearby.managers;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
@@ -23,10 +24,12 @@
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.nearby.DataElement;
import android.nearby.IScanListener;
@@ -36,6 +39,8 @@
import android.nearby.ScanRequest;
import android.os.IBinder;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.provider.BleDiscoveryProvider;
import com.android.server.nearby.provider.ChreCommunication;
@@ -86,6 +91,8 @@
DiscoveryProviderManagerLegacy.ScanListenerDeathRecipient mScanListenerDeathRecipient;
@Mock
IBinder mIBinder;
+ @Mock
+ BluetoothAdapter mBluetoothAdapter;
private DiscoveryProviderManagerLegacy mDiscoveryProviderManager;
private Map<IBinder, DiscoveryProviderManagerLegacy.ScanListenerRecord>
mScanTypeScanListenerRecordMap;
@@ -135,10 +142,15 @@
@Before
public void setup() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+ READ_DEVICE_CONFIG);
MockitoAnnotations.initMocks(this);
when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+ when(mInjector.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
mScanTypeScanListenerRecordMap = new HashMap<>();
mDiscoveryProviderManager =
@@ -164,6 +176,13 @@
}
@Test
+ public void test_enableBleWhenBleOff() throws Exception {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ mDiscoveryProviderManager.init();
+ verify(mBluetoothAdapter, times(1)).enableBLE();
+ }
+
+ @Test
public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
when(mChreDiscoveryProvider.available()).thenReturn(true);
@@ -375,4 +394,62 @@
.isTrue();
assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
}
+
+ @Test
+ public void isBluetoothEnabledTest_bluetoothEnabled() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void isBluetoothEnabledTest_bleEnabled() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(true);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void enabledTest_enabled() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void enabledTest_enableFailed() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(false);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+ }
+
+ @Test
+ public void enabledTest_scanIsOn() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void enabledTest_failed() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
index 7ecf631..7cea34a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
@@ -16,6 +16,7 @@
package com.android.server.nearby.managers;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
@@ -24,10 +25,12 @@
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.nearby.DataElement;
import android.nearby.IScanListener;
@@ -37,6 +40,8 @@
import android.nearby.ScanRequest;
import android.os.IBinder;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.provider.BleDiscoveryProvider;
import com.android.server.nearby.provider.ChreCommunication;
@@ -80,6 +85,8 @@
CallerIdentity mCallerIdentity;
@Mock
IBinder mIBinder;
+ @Mock
+ BluetoothAdapter mBluetoothAdapter;
private Executor mExecutor;
private DiscoveryProviderManager mDiscoveryProviderManager;
@@ -128,12 +135,17 @@
@Before
public void setup() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+ READ_DEVICE_CONFIG);
MockitoAnnotations.initMocks(this);
mExecutor = Executors.newSingleThreadExecutor();
when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
when(mScanListener.asBinder()).thenReturn(mIBinder);
+ when(mInjector.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
mDiscoveryProviderManager =
new DiscoveryProviderManager(mContext, mExecutor, mInjector,
@@ -157,6 +169,13 @@
}
@Test
+ public void test_enableBleWhenBleOff() throws Exception {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ mDiscoveryProviderManager.init();
+ verify(mBluetoothAdapter, times(1)).enableBLE();
+ }
+
+ @Test
public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
reset(mBluetoothController);
when(mChreDiscoveryProvider.available()).thenReturn(true);
@@ -336,4 +355,62 @@
.isTrue();
assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
}
+
+ @Test
+ public void isBluetoothEnabledTest_bluetoothEnabled() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void isBluetoothEnabledTest_bleEnabled() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(true);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void enabledTest_enabled() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void enabledTest_enableFailed() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+ when(mBluetoothAdapter.enableBLE()).thenReturn(false);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+ }
+
+ @Test
+ public void enabledTest_scanIsOn() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+ }
+
+ @Test
+ public void enabledTest_failed() {
+ when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+ when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+ assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
new file mode 100644
index 0000000..6ec7c57
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.server.nearby.presence.EncryptionInfo.EncodingScheme;
+import com.android.server.nearby.util.ArrayUtils;
+
+import org.junit.Test;
+
+
+/**
+ * Unit test for {@link EncryptionInfo}.
+ */
+public class EncryptionInfoTest {
+ private static final byte[] SALT =
+ new byte[]{25, -21, 35, -108, -26, -126, 99, 60, 110, 45, -116, 34, 91, 126, -23, 127};
+
+ @Test
+ public void test_illegalLength() {
+ byte[] data = new byte[]{1, 2};
+ assertThrows(IllegalArgumentException.class, () -> new EncryptionInfo(data));
+ }
+
+ @Test
+ public void test_illegalEncodingScheme() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new EncryptionInfo(ArrayUtils.append((byte) 0b10110000, SALT)));
+ assertThrows(IllegalArgumentException.class,
+ () -> new EncryptionInfo(ArrayUtils.append((byte) 0b01101000, SALT)));
+ }
+
+ @Test
+ public void test_getMethods_signature() {
+ byte[] data = ArrayUtils.append((byte) 0b10001000, SALT);
+ EncryptionInfo info = new EncryptionInfo(data);
+ assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.SIGNATURE);
+ assertThat(info.getSalt()).isEqualTo(SALT);
+ }
+
+ @Test
+ public void test_getMethods_mic() {
+ byte[] data = ArrayUtils.append((byte) 0b10000000, SALT);
+ EncryptionInfo info = new EncryptionInfo(data);
+ assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.MIC);
+ assertThat(info.getSalt()).isEqualTo(SALT);
+ }
+ @Test
+ public void test_toBytes() {
+ byte[] data = EncryptionInfo.toByte(EncodingScheme.MIC, SALT);
+ EncryptionInfo info = new EncryptionInfo(data);
+ assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.MIC);
+ assertThat(info.getSalt()).isEqualTo(SALT);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
index 895df69..3f00a42 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
@@ -16,6 +16,8 @@
package com.android.server.nearby.presence;
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID_BYTES;
+
import static com.google.common.truth.Truth.assertThat;
import android.nearby.BroadcastRequest;
@@ -25,19 +27,22 @@
import android.nearby.PrivateCredential;
import android.nearby.PublicCredential;
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
-import com.android.server.nearby.util.encryption.CryptorImpV1;
+import com.android.server.nearby.util.ArrayUtils;
+import com.android.server.nearby.util.encryption.CryptorMicImp;
import org.junit.Before;
import org.junit.Test;
import java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ExtendedAdvertisementTest {
+ private static final int EXTENDED_ADVERTISEMENT_BYTE_LENGTH = 67;
private static final int IDENTITY_TYPE = PresenceCredential.IDENTITY_TYPE_PRIVATE;
+ private static final int DATA_TYPE_ACTION = 6;
private static final int DATA_TYPE_MODEL_ID = 7;
private static final int DATA_TYPE_BLE_ADDRESS = 101;
private static final int DATA_TYPE_PUBLIC_IDENTITY = 3;
@@ -49,18 +54,23 @@
private static final DataElement BLE_ADDRESS_ELEMENT =
new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS);
- private static final byte[] IDENTITY =
- new byte[]{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+ private static final byte[] METADATA_ENCRYPTION_KEY =
+ new byte[]{-39, -55, 115, 78, -57, 40, 115, 0, -112, 86, -86, 7, -42, 68, 11, 12};
private static final int MEDIUM_TYPE_BLE = 0;
private static final byte[] SALT = {2, 3};
+
private static final int PRESENCE_ACTION_1 = 1;
private static final int PRESENCE_ACTION_2 = 2;
+ private static final DataElement PRESENCE_ACTION_DE_1 =
+ new DataElement(DATA_TYPE_ACTION, new byte[]{PRESENCE_ACTION_1});
+ private static final DataElement PRESENCE_ACTION_DE_2 =
+ new DataElement(DATA_TYPE_ACTION, new byte[]{PRESENCE_ACTION_2});
private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
private static final byte[] AUTHENTICITY_KEY =
new byte[]{-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
private static final byte[] PUBLIC_KEY =
- new byte[] {
+ new byte[]{
48, 89, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6, 8, 42, -122, 72, -50, 61,
66, 0, 4, -56, -39, -92, 69, 0, 52, 23, 67, 83, -14, 75, 52, -14, -5, -41, 48,
-83, 31, 42, -39, 102, -13, 22, -73, -73, 86, 30, -96, -84, -13, 4, 122, 104,
@@ -68,7 +78,7 @@
123, 41, -119, -25, 1, -112, 112
};
private static final byte[] ENCRYPTED_METADATA_BYTES =
- new byte[] {
+ new byte[]{
-44, -25, -95, -124, -7, 90, 116, -8, 7, -120, -23, -22, -106, -44, -19, 61,
-18, 39, 29, 78, 108, -11, -39, 85, -30, 64, -99, 102, 65, 37, -42, 114, -37,
88, -112, 8, -75, -53, 23, -16, -104, 67, 49, 48, -53, 73, -109, 44, -23, -11,
@@ -78,23 +88,65 @@
-4, -46, -30, -85, -50, 100, 46, -66, -128, 7, 66, 9, 88, 95, 12, -13, 81, -91,
};
private static final byte[] METADATA_ENCRYPTION_KEY_TAG =
- new byte[] {-126, -104, 1, -1, 26, -46, -68, -86};
+ new byte[]{-100, 102, -35, -99, 66, -85, -55, -58, -52, 11, -74, 102, 109, -89, 1, -34,
+ 45, 43, 107, -60, 99, -21, 28, 34, 31, -100, -96, 108, 108, -18, 107, 5};
+
+ private static final String ENCODED_ADVERTISEMENT_ENCRYPTION_INFO =
+ "2091911000DE2A89ED98474AF3E41E48487E8AEBDE90014C18BCB9F9AAC5C11A1BE00A10A5DCD2C49A74BE"
+ + "BAF0FE72FD5053B9DF8B9976C80BE0DCE8FEE83F1BFA9A89EB176CA48EE4ED5D15C6CDAD6B9E"
+ + "41187AA6316D7BFD8E454A53971AC00836F7AB0771FF0534050037D49C6AEB18CF9F8590E5CD"
+ + "EE2FBC330FCDC640C63F0735B7E3F02FE61A0496EF976A158AD3455D";
+ private static final byte[] METADATA_ENCRYPTION_KEY_TAG_2 =
+ new byte[]{-54, -39, 41, 16, 61, 79, -116, 14, 94, 0, 84, 45, 26, -108, 66, -48, 124,
+ -81, 61, 56, -98, -47, 14, -19, 116, 106, -27, 123, -81, 49, 83, -42};
+
private static final String DEVICE_NAME = "test_device";
+ private static final byte[] SALT_16 =
+ ArrayUtils.stringToBytes("DE2A89ED98474AF3E41E48487E8AEBDE");
+ private static final byte[] AUTHENTICITY_KEY_2 = ArrayUtils.stringToBytes(
+ "959D2F3CAB8EE4A2DEB0255C03762CF5D39EB919300420E75A089050FB025E20");
+ private static final byte[] METADATA_ENCRYPTION_KEY_2 = ArrayUtils.stringToBytes(
+ "EF5E9A0867560E52AE1F05FCA7E48D29");
+
+ private static final DataElement DE1 = new DataElement(571, ArrayUtils.stringToBytes(
+ "537F96FD94E13BE589F0141145CFC0EEC4F86FBDB2"));
+ private static final DataElement DE2 = new DataElement(541, ArrayUtils.stringToBytes(
+ "D301FFB24B5B"));
+ private static final DataElement DE3 = new DataElement(51, ArrayUtils.stringToBytes(
+ "EA95F07C25B75C04E1B2B8731F6A55BA379FB141"));
+ private static final DataElement DE4 = new DataElement(729, ArrayUtils.stringToBytes(
+ "2EFD3101E2311BBB108F0A7503907EAF0C2EAAA60CDA8D33A294C4CEACE0"));
+ private static final DataElement DE5 = new DataElement(411, ArrayUtils.stringToBytes("B0"));
+
private PresenceBroadcastRequest.Builder mBuilder;
+ private PresenceBroadcastRequest.Builder mBuilderCredentialInfo;
private PrivateCredential mPrivateCredential;
+ private PrivateCredential mPrivateCredential2;
+
private PublicCredential mPublicCredential;
+ private PublicCredential mPublicCredential2;
@Before
public void setUp() {
mPrivateCredential =
- new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, IDENTITY, DEVICE_NAME)
+ new PrivateCredential.Builder(
+ SECRET_ID, AUTHENTICITY_KEY, METADATA_ENCRYPTION_KEY, DEVICE_NAME)
+ .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+ .build();
+ mPrivateCredential2 =
+ new PrivateCredential.Builder(
+ SECRET_ID, AUTHENTICITY_KEY_2, METADATA_ENCRYPTION_KEY_2, DEVICE_NAME)
.setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
.build();
mPublicCredential =
new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG)
.build();
+ mPublicCredential2 =
+ new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY_2, PUBLIC_KEY,
+ ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG_2)
+ .build();
mBuilder =
new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
SALT, mPrivateCredential)
@@ -103,6 +155,16 @@
.addAction(PRESENCE_ACTION_2)
.addExtendedProperty(new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS))
.addExtendedProperty(new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA));
+
+ mBuilderCredentialInfo =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ SALT_16, mPrivateCredential2)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+ .addExtendedProperty(DE1)
+ .addExtendedProperty(DE2)
+ .addExtendedProperty(DE3)
+ .addExtendedProperty(DE4)
+ .addExtendedProperty(DE5);
}
@Test
@@ -112,36 +174,50 @@
assertThat(originalAdvertisement.getActions())
.containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
- assertThat(originalAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(originalAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
- assertThat(originalAdvertisement.getLength()).isEqualTo(66);
assertThat(originalAdvertisement.getVersion()).isEqualTo(
BroadcastRequest.PRESENCE_VERSION_V1);
assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
assertThat(originalAdvertisement.getDataElements())
- .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ .containsExactly(PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2,
+ MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ assertThat(originalAdvertisement.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
+ }
+
+ @Test
+ public void test_createFromRequest_credentialInfo() {
+ ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+ mBuilderCredentialInfo.build());
+
+ assertThat(originalAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY_2);
+ assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(originalAdvertisement.getVersion()).isEqualTo(
+ BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT_16);
+ assertThat(originalAdvertisement.getDataElements())
+ .containsExactly(DE1, DE2, DE3, DE4, DE5);
}
@Test
public void test_createFromRequest_encodeAndDecode() {
ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
mBuilder.build());
-
byte[] generatedBytes = originalAdvertisement.toBytes();
-
ExtendedAdvertisement newAdvertisement =
ExtendedAdvertisement.fromBytes(generatedBytes, mPublicCredential);
assertThat(newAdvertisement.getActions())
.containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
- assertThat(newAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(newAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
assertThat(newAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
- assertThat(newAdvertisement.getLength()).isEqualTo(66);
+ assertThat(newAdvertisement.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
assertThat(newAdvertisement.getVersion()).isEqualTo(
BroadcastRequest.PRESENCE_VERSION_V1);
assertThat(newAdvertisement.getSalt()).isEqualTo(SALT);
assertThat(newAdvertisement.getDataElements())
- .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT,
+ PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2);
}
@Test
@@ -170,45 +246,77 @@
.setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
.addAction(PRESENCE_ACTION_1);
assertThat(ExtendedAdvertisement.createFromRequest(builder2.build())).isNull();
-
- // empty action
- PresenceBroadcastRequest.Builder builder3 =
- new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
- SALT, mPrivateCredential)
- .setVersion(BroadcastRequest.PRESENCE_VERSION_V1);
- assertThat(ExtendedAdvertisement.createFromRequest(builder3.build())).isNull();
}
@Test
- public void test_toBytes() {
+ public void test_toBytesSalt() throws Exception {
ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
assertThat(adv.toBytes()).isEqualTo(getExtendedAdvertisementByteArray());
}
@Test
- public void test_fromBytes() {
+ public void test_fromBytesSalt() throws Exception {
byte[] originalBytes = getExtendedAdvertisementByteArray();
ExtendedAdvertisement adv =
ExtendedAdvertisement.fromBytes(originalBytes, mPublicCredential);
assertThat(adv.getActions())
.containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
- assertThat(adv.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(adv.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
- assertThat(adv.getLength()).isEqualTo(66);
+ assertThat(adv.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
assertThat(adv.getVersion()).isEqualTo(
BroadcastRequest.PRESENCE_VERSION_V1);
assertThat(adv.getSalt()).isEqualTo(SALT);
assertThat(adv.getDataElements())
- .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT,
+ PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2);
+ }
+
+ @Test
+ public void test_toBytesCredentialElement() {
+ ExtendedAdvertisement adv =
+ ExtendedAdvertisement.createFromRequest(mBuilderCredentialInfo.build());
+ assertThat(ArrayUtils.bytesToStringUppercase(adv.toBytes())).isEqualTo(
+ ENCODED_ADVERTISEMENT_ENCRYPTION_INFO);
+ }
+
+ @Test
+ public void test_fromBytesCredentialElement() {
+ ExtendedAdvertisement adv =
+ ExtendedAdvertisement.fromBytes(
+ ArrayUtils.stringToBytes(ENCODED_ADVERTISEMENT_ENCRYPTION_INFO),
+ mPublicCredential2);
+ assertThat(adv.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY_2);
+ assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(adv.getVersion()).isEqualTo(BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(adv.getSalt()).isEqualTo(SALT_16);
+ assertThat(adv.getDataElements()).containsExactly(DE1, DE2, DE3, DE4, DE5);
+ }
+
+ @Test
+ public void test_fromBytes_metadataTagNotMatched_fail() throws Exception {
+ byte[] originalBytes = getExtendedAdvertisementByteArray();
+ PublicCredential credential =
+ new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
+ ENCRYPTED_METADATA_BYTES,
+ new byte[]{113, 90, -55, 73, 25, -9, 55, -44, 102, 44, 81, -68, 101, 21, 32,
+ 92, -107, 3, 108, 90, 28, -73, 16, 49, -95, -121, 8, -45, -27, 16,
+ 6, 108})
+ .build();
+ ExtendedAdvertisement adv =
+ ExtendedAdvertisement.fromBytes(originalBytes, credential);
+ assertThat(adv).isNull();
}
@Test
public void test_toString() {
ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
assertThat(adv.toString()).isEqualTo("ExtendedAdvertisement:"
- + "<VERSION: 1, length: 66, dataElementCount: 2, identityType: 1, "
- + "identity: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], salt: [2, 3],"
+ + "<VERSION: 1, length: " + EXTENDED_ADVERTISEMENT_BYTE_LENGTH
+ + ", dataElementCount: 4, identityType: 1, "
+ + "identity: " + Arrays.toString(METADATA_ENCRYPTION_KEY)
+ + ", salt: [2, 3],"
+ " actions: [1, 2]>");
}
@@ -222,26 +330,22 @@
assertThat(adv.getDataElements(DATA_TYPE_PUBLIC_IDENTITY)).isEmpty();
}
- private static byte[] getExtendedAdvertisementByteArray() {
- ByteBuffer buffer = ByteBuffer.allocate(66);
+ private static byte[] getExtendedAdvertisementByteArray() throws Exception {
+ CryptorMicImp cryptor = CryptorMicImp.getInstance();
+ ByteBuffer buffer = ByteBuffer.allocate(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
buffer.put((byte) 0b00100000); // Header V1
- buffer.put((byte) 0b00100000); // Salt header: length 2, type 0
+ buffer.put(
+ (byte) (EXTENDED_ADVERTISEMENT_BYTE_LENGTH - 2)); // Section header (section length)
+
// Salt data
- buffer.put(SALT);
+ // Salt header: length 2, type 0
+ byte[] saltBytes = ArrayUtils.concatByteArrays(new byte[]{(byte) 0b00100000}, SALT);
+ buffer.put(saltBytes);
// Identity header: length 16, type 1 (private identity)
- buffer.put(new byte[]{(byte) 0b10010000, (byte) 0b00000001});
- // Identity data
- buffer.put(CryptorImpIdentityV1.getInstance().encrypt(IDENTITY, SALT, AUTHENTICITY_KEY));
+ byte[] identityHeader = new byte[]{(byte) 0b10010000, (byte) 0b00000001};
+ buffer.put(identityHeader);
ByteBuffer deBuffer = ByteBuffer.allocate(28);
- // Action1 header: length 1, type 6
- deBuffer.put(new byte[]{(byte) 0b00010110});
- // Action1 data
- deBuffer.put((byte) PRESENCE_ACTION_1);
- // Action2 header: length 1, type 6
- deBuffer.put(new byte[]{(byte) 0b00010110});
- // Action2 data
- deBuffer.put((byte) PRESENCE_ACTION_2);
// Ble address header: length 7, type 102
deBuffer.put(new byte[]{(byte) 0b10000111, (byte) 0b01100101});
// Ble address data
@@ -250,11 +354,30 @@
deBuffer.put(new byte[]{(byte) 0b10001101, (byte) 0b00000111});
// model id data
deBuffer.put(MODE_ID_DATA);
+ // Action1 header: length 1, type 6
+ deBuffer.put(new byte[]{(byte) 0b00010110});
+ // Action1 data
+ deBuffer.put((byte) PRESENCE_ACTION_1);
+ // Action2 header: length 1, type 6
+ deBuffer.put(new byte[]{(byte) 0b00010110});
+ // Action2 data
+ deBuffer.put((byte) PRESENCE_ACTION_2);
+ byte[] deBytes = deBuffer.array();
+ byte[] nonce = CryptorMicImp.generateAdvNonce(SALT);
+ byte[] ciphertext =
+ cryptor.encrypt(
+ ArrayUtils.concatByteArrays(METADATA_ENCRYPTION_KEY, deBytes),
+ nonce, AUTHENTICITY_KEY);
+ buffer.put(ciphertext);
- byte[] data = deBuffer.array();
- CryptorImpV1 cryptor = CryptorImpV1.getInstance();
- buffer.put(cryptor.encrypt(data, SALT, AUTHENTICITY_KEY));
- buffer.put(cryptor.sign(data, AUTHENTICITY_KEY));
+ byte[] dataToSign = ArrayUtils.concatByteArrays(
+ PRESENCE_UUID_BYTES, /* UUID */
+ new byte[]{(byte) 0b00100000}, /* header */
+ new byte[]{(byte) (EXTENDED_ADVERTISEMENT_BYTE_LENGTH - 2)} /* sectionHeader */,
+ saltBytes, /* salt */
+ nonce, identityHeader, ciphertext);
+ byte[] mic = cryptor.sign(dataToSign, AUTHENTICITY_KEY);
+ buffer.put(mic);
return buffer.array();
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
index 05b556b..0b1a742 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -16,6 +16,7 @@
package com.android.server.nearby.provider;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
@@ -69,7 +70,7 @@
AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
mBleBroadcastProvider.onStartSuccess(settings);
- verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+ verify(mBroadcastListener, atLeast(1)).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
}
@Test
@@ -81,7 +82,7 @@
// advertising set can not be mocked, so we will allow nulls
mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
AdvertisingSetCallback.ADVERTISE_SUCCESS);
- verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+ verify(mBroadcastListener, atLeast(1)).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
}
@Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
index 154441b..590a46e 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
@@ -192,7 +192,12 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testOnNearbyDeviceDiscoveredWithDataElements() {
+ public void testOnNearbyDeviceDiscoveredWithDataElements_TIME() {
+ // The feature only supports user-debug builds.
+ if (!Build.isDebuggable()) {
+ return;
+ }
+
// Disables the setting of test app support
boolean isSupportedTestApp = getDeviceConfigBoolean(
NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
@@ -209,6 +214,7 @@
// First byte is length of service data, padding zeros should be thrown away.
final byte [] bleServiceData = new byte[] {5, 1, 2, 3, 4, 5, 0, 0, 0, 0};
final byte [] testData = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+ final long timestampNs = 1697765417070000000L;
final List<DataElement> expectedExtendedProperties = new ArrayList<>();
expectedExtendedProperties.add(new DataElement(DATA_TYPE_CONNECTION_STATUS_KEY,
@@ -262,6 +268,7 @@
.setValue(ByteString.copyFrom(testData))
.setValueLength(testData.length)
)
+ .setTimestampNs(timestampNs)
.build();
Blefilter.BleFilterResults results =
Blefilter.BleFilterResults.newBuilder().addResult(result).build();
@@ -285,11 +292,18 @@
DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
}
+ // Nanoseconds to Milliseconds
+ assertThat((mNearbyDevice.getValue().getPresenceDevice())
+ .getDiscoveryTimestampMillis()).isEqualTo(timestampNs / 1000000);
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testOnNearbyDeviceDiscoveredWithTestDataElements() {
+ // The feature only supports user-debug builds.
+ if (!Build.isDebuggable()) {
+ return;
+ }
// Enables the setting of test app support
boolean isSupportedTestApp = getDeviceConfigBoolean(
NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
index a759baf..455c432 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
@@ -18,8 +18,6 @@
import static com.google.common.truth.Truth.assertThat;
-import androidx.test.filters.SdkSuppress;
-
import org.junit.Test;
public final class ArrayUtilsTest {
@@ -30,51 +28,58 @@
private static final byte[] BYTES_ALL = new byte[] {7, 9, 8};
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testConcatByteArraysNoInput() {
assertThat(ArrayUtils.concatByteArrays().length).isEqualTo(0);
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testConcatByteArraysOneEmptyArray() {
assertThat(ArrayUtils.concatByteArrays(BYTES_EMPTY).length).isEqualTo(0);
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testConcatByteArraysOneNonEmptyArray() {
assertThat(ArrayUtils.concatByteArrays(BYTES_ONE)).isEqualTo(BYTES_ONE);
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testConcatByteArraysMultipleNonEmptyArrays() {
assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_TWO)).isEqualTo(BYTES_ALL);
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testConcatByteArraysMultipleArrays() {
assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_EMPTY, BYTES_TWO))
.isEqualTo(BYTES_ALL);
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testIsEmptyNull_returnsTrue() {
assertThat(ArrayUtils.isEmpty(null)).isTrue();
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testIsEmpty_returnsTrue() {
assertThat(ArrayUtils.isEmpty(new byte[]{})).isTrue();
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testIsEmpty_returnsFalse() {
assertThat(ArrayUtils.isEmpty(BYTES_ALL)).isFalse();
}
+
+ @Test
+ public void testAppendByte() {
+ assertThat(ArrayUtils.append((byte) 2, BYTES_ONE)).isEqualTo(new byte[]{2, 7, 9});
+ }
+
+ @Test
+ public void testAppendByteNull() {
+ assertThat(ArrayUtils.append((byte) 2, null)).isEqualTo(new byte[]{2});
+ }
+
+ @Test
+ public void testAppendByteToArray() {
+ assertThat(ArrayUtils.append(BYTES_ONE, (byte) 2)).isEqualTo(new byte[]{7, 9, 2});
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
deleted file mode 100644
index f0294fc..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2022 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.server.nearby.util.encryption;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-
-public class CryptorImpIdentityV1Test {
- private static final String TAG = "CryptorImpIdentityV1Test";
- private static final byte[] SALT = new byte[] {102, 22};
- private static final byte[] DATA =
- new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
- private static final byte[] AUTHENTICITY_KEY =
- new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
-
- @Test
- public void test_encrypt_decrypt() {
- Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
- byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
- assertThat(identityCryptor.decrypt(encryptedData, SALT, AUTHENTICITY_KEY)).isEqualTo(DATA);
- }
-
- @Test
- public void test_encryption() {
- Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
- byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
- // for debugging
- Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
-
- assertThat(encryptedData).isEqualTo(getEncryptedData());
- }
-
- @Test
- public void test_decryption() {
- Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
- byte[] decryptedData =
- identityCryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
- // for debugging
- Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
-
- assertThat(decryptedData).isEqualTo(DATA);
- }
-
- @Test
- public void generateHmacTag() {
- CryptorImpIdentityV1 identityCryptor = CryptorImpIdentityV1.getInstance();
- byte[] generatedTag = identityCryptor.sign(DATA);
- byte[] expectedTag = new byte[]{50, 116, 95, -87, 63, 123, -79, -43};
- assertThat(generatedTag).isEqualTo(expectedTag);
- }
-
- private static byte[] getEncryptedData() {
- return new byte[]{6, -31, -32, -123, 43, -92, -47, -110, -65, 126, -15, -51, -19, -43};
- }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
deleted file mode 100644
index 3ca2575..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2022 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.server.nearby.util.encryption;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-
-/**
- * Unit test for {@link CryptorImpV1}
- */
-public final class CryptorImpV1Test {
- private static final String TAG = "CryptorImpV1Test";
- private static final byte[] SALT = new byte[] {102, 22};
- private static final byte[] DATA =
- new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
- private static final byte[] AUTHENTICITY_KEY =
- new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
-
- @Test
- public void test_encryption() {
- Cryptor v1Cryptor = CryptorImpV1.getInstance();
- byte[] encryptedData = v1Cryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
- // for debugging
- Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
-
- assertThat(encryptedData).isEqualTo(getEncryptedData());
- }
-
- @Test
- public void test_encryption_invalidInput() {
- Cryptor v1Cryptor = CryptorImpV1.getInstance();
- assertThat(v1Cryptor.encrypt(DATA, SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
- }
-
- @Test
- public void test_decryption() {
- Cryptor v1Cryptor = CryptorImpV1.getInstance();
- byte[] decryptedData =
- v1Cryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
- // for debugging
- Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
-
- assertThat(decryptedData).isEqualTo(DATA);
- }
-
- @Test
- public void test_decryption_invalidInput() {
- Cryptor v1Cryptor = CryptorImpV1.getInstance();
- assertThat(v1Cryptor.decrypt(getEncryptedData(), SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
- }
-
- @Test
- public void generateSign() {
- CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
- byte[] generatedTag = v1Cryptor.sign(DATA, AUTHENTICITY_KEY);
- byte[] expectedTag = new byte[]{
- 100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
- assertThat(generatedTag).isEqualTo(expectedTag);
- }
-
- @Test
- public void test_verify() {
- CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
- byte[] expectedTag = new byte[]{
- 100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
-
- assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
- assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
- }
-
- @Test
- public void test_generateHmacTag_sameResult() {
- CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
- byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
- assertThat(res1)
- .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
- }
-
- @Test
- public void test_generateHmacTag_nullData() {
- CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
- assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
- }
-
- @Test
- public void test_generateHmacTag_nullKey() {
- CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
- assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
- }
-
- private static byte[] getEncryptedData() {
- return new byte[]{-92, 94, -99, -97, 81, -48, -7, 119, -64, -22, 45, -49, -50, 92};
- }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java
new file mode 100644
index 0000000..b6d2333
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 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.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+/**
+ * Unit test for {@link CryptorMicImp}
+ */
+public final class CryptorMicImpTest {
+ private static final String TAG = "CryptorImpV1Test";
+ private static final byte[] SALT = new byte[]{102, 22};
+ private static final byte[] DATA =
+ new byte[]{107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[]{-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+ private static byte[] getEncryptedData() {
+ return new byte[]{112, 23, -111, 87, 122, -27, 45, -25, -35, 84, -89, 115, 61, 113};
+ }
+
+ @Test
+ public void test_encryption() throws Exception {
+ Cryptor v1Cryptor = CryptorMicImp.getInstance();
+ byte[] encryptedData =
+ v1Cryptor.encrypt(DATA, CryptorMicImp.generateAdvNonce(SALT), AUTHENTICITY_KEY);
+ assertThat(encryptedData).isEqualTo(getEncryptedData());
+ }
+
+ @Test
+ public void test_decryption() throws Exception {
+ Cryptor v1Cryptor = CryptorMicImp.getInstance();
+ byte[] decryptedData =
+ v1Cryptor.decrypt(getEncryptedData(), CryptorMicImp.generateAdvNonce(SALT),
+ AUTHENTICITY_KEY);
+ assertThat(decryptedData).isEqualTo(DATA);
+ }
+
+ @Test
+ public void test_verify() {
+ CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+ byte[] expectedTag = new byte[]{
+ -80, -51, -101, -7, -65, 110, 37, 68, 122, -128, 57, -90, -115, -59, -61, 46};
+ assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
+ assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
+ }
+
+ @Test
+ public void test_generateHmacTag_sameResult() {
+ CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+ byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
+ assertThat(res1)
+ .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
+ }
+
+ @Test
+ public void test_generateHmacTag_nullData() {
+ CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+ assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
+ }
+
+ @Test
+ public void test_generateHmacTag_nullKey() {
+ CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+ assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
index ca612e3..1fb2236 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
@@ -42,7 +42,7 @@
assertThat(res2).hasLength(outputSize);
assertThat(res1).isNotEqualTo(res2);
assertThat(res1)
- .isEqualTo(CryptorImpV1.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
+ .isEqualTo(CryptorMicImp.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
}
@Test
diff --git a/service-t/src/com/android/server/IpSecService.java b/service-t/src/com/android/server/IpSecService.java
index a884840..ea91e64 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.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.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 @@
@VisibleForTesting static final int MAX_PORT_BIND_ATTEMPTS = 10;
private final INetd mNetd;
+ private final IpSecXfrmController mIpSecXfrmCtrl;
static {
try {
@@ -152,6 +155,11 @@
}
return netd;
}
+
+ /** Get a instance of IpSecXfrmController */
+ public IpSecXfrmController getIpSecXfrmController() {
+ return new IpSecXfrmController();
+ }
}
final UidFdTagger mUidFdTagger;
@@ -1111,6 +1119,7 @@
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 @@
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/service-t/src/com/android/server/IpSecXfrmController.java b/service-t/src/com/android/server/IpSecXfrmController.java
new file mode 100644
index 0000000..c8abd40
--- /dev/null
+++ b/service-t/src/com/android/server/IpSecXfrmController.java
@@ -0,0 +1,198 @@
+/*
+ * 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 com.android.server;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.NETLINK_XFRM;
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.XFRM_MSG_NEWSA;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkErrorMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkGetSaMessage;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+
+/**
+ * This class handles IPSec XFRM commands between IpSecService and the Linux kernel
+ *
+ * <p>Synchronization in IpSecXfrmController is done on all entrypoints due to potential race
+ * conditions at the kernel/xfrm level.
+ */
+public class IpSecXfrmController {
+ private static final String TAG = IpSecXfrmController.class.getSimpleName();
+
+ private static final boolean VDBG = false; // STOPSHIP: if true
+
+ private static final int TIMEOUT_MS = 500;
+ private static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
+
+ @NonNull private final Dependencies mDependencies;
+ @Nullable private FileDescriptor mNetlinkSocket;
+
+ @VisibleForTesting
+ public IpSecXfrmController(@NonNull Dependencies dependencies) {
+ mDependencies = dependencies;
+ }
+
+ public IpSecXfrmController() {
+ this(new Dependencies());
+ }
+
+ /**
+ * Start the XfrmController
+ *
+ * <p>The method is idempotent
+ */
+ public synchronized void openNetlinkSocketIfNeeded() throws ErrnoException, SocketException {
+ if (mNetlinkSocket == null) {
+ mNetlinkSocket = mDependencies.newNetlinkSocket();
+ }
+ }
+
+ /**
+ * Stop the XfrmController
+ *
+ * <p>The method is idempotent
+ */
+ public synchronized void closeNetlinkSocketIfNeeded() {
+ if (mNetlinkSocket != null) {
+ mDependencies.releaseNetlinkSocket(mNetlinkSocket);
+ mNetlinkSocket = null;
+ }
+ }
+
+ @VisibleForTesting
+ public synchronized FileDescriptor getNetlinkSocket() {
+ return mNetlinkSocket;
+ }
+
+ /** Dependencies of IpSecXfrmController, for injection in tests. */
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Get a new XFRM netlink socket and connect it */
+ public FileDescriptor newNetlinkSocket() throws ErrnoException, SocketException {
+ final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_XFRM);
+ NetlinkUtils.connectToKernel(fd);
+ return fd;
+ }
+
+ /** Close the netlink socket */
+ // TODO: b/205923322 This annotation is to suppress the lint error complaining that
+ // #closeQuietly requires Android S. It can be removed when the infra supports setting
+ // service-connectivity min_sdk to 31
+ @TargetApi(Build.VERSION_CODES.S)
+ public void releaseNetlinkSocket(FileDescriptor fd) {
+ IoUtils.closeQuietly(fd);
+ }
+
+ /** Send a netlink message to a socket */
+ public void sendMessage(FileDescriptor fd, byte[] bytes)
+ throws ErrnoException, InterruptedIOException {
+ NetlinkUtils.sendMessage(fd, bytes, 0, bytes.length, TIMEOUT_MS);
+ }
+
+ /** Receive a netlink message from a socket */
+ public ByteBuffer recvMessage(FileDescriptor fd)
+ throws ErrnoException, InterruptedIOException {
+ return NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TIMEOUT_MS);
+ }
+ }
+
+ @GuardedBy("IpSecXfrmController.this")
+ private NetlinkMessage sendRequestAndGetResponse(String methodTag, byte[] req)
+ throws ErrnoException, InterruptedIOException, IOException {
+ openNetlinkSocketIfNeeded();
+
+ logD(methodTag + ": send request " + req.length + " bytes");
+ logV(HexDump.dumpHexString(req));
+ mDependencies.sendMessage(mNetlinkSocket, req);
+
+ final ByteBuffer response = mDependencies.recvMessage(mNetlinkSocket);
+ logD(methodTag + ": receive response " + response.limit() + " bytes");
+ logV(HexDump.dumpHexString(response.array(), 0 /* offset */, response.limit()));
+
+ final NetlinkMessage msg = XfrmNetlinkMessage.parse(response, NETLINK_XFRM);
+ if (msg == null) {
+ throw new IOException("Fail to parse the response message");
+ }
+
+ final int msgType = msg.getHeader().nlmsg_type;
+ if (msgType == NetlinkConstants.NLMSG_ERROR) {
+ final NetlinkErrorMessage errorMsg = (NetlinkErrorMessage) msg;
+ final int errorCode = errorMsg.getNlMsgError().error;
+ throw new ErrnoException(methodTag, errorCode);
+ }
+
+ return msg;
+ }
+
+ /** Get the state of an IPsec SA */
+ @NonNull
+ public synchronized XfrmNetlinkNewSaMessage ipSecGetSa(
+ @NonNull final InetAddress destAddress, long spi)
+ throws ErrnoException, InterruptedIOException, IOException {
+ logD("ipSecGetSa: destAddress=" + destAddress + " spi=" + spi);
+
+ final byte[] req =
+ XfrmNetlinkGetSaMessage.newXfrmNetlinkGetSaMessage(
+ destAddress, spi, (short) IPPROTO_ESP);
+ try {
+ final NetlinkMessage msg = sendRequestAndGetResponse("ipSecGetSa", req);
+
+ final int messageType = msg.getHeader().nlmsg_type;
+ if (messageType != XFRM_MSG_NEWSA) {
+ throw new IOException("unexpected response type " + messageType);
+ }
+
+ return (XfrmNetlinkNewSaMessage) msg;
+ } catch (IllegalArgumentException exception) {
+ // Maybe thrown from Struct.parse
+ throw new IOException("Failed to parse the response " + exception);
+ }
+ }
+
+ private static void logV(String details) {
+ if (VDBG) {
+ Log.v(TAG, details);
+ }
+ }
+
+ private static void logD(String details) {
+ Log.d(TAG, details);
+ }
+}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 2640332..9c01dda 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -111,7 +111,9 @@
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -170,6 +172,8 @@
"mdns_advertiser_allowlist_";
private static final String MDNS_ALLOWLIST_FLAG_SUFFIX = "_version";
+ private static final String TYPE_SUBTYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
@VisibleForTesting
static final String MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF =
"mdns_config_running_app_active_importance_cutoff";
@@ -186,6 +190,7 @@
static final int NO_TRANSACTION = -1;
private static final int NO_SENT_QUERY_COUNT = 0;
private static final int DISCOVERY_QUERY_SENT_CALLBACK = 1000;
+ private static final int MAX_SUBTYPE_COUNT = 100;
private static final SharedLog LOGGER = new SharedLog("serviceDiscovery");
private final Context mContext;
@@ -688,6 +693,35 @@
return mClients.get(args.connector);
}
+ /**
+ * Returns {@code false} if {@code subtypes} exceeds the maximum number limit or
+ * contains invalid subtype label.
+ */
+ private boolean checkSubtypeLabels(Set<String> subtypes) {
+ if (subtypes.size() > MAX_SUBTYPE_COUNT) {
+ mServiceLogs.e(
+ "Too many subtypes: " + subtypes.size() + " (max = "
+ + MAX_SUBTYPE_COUNT + ")");
+ return false;
+ }
+
+ for (String subtype : subtypes) {
+ if (!checkSubtypeLabel(subtype)) {
+ mServiceLogs.e("Subtype " + subtype + " is invalid");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private Set<String> dedupSubtypeLabels(Collection<String> subtypes) {
+ final Map<String, String> subtypeMap = new LinkedHashMap<>(subtypes.size());
+ for (String subtype : subtypes) {
+ subtypeMap.put(MdnsUtils.toDnsLowerCase(subtype), subtype);
+ }
+ return new ArraySet<>(subtypeMap.values());
+ }
+
@Override
public boolean processMessage(Message msg) {
final ClientInfo clientInfo;
@@ -846,13 +880,23 @@
serviceInfo.setServiceName(truncateServiceName(
serviceInfo.getServiceName()));
+ Set<String> subtypes = new ArraySet<>(serviceInfo.getSubtypes());
+ if (!TextUtils.isEmpty(typeSubtype.second)) {
+ subtypes.add(typeSubtype.second);
+ }
+
+ subtypes = dedupSubtypeLabels(subtypes);
+
+ if (!checkSubtypeLabels(subtypes)) {
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+ break;
+ }
+
+ serviceInfo.setSubtypes(subtypes);
+
maybeStartMonitoringSockets();
- // TODO: pass in the subtype as well. Including the subtype in the
- // service type would generate service instance names like
- // Name._subtype._sub._type._tcp, which is incorrect
- // (it should be Name._type._tcp).
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
- typeSubtype.second,
MdnsAdvertisingOptions.newBuilder().build());
storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
serviceInfo.getNetwork());
@@ -1388,6 +1432,7 @@
servInfo,
network == null ? INetd.LOCAL_NET_ID : network.netId,
serviceInfo.getInterfaceIndex());
+ servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
return servInfo;
}
@@ -1591,18 +1636,17 @@
public static Pair<String, String> parseTypeAndSubtype(String serviceType) {
if (TextUtils.isEmpty(serviceType)) return null;
- final String typeOrSubtypePattern = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
final Pattern serviceTypePattern = Pattern.compile(
// Optional leading subtype (_subtype._type._tcp)
// (?: xxx) is a non-capturing parenthesis, don't capture the dot
- "^(?:(" + typeOrSubtypePattern + ")\\.)?"
+ "^(?:(" + TYPE_SUBTYPE_LABEL_REGEX + ")\\.)?"
// Actual type (_type._tcp.local)
- + "(" + typeOrSubtypePattern + "\\._(?:tcp|udp))"
+ + "(" + TYPE_SUBTYPE_LABEL_REGEX + "\\._(?:tcp|udp))"
// Drop '.' at the end of service type that is compatible with old backend.
// e.g. allow "_type._tcp.local."
+ "\\.?"
// Optional subtype after comma, for "_type._tcp,_subtype" format
- + "(?:,(" + typeOrSubtypePattern + "))?"
+ + "(?:,(" + TYPE_SUBTYPE_LABEL_REGEX + "))?"
+ "$");
final Matcher matcher = serviceTypePattern.matcher(serviceType);
if (!matcher.matches()) return null;
@@ -1611,6 +1655,11 @@
return new Pair<>(matcher.group(2), subtype);
}
+ /** Returns {@code true} if {@code subtype} is a valid DNS-SD subtype label. */
+ private static boolean checkSubtypeLabel(String subtype) {
+ return Pattern.compile("^" + TYPE_SUBTYPE_LABEL_REGEX + "$").matcher(subtype).matches();
+ }
+
@VisibleForTesting
NsdService(Context ctx, Handler handler, long cleanupDelayMs) {
this(ctx, handler, cleanupDelayMs, new Dependencies());
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index fc0e11b..135d957 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -44,6 +44,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.UUID;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
@@ -351,8 +352,7 @@
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
try {
- mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(),
- registration.getSubtype());
+ mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@@ -367,7 +367,8 @@
void updateService(int id, @NonNull Registration registration) {
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
- mAdvertisers.valueAt(i).updateService(id, registration.getSubtype());
+ mAdvertisers.valueAt(i).updateService(
+ id, registration.getServiceInfo().getSubtypes());
}
}
@@ -417,7 +418,7 @@
final Registration registration = mPendingRegistrations.valueAt(i);
try {
advertiser.addService(mPendingRegistrations.keyAt(i),
- registration.getServiceInfo(), registration.getSubtype());
+ registration.getServiceInfo());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@@ -485,16 +486,12 @@
private int mConflictCount;
@NonNull
private NsdServiceInfo mServiceInfo;
- @Nullable
- private String mSubtype;
-
int mConflictDuringProbingCount;
int mConflictAfterProbingCount;
- private Registration(@NonNull NsdServiceInfo serviceInfo, @Nullable String subtype) {
+ private Registration(@NonNull NsdServiceInfo serviceInfo) {
this.mOriginalName = serviceInfo.getServiceName();
this.mServiceInfo = serviceInfo;
- this.mSubtype = subtype;
}
/**
@@ -507,10 +504,11 @@
}
/**
- * Update subType for the registration.
+ * Update subTypes for the registration.
*/
- public void updateSubtype(@Nullable String subtype) {
- this.mSubtype = subtype;
+ public void updateSubtypes(@NonNull Set<String> subtypes) {
+ mServiceInfo = new NsdServiceInfo(mServiceInfo);
+ mServiceInfo.setSubtypes(subtypes);
}
/**
@@ -540,17 +538,8 @@
// In case of conflict choose a different service name. After the first conflict use
// "Name (2)", then "Name (3)" etc.
// TODO: use a hidden method in NsdServiceInfo once MdnsAdvertiser is moved to service-t
- final NsdServiceInfo newInfo = new NsdServiceInfo();
+ final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo);
newInfo.setServiceName(getUpdatedServiceName(renameCount));
- newInfo.setServiceType(mServiceInfo.getServiceType());
- for (Map.Entry<String, byte[]> attr : mServiceInfo.getAttributes().entrySet()) {
- newInfo.setAttribute(attr.getKey(),
- attr.getValue() == null ? null : new String(attr.getValue()));
- }
- newInfo.setHost(mServiceInfo.getHost());
- newInfo.setPort(mServiceInfo.getPort());
- newInfo.setNetwork(mServiceInfo.getNetwork());
- // interfaceIndex is not set when registering
return newInfo;
}
@@ -565,11 +554,6 @@
public NsdServiceInfo getServiceInfo() {
return mServiceInfo;
}
-
- @Nullable
- public String getSubtype() {
- return mSubtype;
- }
}
/**
@@ -665,14 +649,14 @@
*
* @param id A unique ID for the service.
* @param service The service info to advertise.
- * @param subtype An optional subtype to advertise the service with.
* @param advertisingOptions The advertising options.
*/
- public void addOrUpdateService(int id, NsdServiceInfo service, @Nullable String subtype,
+ public void addOrUpdateService(int id, NsdServiceInfo service,
MdnsAdvertisingOptions advertisingOptions) {
checkThread();
final Registration existingRegistration = mRegistrations.get(id);
final Network network = service.getNetwork();
+ final Set<String> subtypes = service.getSubtypes();
Registration registration;
if (advertisingOptions.isOnlyUpdate()) {
if (existingRegistration == null) {
@@ -687,10 +671,10 @@
return;
}
- mSharedLog.i("Update service " + service + " with ID " + id + " and subtype " + subtype
- + " advertisingOptions " + advertisingOptions);
+ mSharedLog.i("Update service " + service + " with ID " + id + " and subtypes "
+ + subtypes + " advertisingOptions " + advertisingOptions);
registration = existingRegistration;
- registration.updateSubtype(subtype);
+ registration.updateSubtypes(subtypes);
} else {
if (existingRegistration != null) {
mSharedLog.e("Adding duplicate registration for " + service);
@@ -698,9 +682,9 @@
mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
return;
}
- mSharedLog.i("Adding service " + service + " with ID " + id + " and subtype " + subtype
- + " advertisingOptions " + advertisingOptions);
- registration = new Registration(service, subtype);
+ mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes "
+ + subtypes + " advertisingOptions " + advertisingOptions);
+ registration = new Registration(service);
final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
if (network == null) {
// If registering on all networks, no advertiser must have conflicts
@@ -793,15 +777,10 @@
private OffloadServiceInfoWrapper createOffloadService(int serviceId,
@NonNull Registration registration, byte[] rawOffloadPacket) {
final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
- final List<String> subTypes = new ArrayList<>();
- String subType = registration.getSubtype();
- if (subType != null) {
- subTypes.add(subType);
- }
final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(),
nsdServiceInfo.getServiceType()),
- subTypes,
+ new ArrayList<>(nsdServiceInfo.getSubtypes()),
String.join(".", mDeviceHostName),
rawOffloadPacket,
// TODO: define overlayable resources in
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 463df63..aa40c92 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -37,6 +37,7 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.List;
+import java.util.Set;
/**
* A class that handles advertising services on a {@link MdnsInterfaceSocket} tied to an interface.
@@ -232,12 +233,12 @@
* Update an already registered service without sending exit/re-announcement packet.
*
* @param id An exiting service id
- * @param subtype A new subtype
+ * @param subtypes New subtypes
*/
- public void updateService(int id, @Nullable String subtype) {
+ public void updateService(int id, @NonNull Set<String> subtypes) {
// The current implementation is intended to be used in cases where subtypes don't get
// announced.
- mRecordRepository.updateService(id, subtype);
+ mRecordRepository.updateService(id, subtypes);
}
/**
@@ -245,9 +246,8 @@
*
* @throws NameConflictException There is already a service being advertised with that name.
*/
- public void addService(int id, NsdServiceInfo service, @Nullable String subtype)
- throws NameConflictException {
- final int replacedExitingService = mRecordRepository.addService(id, service, subtype);
+ public void addService(int id, NsdServiceInfo service) throws NameConflictException {
+ final int replacedExitingService = mRecordRepository.addService(id, service);
// Cancel announcements for the existing service. This only happens for exiting services
// (so cancelling exiting announcements), as per RecordRepository.addService.
if (replacedExitingService >= 0) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index 48ece68..1909208 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -74,8 +74,6 @@
// Top-level domain for link-local queries, as per RFC6762 3.
private static final String LOCAL_TLD = "local";
- // Subtype separator as per RFC6763 7.1 (_printer._sub._http._tcp.local)
- private static final String SUBTYPE_SEPARATOR = "_sub";
// Service type for service enumeration (RFC6763 9.)
private static final String[] DNS_SD_SERVICE_TYPE =
@@ -161,8 +159,6 @@
public final RecordInfo<MdnsTextRecord> txtRecord;
@NonNull
public final NsdServiceInfo serviceInfo;
- @Nullable
- public final String subtype;
/**
* Whether the service is sending exit announcements and will be destroyed soon.
@@ -185,28 +181,28 @@
private boolean isProbing;
/**
- * Create a ServiceRegistration with only update the subType
+ * Create a ServiceRegistration with only update the subType.
*/
- ServiceRegistration withSubtype(String newSubType) {
- return new ServiceRegistration(srvRecord.record.getServiceHost(), serviceInfo,
- newSubType, repliedServiceCount, sentPacketCount, exiting, isProbing);
+ ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes) {
+ NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
+ newServiceInfo.setSubtypes(newSubtypes);
+ return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
+ repliedServiceCount, sentPacketCount, exiting, isProbing);
}
-
/**
* Create a ServiceRegistration for dns-sd service registration (RFC6763).
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- @Nullable String subtype, int repliedServiceCount, int sentPacketCount,
- boolean exiting, boolean isProbing) {
+ int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing) {
this.serviceInfo = serviceInfo;
- this.subtype = subtype;
final String[] serviceType = splitServiceType(serviceInfo);
final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
- // Service PTR record
- final RecordInfo<MdnsPointerRecord> ptrRecord = new RecordInfo<>(
+ // Service PTR records
+ ptrRecords = new ArrayList<>(serviceInfo.getSubtypes().size() + 1);
+ ptrRecords.add(new RecordInfo<>(
serviceInfo,
new MdnsPointerRecord(
serviceType,
@@ -214,26 +210,17 @@
false /* cacheFlush */,
NON_NAME_RECORDS_TTL_MILLIS,
serviceName),
- true /* sharedName */);
-
- if (subtype == null) {
- this.ptrRecords = Collections.singletonList(ptrRecord);
- } else {
- final String[] subtypeName = new String[serviceType.length + 2];
- System.arraycopy(serviceType, 0, subtypeName, 2, serviceType.length);
- subtypeName[0] = subtype;
- subtypeName[1] = SUBTYPE_SEPARATOR;
- final RecordInfo<MdnsPointerRecord> subtypeRecord = new RecordInfo<>(
- serviceInfo,
- new MdnsPointerRecord(
- subtypeName,
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
- serviceName),
- true /* sharedName */);
-
- this.ptrRecords = List.of(ptrRecord, subtypeRecord);
+ true /* sharedName */));
+ for (String subtype : serviceInfo.getSubtypes()) {
+ ptrRecords.add(new RecordInfo<>(
+ serviceInfo,
+ new MdnsPointerRecord(
+ MdnsUtils.constructFullSubtype(serviceType, subtype),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ NON_NAME_RECORDS_TTL_MILLIS,
+ serviceName),
+ true /* sharedName */));
}
srvRecord = new RecordInfo<>(
@@ -284,8 +271,8 @@
* @param serviceInfo Service to advertise
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- @Nullable String subtype, int repliedServiceCount, int sentPacketCount) {
- this(deviceHostname, serviceInfo, subtype, repliedServiceCount, sentPacketCount,
+ int repliedServiceCount, int sentPacketCount) {
+ this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
false /* exiting */, true /* isProbing */);
}
@@ -328,17 +315,17 @@
* Update a service that already registered in the repository.
*
* @param serviceId An existing service ID.
- * @param subtype A new subtype
+ * @param subtypes New subtypes
* @return
*/
- public void updateService(int serviceId, @Nullable String subtype) {
+ public void updateService(int serviceId, @NonNull Set<String> subtypes) {
final ServiceRegistration existingRegistration = mServices.get(serviceId);
if (existingRegistration == null) {
throw new IllegalArgumentException(
"Service ID must already exist for an update request: " + serviceId);
}
- final ServiceRegistration updatedRegistration = existingRegistration.withSubtype(
- subtype);
+ final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
+ subtypes);
mServices.put(serviceId, updatedRegistration);
}
@@ -352,8 +339,7 @@
* ID of the replaced service.
* @throws NameConflictException There is already a (non-exiting) service using the name.
*/
- public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable String subtype)
- throws NameConflictException {
+ public int addService(int serviceId, NsdServiceInfo serviceInfo) throws NameConflictException {
if (mServices.contains(serviceId)) {
throw new IllegalArgumentException(
"Service ID must not be reused across registrations: " + serviceId);
@@ -366,7 +352,7 @@
}
final ServiceRegistration registration = new ServiceRegistration(
- mDeviceHostname, serviceInfo, subtype, NO_PACKET /* repliedServiceCount */,
+ mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
NO_PACKET /* sentPacketCount */);
mServices.put(serviceId, registration);
@@ -929,7 +915,7 @@
if (existing == null) return null;
final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
- existing.subtype, existing.repliedServiceCount, existing.sentPacketCount);
+ existing.repliedServiceCount, existing.sentPacketCount);
mServices.put(serviceId, newService);
return makeProbingInfo(
serviceId, newService.srvRecord.record, makeProbingInetAddressRecords());
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 1482ebb..8fc8114 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -233,6 +233,20 @@
&& mdnsRecord.getRemainingTTL(now) <= mdnsRecord.getTtl() / 2;
}
+ /**
+ * Creates a new full subtype name with given service type and subtype labels.
+ *
+ * For example, given ["_http", "_tcp"] and "_printer", this method returns a new String array
+ * of ["_printer", "_sub", "_http", "_tcp"].
+ */
+ public static String[] constructFullSubtype(String[] serviceType, String subtype) {
+ String[] fullSubtype = new String[serviceType.length + 2];
+ fullSubtype[0] = subtype;
+ fullSubtype[1] = MdnsConstants.SUBTYPE_LABEL;
+ System.arraycopy(serviceType, 0, fullSubtype, 2, serviceType.length);
+ return fullSubtype;
+ }
+
/** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
public static class Clock {
/**
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 22120e9..0ec0f13 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -32,6 +32,7 @@
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
@@ -1770,6 +1771,19 @@
mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter,
null /* broadcastPermission */, mHandler);
+ // This is needed for pre-V devices to propagate the data saver status
+ // to the BPF map. This isn't supported before Android T because BPF maps are
+ // unsupported, and it's also unnecessary on Android V and later versions,
+ // as the platform code handles data saver bit updates. Additionally, checking
+ // the initial data saver status here is superfluous because the intent won't
+ // be sent until the system is ready.
+ if (mDeps.isAtLeastT() && !mDeps.isAtLeastV()) {
+ final IntentFilter dataSaverIntentFilter =
+ new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED);
+ mUserAllContext.registerReceiver(mDataSaverReceiver, dataSaverIntentFilter,
+ null /* broadcastPermission */, mHandler);
+ }
+
// TrackMultiNetworkActivities feature should be enabled by trunk stable flag.
// But reading the trunk stable flags from mainline modules is not supported yet.
// So enabling this feature on V+ release.
@@ -7051,6 +7065,32 @@
}
};
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private final BroadcastReceiver mDataSaverReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mDeps.isAtLeastV()) {
+ throw new IllegalStateException(
+ "data saver status should be updated from platform");
+ }
+ ensureRunningOnConnectivityServiceThread();
+ switch (intent.getAction()) {
+ case ACTION_RESTRICT_BACKGROUND_CHANGED:
+ // If the uid is present in the deny list, the API will consistently
+ // return ENABLED. To retrieve the global switch status, the system
+ // uid is chosen because it will never be included in the deny list.
+ final int dataSaverForSystemUid =
+ mPolicyManager.getRestrictBackgroundStatus(Process.SYSTEM_UID);
+ final boolean isDataSaverEnabled = (dataSaverForSystemUid
+ != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED);
+ mBpfNetMaps.setDataSaverEnabled(isDataSaverEnabled);
+ break;
+ default:
+ Log.wtf(TAG, "received unexpected intent: " + intent.getAction());
+ }
+ }
+ };
+
private final HashMap<Messenger, NetworkProviderInfo> mNetworkProviderInfos = new HashMap<>();
private final HashMap<NetworkRequest, NetworkRequestInfo> mNetworkRequests = new HashMap<>();
@@ -12988,7 +13028,7 @@
}
}
- @TargetApi(Build.VERSION_CODES.TIRAMISU)
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Override
public void setDataSaverEnabled(final boolean enable) {
enforceNetworkStackOrSettingsPermission();
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
index 44c51d8..ad7a4d7 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -151,6 +151,9 @@
public static final int RTNLGRP_ND_USEROPT = 20;
public static final int RTMGRP_ND_USEROPT = 1 << (RTNLGRP_ND_USEROPT - 1);
+ // Netlink family
+ public static final short RTNL_FAMILY_IP6MR = 129;
+
// Device flags.
public static final int IFF_UP = 1 << 0;
public static final int IFF_LOWER_UP = 1 << 16;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
index 111e0ba..781a04e 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -142,6 +142,7 @@
return (NetlinkMessage) RtNetlinkAddressMessage.parse(nlmsghdr, byteBuffer);
case NetlinkConstants.RTM_NEWROUTE:
case NetlinkConstants.RTM_DELROUTE:
+ case NetlinkConstants.RTM_GETROUTE:
return (NetlinkMessage) RtNetlinkRouteMessage.parse(nlmsghdr, byteBuffer);
case NetlinkConstants.RTM_NEWNEIGH:
case NetlinkConstants.RTM_DELNEIGH:
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index f1f30d3..f6282fd 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -29,7 +29,12 @@
import static android.system.OsConstants.SO_RCVBUF;
import static android.system.OsConstants.SO_RCVTIMEO;
import static android.system.OsConstants.SO_SNDTIMEO;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+import android.net.ParseException;
import android.net.util.SocketUtils;
import android.system.ErrnoException;
import android.system.Os;
@@ -47,7 +52,11 @@
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
/**
* Utilities for netlink related class that may not be able to fit into a specific class.
@@ -163,11 +172,7 @@
Log.e(TAG, errPrefix, e);
throw new ErrnoException(errPrefix, EIO, e);
} finally {
- try {
- SocketUtils.closeSocket(fd);
- } catch (IOException e) {
- // Nothing we can do here
- }
+ closeSocketQuietly(fd);
}
}
@@ -308,4 +313,85 @@
}
private NetlinkUtils() {}
+
+ /**
+ * Sends a netlink dump request and processes the returned dump messages
+ *
+ * @param <T> extends NetlinkMessage
+ * @param dumpRequestMessage netlink dump request message to be sent
+ * @param nlFamily netlink family
+ * @param msgClass expected class of the netlink message
+ * @param func function defined by caller to handle the dump messages
+ * @throws SocketException when fails to create socket
+ * @throws InterruptedIOException when fails to read the dumpFd
+ * @throws ErrnoException when fails to send dump request
+ * @throws ParseException when message can't be parsed
+ */
+ public static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessages(
+ byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
+ Consumer<T> func)
+ throws SocketException, InterruptedIOException, ErrnoException, ParseException {
+ // Create socket and send dump request
+ final FileDescriptor fd;
+ try {
+ fd = netlinkSocketForProto(nlFamily);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to create netlink socket " + e);
+ throw e.rethrowAsSocketException();
+ }
+
+ try {
+ connectToKernel(fd);
+ } catch (ErrnoException | SocketException e) {
+ Log.e(TAG, "Failed to connect netlink socket to kernel " + e);
+ closeSocketQuietly(fd);
+ return;
+ }
+
+ try {
+ sendMessage(fd, dumpRequestMessage, 0, dumpRequestMessage.length, IO_TIMEOUT_MS);
+ } catch (InterruptedIOException | ErrnoException e) {
+ Log.e(TAG, "Failed to send dump request " + e);
+ closeSocketQuietly(fd);
+ throw e;
+ }
+
+ while (true) {
+ final ByteBuffer buf = recvMessage(
+ fd, NetlinkUtils.DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
+
+ while (buf.remaining() > 0) {
+ final int position = buf.position();
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(buf, nlFamily);
+ if (nlMsg == null) {
+ // Move to the position where parse started for error log.
+ buf.position(position);
+ Log.e(TAG, "Failed to parse netlink message: " + hexify(buf));
+ closeSocketQuietly(fd);
+ throw new ParseException("Failed to parse netlink message");
+ }
+
+ if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
+ closeSocketQuietly(fd);
+ return;
+ }
+
+ if (!msgClass.isInstance(nlMsg)) {
+ Log.e(TAG, "Received unexpected netlink message: " + nlMsg);
+ continue;
+ }
+
+ final T msg = (T) nlMsg;
+ func.accept(msg);
+ }
+ }
+ }
+
+ private static void closeSocketQuietly(final FileDescriptor fd) {
+ try {
+ SocketUtils.closeSocket(fd);
+ } catch (IOException e) {
+ // Nothing we can do here
+ }
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
index 9acac69..b2b1e93 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
@@ -19,11 +19,15 @@
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.NETLINK_ROUTE;
import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
import android.annotation.SuppressLint;
import android.net.IpPrefix;
+import android.net.RouteInfo;
import android.system.OsConstants;
import androidx.annotation.NonNull;
@@ -34,6 +38,9 @@
import java.net.Inet6Address;
import java.net.InetAddress;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.IntBuffer;
+import java.util.Arrays;
/**
* A NetlinkMessage subclass for rtnetlink route messages.
@@ -49,31 +56,69 @@
*/
public class RtNetlinkRouteMessage extends NetlinkMessage {
public static final short RTA_DST = 1;
+ public static final short RTA_SRC = 2;
+ public static final short RTA_IIF = 3;
public static final short RTA_OIF = 4;
public static final short RTA_GATEWAY = 5;
public static final short RTA_CACHEINFO = 12;
+ public static final short RTA_EXPIRES = 23;
- private int mIfindex;
+ public static final short RTNH_F_UNRESOLVED = 32; // The multicast route is unresolved
+
+ public static final String TAG = "NetlinkRouteMessage";
+
+ // For multicast routes, whether the route is resolved or unresolved
+ private boolean mIsResolved;
+ // The interface index for incoming interface, this is set for multicast
+ // routes, see common/net/ipv4/ipmr_base.c mr_fill_mroute
+ private int mIifIndex; // Incoming interface of a route, for resolved multicast routes
+ private int mOifIndex;
@NonNull
private StructRtMsg mRtmsg;
- @NonNull
- private IpPrefix mDestination;
+ @Nullable
+ private IpPrefix mSource; // Source address of a route, for all multicast routes
+ @Nullable
+ private IpPrefix mDestination; // Destination of a route, can be null for RTM_GETROUTE
@Nullable
private InetAddress mGateway;
@Nullable
private StructRtaCacheInfo mRtaCacheInfo;
+ private long mSinceLastUseMillis; // Milliseconds since the route was used,
+ // for resolved multicast routes
- private RtNetlinkRouteMessage(StructNlMsgHdr header) {
+ public RtNetlinkRouteMessage(StructNlMsgHdr header, StructRtMsg rtMsg) {
super(header);
- mRtmsg = null;
+ mRtmsg = rtMsg;
+ mSource = null;
mDestination = null;
mGateway = null;
- mIfindex = 0;
+ mIifIndex = 0;
+ mOifIndex = 0;
mRtaCacheInfo = null;
+ mSinceLastUseMillis = -1;
+ }
+
+ /**
+ * Returns the rtnetlink family.
+ */
+ public short getRtmFamily() {
+ return mRtmsg.family;
+ }
+
+ /**
+ * Returns if the route is resolved. This is always true for unicast,
+ * and may be false only for multicast routes.
+ */
+ public boolean isResolved() {
+ return mIsResolved;
+ }
+
+ public int getIifIndex() {
+ return mIifIndex;
}
public int getInterfaceIndex() {
- return mIfindex;
+ return mOifIndex;
}
@NonNull
@@ -86,6 +131,14 @@
return mDestination;
}
+ /**
+ * Get source address of a route. This is for multicast routes.
+ */
+ @NonNull
+ public IpPrefix getSource() {
+ return mSource;
+ }
+
@Nullable
public InetAddress getGateway() {
return mGateway;
@@ -97,6 +150,18 @@
}
/**
+ * RTA_EXPIRES attribute returned by kernel to indicate the clock ticks
+ * from the route was last used to now, converted to milliseconds.
+ * This is set for multicast routes.
+ *
+ * Note that this value is not updated with the passage of time. It always
+ * returns the value that was read when the netlink message was parsed.
+ */
+ public long getSinceLastUseMillis() {
+ return mSinceLastUseMillis;
+ }
+
+ /**
* Check whether the address families of destination and gateway match rtm_family in
* StructRtmsg.
*
@@ -107,7 +172,8 @@
private static boolean matchRouteAddressFamily(@NonNull final InetAddress address,
int family) {
return ((address instanceof Inet4Address) && (family == AF_INET))
- || ((address instanceof Inet6Address) && (family == AF_INET6));
+ || ((address instanceof Inet6Address) &&
+ (family == AF_INET6 || family == RTNL_FAMILY_IP6MR));
}
/**
@@ -121,11 +187,11 @@
@Nullable
public static RtNetlinkRouteMessage parse(@NonNull final StructNlMsgHdr header,
@NonNull final ByteBuffer byteBuffer) {
- final RtNetlinkRouteMessage routeMsg = new RtNetlinkRouteMessage(header);
-
- routeMsg.mRtmsg = StructRtMsg.parse(byteBuffer);
- if (routeMsg.mRtmsg == null) return null;
+ final StructRtMsg rtmsg = StructRtMsg.parse(byteBuffer);
+ if (rtmsg == null) return null;
+ final RtNetlinkRouteMessage routeMsg = new RtNetlinkRouteMessage(header, rtmsg);
int rtmFamily = routeMsg.mRtmsg.family;
+ routeMsg.mIsResolved = ((routeMsg.mRtmsg.flags & RTNH_F_UNRESOLVED) == 0);
// RTA_DST
final int baseOffset = byteBuffer.position();
@@ -139,12 +205,24 @@
routeMsg.mDestination = new IpPrefix(destination, routeMsg.mRtmsg.dstLen);
} else if (rtmFamily == AF_INET) {
routeMsg.mDestination = new IpPrefix(IPV4_ADDR_ANY, 0);
- } else if (rtmFamily == AF_INET6) {
+ } else if (rtmFamily == AF_INET6 || rtmFamily == RTNL_FAMILY_IP6MR) {
routeMsg.mDestination = new IpPrefix(IPV6_ADDR_ANY, 0);
} else {
return null;
}
+ // RTA_SRC
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(RTA_SRC, byteBuffer);
+ if (nlAttr != null) {
+ final InetAddress source = nlAttr.getValueAsInetAddress();
+ // If the RTA_SRC attribute is malformed, return null.
+ if (source == null) return null;
+ // If the address family of destination doesn't match rtm_family, return null.
+ if (!matchRouteAddressFamily(source, rtmFamily)) return null;
+ routeMsg.mSource = new IpPrefix(source, routeMsg.mRtmsg.srcLen);
+ }
+
// RTA_GATEWAY
byteBuffer.position(baseOffset);
nlAttr = StructNlAttr.findNextAttrOfType(RTA_GATEWAY, byteBuffer);
@@ -156,6 +234,17 @@
if (!matchRouteAddressFamily(routeMsg.mGateway, rtmFamily)) return null;
}
+ // RTA_IIF
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(RTA_IIF, byteBuffer);
+ if (nlAttr != null) {
+ Integer iifInteger = nlAttr.getValueAsInteger();
+ if (iifInteger == null) {
+ return null;
+ }
+ routeMsg.mIifIndex = iifInteger;
+ }
+
// RTA_OIF
byteBuffer.position(baseOffset);
nlAttr = StructNlAttr.findNextAttrOfType(RTA_OIF, byteBuffer);
@@ -164,7 +253,7 @@
// the interface index to a name themselves. This may not succeed or may be
// incorrect, because the interface might have been deleted, or even deleted
// and re-added with a different index, since the netlink message was sent.
- routeMsg.mIfindex = nlAttr.getValueAsInt(0 /* 0 isn't a valid ifindex */);
+ routeMsg.mOifIndex = nlAttr.getValueAsInt(0 /* 0 isn't a valid ifindex */);
}
// RTA_CACHEINFO
@@ -174,33 +263,59 @@
routeMsg.mRtaCacheInfo = StructRtaCacheInfo.parse(nlAttr.getValueAsByteBuffer());
}
+ // RTA_EXPIRES
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(RTA_EXPIRES, byteBuffer);
+ if (nlAttr != null) {
+ final Long sinceLastUseCentis = nlAttr.getValueAsLong();
+ // If the RTA_EXPIRES attribute is malformed, return null.
+ if (sinceLastUseCentis == null) return null;
+ // RTA_EXPIRES returns time in clock ticks of USER_HZ(100), which is centiseconds
+ routeMsg.mSinceLastUseMillis = sinceLastUseCentis * 10;
+ }
+
return routeMsg;
}
/**
* Write a rtnetlink address message to {@link ByteBuffer}.
*/
- @VisibleForTesting
- protected void pack(ByteBuffer byteBuffer) {
+ public void pack(ByteBuffer byteBuffer) {
getHeader().pack(byteBuffer);
mRtmsg.pack(byteBuffer);
- final StructNlAttr destination = new StructNlAttr(RTA_DST, mDestination.getAddress());
- destination.pack(byteBuffer);
+ if (mSource != null) {
+ final StructNlAttr source = new StructNlAttr(RTA_SRC, mSource.getAddress());
+ source.pack(byteBuffer);
+ }
+
+ if (mDestination != null) {
+ final StructNlAttr destination = new StructNlAttr(RTA_DST, mDestination.getAddress());
+ destination.pack(byteBuffer);
+ }
if (mGateway != null) {
final StructNlAttr gateway = new StructNlAttr(RTA_GATEWAY, mGateway.getAddress());
gateway.pack(byteBuffer);
}
- if (mIfindex != 0) {
- final StructNlAttr ifindex = new StructNlAttr(RTA_OIF, mIfindex);
- ifindex.pack(byteBuffer);
+ if (mIifIndex != 0) {
+ final StructNlAttr iifindex = new StructNlAttr(RTA_IIF, mIifIndex);
+ iifindex.pack(byteBuffer);
+ }
+ if (mOifIndex != 0) {
+ final StructNlAttr oifindex = new StructNlAttr(RTA_OIF, mOifIndex);
+ oifindex.pack(byteBuffer);
}
if (mRtaCacheInfo != null) {
final StructNlAttr cacheInfo = new StructNlAttr(RTA_CACHEINFO,
mRtaCacheInfo.writeToBytes());
cacheInfo.pack(byteBuffer);
}
+ if (mSinceLastUseMillis >= 0) {
+ final long sinceLastUseCentis = mSinceLastUseMillis / 10;
+ final StructNlAttr expires = new StructNlAttr(RTA_EXPIRES, sinceLastUseCentis);
+ expires.pack(byteBuffer);
+ }
}
@Override
@@ -208,10 +323,14 @@
return "RtNetlinkRouteMessage{ "
+ "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+ "Rtmsg{" + mRtmsg.toString() + "}, "
- + "destination{" + mDestination.getAddress().getHostAddress() + "}, "
+ + (mSource == null ? "" : "source{" + mSource.getAddress().getHostAddress() + "}, ")
+ + (mDestination == null ?
+ "" : "destination{" + mDestination.getAddress().getHostAddress() + "}, ")
+ "gateway{" + (mGateway == null ? "" : mGateway.getHostAddress()) + "}, "
- + "ifindex{" + mIfindex + "}, "
+ + (mIifIndex == 0 ? "" : "iifindex{" + mIifIndex + "}, ")
+ + "oifindex{" + mOifIndex + "}, "
+ "rta_cacheinfo{" + (mRtaCacheInfo == null ? "" : mRtaCacheInfo.toString()) + "} "
+ + (mSinceLastUseMillis < 0 ? "" : "sinceLastUseMillis{" + mSinceLastUseMillis + "}")
+ "}";
}
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
index a9b6495..43e8312 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
@@ -21,6 +21,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import static java.nio.ByteOrder.nativeOrder;
+
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -152,12 +154,12 @@
nla_type = type;
setValue(new byte[Short.BYTES]);
final ByteBuffer buf = getValueAsByteBuffer();
- final ByteOrder originalOrder = buf.order();
+ // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
try {
buf.order(order);
buf.putShort(value);
} finally {
- buf.order(originalOrder);
+ buf.order(nativeOrder());
}
}
@@ -169,12 +171,29 @@
nla_type = type;
setValue(new byte[Integer.BYTES]);
final ByteBuffer buf = getValueAsByteBuffer();
- final ByteOrder originalOrder = buf.order();
+ // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
try {
buf.order(order);
buf.putInt(value);
} finally {
- buf.order(originalOrder);
+ buf.order(nativeOrder());
+ }
+ }
+
+ public StructNlAttr(short type, long value) {
+ this(type, value, ByteOrder.nativeOrder());
+ }
+
+ public StructNlAttr(short type, long value, ByteOrder order) {
+ nla_type = type;
+ setValue(new byte[Long.BYTES]);
+ final ByteBuffer buf = getValueAsByteBuffer();
+ // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
+ try {
+ buf.order(order);
+ buf.putLong(value);
+ } finally {
+ buf.order(nativeOrder());
}
}
@@ -288,6 +307,7 @@
/**
* Get attribute value as Integer, or null if malformed (e.g., length is not 4 bytes).
+ * The attribute value is assumed to be in native byte order.
*/
public Integer getValueAsInteger() {
final ByteBuffer byteBuffer = getValueAsByteBuffer();
@@ -298,6 +318,18 @@
}
/**
+ * Get attribute value as Long, or null if malformed (e.g., length is not 8 bytes).
+ * The attribute value is assumed to be in native byte order.
+ */
+ public Long getValueAsLong() {
+ final ByteBuffer byteBuffer = getValueAsByteBuffer();
+ if (byteBuffer == null || byteBuffer.remaining() != Long.BYTES) {
+ return null;
+ }
+ return byteBuffer.getLong();
+ }
+
+ /**
* Get attribute value as Int, default value if malformed.
*/
public int getValueAsInt(int defaultValue) {
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
index ff37639..5272366 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
@@ -71,13 +71,17 @@
}
sb.append("NLM_F_ECHO");
}
- if ((flags & NLM_F_ROOT) != 0) {
+ if ((flags & NLM_F_DUMP) == NLM_F_DUMP) {
+ if (sb.length() > 0) {
+ sb.append("|");
+ }
+ sb.append("NLM_F_DUMP");
+ } else if ((flags & NLM_F_ROOT) != 0) { // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH
if (sb.length() > 0) {
sb.append("|");
}
sb.append("NLM_F_ROOT");
- }
- if ((flags & NLM_F_MATCH) != 0) {
+ } else if ((flags & NLM_F_MATCH) != 0) {
if (sb.length() > 0) {
sb.append("|");
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java
index 01dc66e..a7bdcd9 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmReplayStateEsn.java
@@ -131,12 +131,12 @@
/** Return the TX sequence number in unisgned long */
public long getTxSequenceNumber() {
- return getSequenceNumber(mWithoutBitmap.seqHi, mWithoutBitmap.seq);
+ return getSequenceNumber(mWithoutBitmap.oSeqHi, mWithoutBitmap.oSeq);
}
/** Return the RX sequence number in unisgned long */
public long getRxSequenceNumber() {
- return getSequenceNumber(mWithoutBitmap.oSeqHi, mWithoutBitmap.oSeq);
+ return getSequenceNumber(mWithoutBitmap.seqHi, mWithoutBitmap.seq);
}
@VisibleForTesting
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 031e52f..0dfca57 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -33,7 +33,10 @@
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/NetworkStack/tests/integration",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ test: true
+ },
}
android_test {
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
index 5a231fc..17d4e81 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
@@ -55,6 +55,9 @@
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -65,19 +68,14 @@
@Test
public void testGetNeighborsQuery() throws Exception {
- final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
- assertNotNull(fd);
-
- NetlinkUtils.connectToKernel(fd);
-
- final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
- assertNotNull(localAddr);
- assertEquals(0, localAddr.getGroupsMask());
- assertTrue(0 != localAddr.getPortId());
-
final byte[] req = RtNetlinkNeighborMessage.newGetNeighborsRequest(TEST_SEQNO);
assertNotNull(req);
+ List<RtNetlinkNeighborMessage> msgs = new ArrayList<>();
+ Consumer<RtNetlinkNeighborMessage> handleNlDumpMsg = (msg) -> {
+ msgs.add(msg);
+ };
+
final Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
final int targetSdk =
ctx.getPackageManager()
@@ -94,7 +92,8 @@
assumeFalse("network_stack context is expected to have permission to send RTM_GETNEIGH",
ctxt.startsWith("u:r:network_stack:s0"));
try {
- NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS);
+ NetlinkUtils.<RtNetlinkNeighborMessage>getAndProcessNetlinkDumpMessages(req,
+ NETLINK_ROUTE, RtNetlinkNeighborMessage.class, handleNlDumpMsg);
fail("RTM_GETNEIGH is not allowed for apps targeting SDK > 31 on T+ platforms,"
+ " target SDK version: " + targetSdk);
} catch (ErrnoException e) {
@@ -105,106 +104,70 @@
}
// Check that apps targeting lower API levels / running on older platforms succeed
- assertEquals(req.length,
- NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS));
+ NetlinkUtils.<RtNetlinkNeighborMessage>getAndProcessNetlinkDumpMessages(req,
+ NETLINK_ROUTE, RtNetlinkNeighborMessage.class, handleNlDumpMsg);
- int neighMessageCount = 0;
- int doneMessageCount = 0;
-
- while (doneMessageCount == 0) {
- ByteBuffer response =
- NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TEST_TIMEOUT_MS);
- assertNotNull(response);
- assertTrue(StructNlMsgHdr.STRUCT_SIZE <= response.limit());
- assertEquals(0, response.position());
- assertEquals(ByteOrder.nativeOrder(), response.order());
-
- // Verify the messages at least appears minimally reasonable.
- while (response.remaining() > 0) {
- final NetlinkMessage msg = NetlinkMessage.parse(response, NETLINK_ROUTE);
- assertNotNull(msg);
- final StructNlMsgHdr hdr = msg.getHeader();
- assertNotNull(hdr);
-
- if (hdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
- doneMessageCount++;
- continue;
- }
-
- assertEquals(NetlinkConstants.RTM_NEWNEIGH, hdr.nlmsg_type);
- assertTrue(msg instanceof RtNetlinkNeighborMessage);
- assertTrue((hdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
- assertEquals(TEST_SEQNO, hdr.nlmsg_seq);
- assertEquals(localAddr.getPortId(), hdr.nlmsg_pid);
-
- neighMessageCount++;
- }
+ for (var msg : msgs) {
+ assertNotNull(msg);
+ final StructNlMsgHdr hdr = msg.getHeader();
+ assertNotNull(hdr);
+ assertEquals(NetlinkConstants.RTM_NEWNEIGH, hdr.nlmsg_type);
+ assertTrue((hdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
+ assertEquals(TEST_SEQNO, hdr.nlmsg_seq);
}
- assertEquals(1, doneMessageCount);
// TODO: make sure this test passes sanely in airplane mode.
- assertTrue(neighMessageCount > 0);
-
- IoUtils.closeQuietly(fd);
+ assertTrue(msgs.size() > 0);
}
@Test
public void testBasicWorkingGetAddrQuery() throws Exception {
- final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
- assertNotNull(fd);
-
- NetlinkUtils.connectToKernel(fd);
-
- final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
- assertNotNull(localAddr);
- assertEquals(0, localAddr.getGroupsMask());
- assertTrue(0 != localAddr.getPortId());
-
final int testSeqno = 8;
final byte[] req = newGetAddrRequest(testSeqno);
assertNotNull(req);
- final long timeout = 500;
- assertEquals(req.length, NetlinkUtils.sendMessage(fd, req, 0, req.length, timeout));
+ List<RtNetlinkAddressMessage> msgs = new ArrayList<>();
+ Consumer<RtNetlinkAddressMessage> handleNlDumpMsg = (msg) -> {
+ msgs.add(msg);
+ };
+ NetlinkUtils.<RtNetlinkAddressMessage>getAndProcessNetlinkDumpMessages(req, NETLINK_ROUTE,
+ RtNetlinkAddressMessage.class, handleNlDumpMsg);
- int addrMessageCount = 0;
+ boolean ipv4LoopbackAddressFound = false;
+ boolean ipv6LoopbackAddressFound = false;
+ final InetAddress loopbackIpv4 = InetAddress.getByName("127.0.0.1");
+ final InetAddress loopbackIpv6 = InetAddress.getByName("::1");
- while (true) {
- ByteBuffer response = NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, timeout);
- assertNotNull(response);
- assertTrue(StructNlMsgHdr.STRUCT_SIZE <= response.limit());
- assertEquals(0, response.position());
- assertEquals(ByteOrder.nativeOrder(), response.order());
-
- final NetlinkMessage msg = NetlinkMessage.parse(response, NETLINK_ROUTE);
+ for (var msg : msgs) {
assertNotNull(msg);
final StructNlMsgHdr nlmsghdr = msg.getHeader();
assertNotNull(nlmsghdr);
-
- if (nlmsghdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
- break;
- }
-
assertEquals(NetlinkConstants.RTM_NEWADDR, nlmsghdr.nlmsg_type);
assertTrue((nlmsghdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
assertEquals(testSeqno, nlmsghdr.nlmsg_seq);
- assertEquals(localAddr.getPortId(), nlmsghdr.nlmsg_pid);
assertTrue(msg instanceof RtNetlinkAddressMessage);
- addrMessageCount++;
-
- // From the query response we can see the RTM_NEWADDR messages representing for IPv4
- // and IPv6 loopback address: 127.0.0.1 and ::1.
+ // When parsing the full response we can see the RTM_NEWADDR messages representing for
+ // IPv4 and IPv6 loopback address: 127.0.0.1 and ::1 and non-loopback addresses.
final StructIfaddrMsg ifaMsg = ((RtNetlinkAddressMessage) msg).getIfaddrHeader();
final InetAddress ipAddress = ((RtNetlinkAddressMessage) msg).getIpAddress();
assertTrue(
"Non-IP address family: " + ifaMsg.family,
ifaMsg.family == AF_INET || ifaMsg.family == AF_INET6);
- assertTrue(ipAddress.isLoopbackAddress());
+ assertNotNull(ipAddress);
+
+ if (ipAddress.equals(loopbackIpv4)) {
+ ipv4LoopbackAddressFound = true;
+ assertTrue(ipAddress.isLoopbackAddress());
+ }
+ if (ipAddress.equals(loopbackIpv6)) {
+ ipv6LoopbackAddressFound = true;
+ assertTrue(ipAddress.isLoopbackAddress());
+ }
}
- assertTrue(addrMessageCount > 0);
-
- IoUtils.closeQuietly(fd);
+ assertTrue(msgs.size() > 0);
+ // Check ipv4 and ipv6 loopback addresses are in the output
+ assertTrue(ipv4LoopbackAddressFound && ipv6LoopbackAddressFound);
}
/** A convenience method to create an RTM_GETADDR request message. */
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
index 9881653..50b8278 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
@@ -16,7 +16,9 @@
package com.android.net.module.util.netlink;
+import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.NETLINK_ROUTE;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -38,6 +40,7 @@
import java.net.Inet6Address;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -127,6 +130,72 @@
assertEquals(RTM_NEWROUTE_PACK_HEX, HexDump.toHexString(packBuffer.array()));
}
+ private static final String RTM_GETROUTE_MULTICAST_IPV6_HEX =
+ "1C0000001A0001030000000000000000" // struct nlmsghr
+ + "810000000000000000000000"; // struct rtmsg
+
+ private static final String RTM_NEWROUTE_MULTICAST_IPV6_HEX =
+ "88000000180002000000000000000000" // struct nlmsghr
+ + "81808000FE11000500000000" // struct rtmsg
+ + "08000F00FE000000" // RTA_TABLE
+ + "14000200FDACC0F1DBDB000195B7C1A464F944EA" // RTA_SRC
+ + "14000100FF040000000000000000000000001234" // RTA_DST
+ + "0800030014000000" // RTA_IIF
+ + "0C0009000800000111000000" // RTA_MULTIPATH
+ + "1C00110001000000000000009400000000000000" // RTA_STATS
+ + "0000000000000000"
+ + "0C0017007617000000000000"; // RTA_EXPIRES
+
+ @Test
+ public void testParseRtmNewRoute_MulticastIpv6() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkRouteMessage);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+ final StructNlMsgHdr hdr = routeMsg.getHeader();
+ assertNotNull(hdr);
+ assertEquals(136, hdr.nlmsg_len);
+ assertEquals(NetlinkConstants.RTM_NEWROUTE, hdr.nlmsg_type);
+
+ final StructRtMsg rtmsg = routeMsg.getRtMsgHeader();
+ assertNotNull(rtmsg);
+ assertEquals((byte) 129, (byte) rtmsg.family);
+ assertEquals(128, rtmsg.dstLen);
+ assertEquals(128, rtmsg.srcLen);
+ assertEquals(0xFE, rtmsg.table);
+
+ assertEquals(routeMsg.getSource(),
+ new IpPrefix("fdac:c0f1:dbdb:1:95b7:c1a4:64f9:44ea/128"));
+ assertEquals(routeMsg.getDestination(), new IpPrefix("ff04::1234/128"));
+ assertEquals(20, routeMsg.getIifIndex());
+ assertEquals(60060, routeMsg.getSinceLastUseMillis());
+ }
+
+ // NEWROUTE message for multicast IPv6 with the packed attributes
+ private static final String RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX =
+ "58000000180002000000000000000000" // struct nlmsghr
+ + "81808000FE11000500000000" // struct rtmsg
+ + "14000200FDACC0F1DBDB000195B7C1A464F944EA" // RTA_SRC
+ + "14000100FF040000000000000000000000001234" // RTA_DST
+ + "0800030014000000" // RTA_IIF
+ + "0C0017007617000000000000"; // RTA_EXPIRES
+ @Test
+ public void testPackRtmNewRoute_MulticastIpv6() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+ final ByteBuffer packBuffer = ByteBuffer.allocate(88);
+ packBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ routeMsg.pack(packBuffer);
+ assertEquals(RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX,
+ HexDump.toHexString(packBuffer.array()));
+ }
+
private static final String RTM_NEWROUTE_TRUNCATED_HEX =
"48000000180000060000000000000000" // struct nlmsghr
+ "0A400000FC02000100000000" // struct rtmsg
@@ -220,10 +289,79 @@
+ "scope: 0, type: 1, flags: 0}, "
+ "destination{2001:db8:1::}, "
+ "gateway{fe80::1}, "
- + "ifindex{735}, "
+ + "oifindex{735}, "
+ "rta_cacheinfo{clntref: 0, lastuse: 0, expires: 59998, error: 0, used: 0, "
+ "id: 0, ts: 0, tsage: 0} "
+ "}";
assertEquals(expected, routeMsg.toString());
}
+
+ @Test
+ public void testToString_RtmGetRoute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_GETROUTE_MULTICAST_IPV6_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkRouteMessage);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+ final String expected = "RtNetlinkRouteMessage{ "
+ + "nlmsghdr{"
+ + "StructNlMsgHdr{ nlmsg_len{28}, nlmsg_type{26(RTM_GETROUTE)}, "
+ + "nlmsg_flags{769(NLM_F_REQUEST|NLM_F_DUMP)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+ + "Rtmsg{"
+ + "family: 129, dstLen: 0, srcLen: 0, tos: 0, table: 0, protocol: 0, "
+ + "scope: 0, type: 0, flags: 0}, "
+ + "destination{::}, "
+ + "gateway{}, "
+ + "oifindex{0}, "
+ + "rta_cacheinfo{} "
+ + "}";
+ assertEquals(expected, routeMsg.toString());
+ }
+
+ @Test
+ public void testToString_RtmNewRouteMulticastIpv6() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ assertNotNull(msg);
+ assertTrue(msg instanceof RtNetlinkRouteMessage);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+ final String expected = "RtNetlinkRouteMessage{ "
+ + "nlmsghdr{"
+ + "StructNlMsgHdr{ nlmsg_len{136}, nlmsg_type{24(RTM_NEWROUTE)}, "
+ + "nlmsg_flags{2(NLM_F_MULTI)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+ + "Rtmsg{"
+ + "family: 129, dstLen: 128, srcLen: 128, tos: 0, table: 254, protocol: 17, "
+ + "scope: 0, type: 5, flags: 0}, "
+ + "source{fdac:c0f1:dbdb:1:95b7:c1a4:64f9:44ea}, "
+ + "destination{ff04::1234}, "
+ + "gateway{}, "
+ + "iifindex{20}, "
+ + "oifindex{0}, "
+ + "rta_cacheinfo{} "
+ + "sinceLastUseMillis{60060}"
+ + "}";
+ assertEquals(expected, routeMsg.toString());
+ }
+
+ @Test
+ public void testGetRtmFamily_RTNL_FAMILY_IP6MR() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+ assertEquals(RTNL_FAMILY_IP6MR, routeMsg.getRtmFamily());
+ }
+
+ @Test
+ public void testGetRtmFamily_AF_INET6() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+ assertEquals(AF_INET6, routeMsg.getRtmFamily());
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
index af3fac2..4c3fde6 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
@@ -92,4 +92,26 @@
assertNull(integer3);
assertEquals(int3, 0x08 /* default value */);
}
+
+ @Test
+ public void testGetValueAsLong() {
+ final Long input = 1234567L;
+ // Not a real netlink attribute, just for testing
+ final StructNlAttr attr = new StructNlAttr(IFA_FLAGS, input);
+
+ final Long output = attr.getValueAsLong();
+
+ assertEquals(input, output);
+ }
+
+ @Test
+ public void testGetValueAsLong_malformed() {
+ final int input = 1234567;
+ // Not a real netlink attribute, just for testing
+ final StructNlAttr attr = new StructNlAttr(IFA_FLAGS, input);
+
+ final Long output = attr.getValueAsLong();
+
+ assertNull(output);
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
index b7f68c6..a0d8b8c 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
@@ -16,6 +16,7 @@
package com.android.net.module.util.netlink;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
import static org.junit.Assert.fail;
import android.system.OsConstants;
@@ -48,10 +49,14 @@
public static final String TEST_NLMSG_PID_STR = "nlmsg_pid{5678}";
private StructNlMsgHdr makeStructNlMsgHdr(short type) {
+ return makeStructNlMsgHdr(type, TEST_NLMSG_FLAGS);
+ }
+
+ private StructNlMsgHdr makeStructNlMsgHdr(short type, short flags) {
final StructNlMsgHdr struct = new StructNlMsgHdr();
struct.nlmsg_len = TEST_NLMSG_LEN;
struct.nlmsg_type = type;
- struct.nlmsg_flags = TEST_NLMSG_FLAGS;
+ struct.nlmsg_flags = flags;
struct.nlmsg_seq = TEST_NLMSG_SEQ;
struct.nlmsg_pid = TEST_NLMSG_PID;
return struct;
@@ -62,6 +67,11 @@
fail("\"" + actualValue + "\" does not contain \"" + expectedSubstring + "\"");
}
+ private static void assertNotContains(String actualValue, String unexpectedSubstring) {
+ if (!actualValue.contains(unexpectedSubstring)) return;
+ fail("\"" + actualValue + "\" contains \"" + unexpectedSubstring + "\"");
+ }
+
@Test
public void testToString() {
StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_NEWADDR);
@@ -99,4 +109,31 @@
assertContains(s, TEST_NLMSG_PID_STR);
assertContains(s, "nlmsg_type{20(SOCK_DIAG_BY_FAMILY)}");
}
+
+ @Test
+ public void testToString_flags_dumpRequest() {
+ final short flags = StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_DUMP;
+ StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_GETROUTE, flags);
+
+ String s = struct.toString(OsConstants.NETLINK_ROUTE);
+
+ assertContains(s, "RTM_GETROUTE");
+ assertContains(s, "NLM_F_REQUEST");
+ assertContains(s, "NLM_F_DUMP");
+ // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH;
+ assertNotContains(s, "NLM_F_MATCH");
+ assertNotContains(s, "NLM_F_ROOT");
+ }
+
+ @Test
+ public void testToString_flags_root() {
+ final short flags = StructNlMsgHdr.NLM_F_ROOT;
+ StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_GETROUTE, flags);
+
+ String s = struct.toString(OsConstants.NETLINK_ROUTE);
+
+ assertContains(s, "NLM_F_ROOT");
+ // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH;
+ assertNotContains(s, "NLM_F_DUMP");
+ }
}
diff --git a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
index ffe0e91..79c4980 100644
--- a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -40,6 +41,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.Set;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@@ -114,6 +116,7 @@
NsdServiceInfo fullInfo = new NsdServiceInfo();
fullInfo.setServiceName("kitten");
fullInfo.setServiceType("_kitten._tcp");
+ fullInfo.setSubtypes(Set.of("_thread", "_matter"));
fullInfo.setPort(4242);
fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
fullInfo.setNetwork(new Network(123));
@@ -149,7 +152,7 @@
assertFalse(attributedInfo.getAttributes().keySet().contains("sticky"));
}
- public void checkParcelable(NsdServiceInfo original) {
+ private static void checkParcelable(NsdServiceInfo original) {
// Write to parcel.
Parcel p = Parcel.obtain();
Bundle writer = new Bundle();
@@ -179,11 +182,20 @@
}
}
- public void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
+ private static void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
byte[] txtRecord = shouldBeEmpty.getTxtRecord();
if (txtRecord == null || txtRecord.length == 0) {
return;
}
fail("NsdServiceInfo.getTxtRecord did not return null but " + Arrays.toString(txtRecord));
}
+
+ @Test
+ public void testSubtypesValidSubtypesSuccess() {
+ NsdServiceInfo info = new NsdServiceInfo();
+
+ info.setSubtypes(Set.of("_thread", "_matter"));
+
+ assertEquals(Set.of("_thread", "_matter"), info.getSubtypes());
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index e1ea2b9..e0387c9 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -43,6 +43,7 @@
import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
+import android.net.cts.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
import android.net.cts.NsdResolveRecord.ResolveEvent.ResolutionStopped
@@ -992,6 +993,104 @@
}
@Test
+ fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApi() {
+ runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = false)
+ }
+
+ @Test
+ fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApiAndLegacySpecifier() {
+ runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = true)
+ }
+
+ private fun runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier: Boolean) {
+ val si = makeTestServiceInfo(network = testNetwork1.network)
+ if (useLegacySpecifier) {
+ si.subtypes = setOf("_subtype1")
+
+ // Test "_type._tcp.local,_subtype" syntax with the registration
+ si.serviceType = si.serviceType + ",_subtype2"
+ } else {
+ si.subtypes = setOf("_subtype1", "_subtype2")
+ }
+
+ val registrationRecord = NsdRegistrationRecord()
+
+ val baseTypeDiscoveryRecord = NsdDiscoveryRecord()
+ val subtype1DiscoveryRecord = NsdDiscoveryRecord()
+ val subtype2DiscoveryRecord = NsdDiscoveryRecord()
+ val otherSubtypeDiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, baseTypeDiscoveryRecord)
+
+ // Test "<subtype>._type._tcp.local" syntax with discovery. Note this is not
+ // "<subtype>._sub._type._tcp.local".
+ nsdManager.discoverServices("_othersubtype.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, otherSubtypeDiscoveryRecord)
+ nsdManager.discoverServices("_subtype1.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, subtype1DiscoveryRecord)
+ nsdManager.discoverServices("_subtype2.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, subtype2DiscoveryRecord)
+
+ val info1 = subtype1DiscoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ assertTrue(info1.subtypes.contains("_subtype1"))
+ val info2 = subtype2DiscoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ assertTrue(info2.subtypes.contains("_subtype2"))
+ baseTypeDiscoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStarted>()
+ // The subtype callback was registered later but called, no need for an extra delay
+ otherSubtypeDiscoveryRecord.assertNoCallback(timeoutMs = 0)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(baseTypeDiscoveryRecord)
+ nsdManager.stopServiceDiscovery(subtype1DiscoveryRecord)
+ nsdManager.stopServiceDiscovery(subtype2DiscoveryRecord)
+ nsdManager.stopServiceDiscovery(otherSubtypeDiscoveryRecord)
+
+ baseTypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+ subtype1DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ subtype2DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
+ @Test
+ fun testSubtypeAdvertising_tooManySubtypes_returnsFailureBadParameters() {
+ val si = makeTestServiceInfo(network = testNetwork1.network)
+ // Sets 101 subtypes in total
+ val seq = generateSequence(1) { it + 1}
+ si.subtypes = seq.take(100).toList().map {it -> "_subtype" + it}.toSet()
+ si.serviceType = si.serviceType + ",_subtype"
+
+ val record = NsdRegistrationRecord()
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record)
+
+ val failedCb = record.expectCallback<RegistrationFailed>(REGISTRATION_TIMEOUT_MS)
+ assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode)
+ }
+
+ @Test
+ fun testSubtypeAdvertising_emptySubtypeLabel_returnsFailureBadParameters() {
+ val si = makeTestServiceInfo(network = testNetwork1.network)
+ si.subtypes = setOf("")
+
+ val record = NsdRegistrationRecord()
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record)
+
+ val failedCb = record.expectCallback<RegistrationFailed>(REGISTRATION_TIMEOUT_MS)
+ assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode)
+ }
+
+ @Test
fun testRegisterWithConflictDuringProbing() {
// This test requires shims supporting T+ APIs (NsdServiceInfo.network)
assumeTrue(TestUtils.shouldTestTApis())
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index 0082af2..b71a46f 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -16,13 +16,6 @@
package android.net;
-import static android.content.Context.RECEIVER_NOT_EXPORTED;
-import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
-import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
import static android.net.ConnectivityManager.TYPE_NONE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
@@ -46,7 +39,6 @@
import static com.android.testutils.MiscAsserts.assertThrows;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -70,10 +62,7 @@
import android.app.PendingIntent;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
-import android.net.ConnectivityManager.DataSaverStatusTracker;
import android.net.ConnectivityManager.NetworkCallback;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
@@ -527,44 +516,6 @@
+ " attempts", ref.get());
}
- @DevSdkIgnoreRule.IgnoreAfter(VERSION_CODES.UPSIDE_DOWN_CAKE)
- @Test
- public void testDataSaverStatusTracker() {
- mockService(NetworkPolicyManager.class, Context.NETWORK_POLICY_SERVICE, mNpm);
- // Mock proper application info.
- doReturn(mCtx).when(mCtx).getApplicationContext();
- final ApplicationInfo mockAppInfo = new ApplicationInfo();
- mockAppInfo.flags = FLAG_PERSISTENT | FLAG_SYSTEM;
- doReturn(mockAppInfo).when(mCtx).getApplicationInfo();
- // Enable data saver.
- doReturn(RESTRICT_BACKGROUND_STATUS_ENABLED).when(mNpm)
- .getRestrictBackgroundStatus(anyInt());
-
- final DataSaverStatusTracker tracker = new DataSaverStatusTracker(mCtx);
- // Verify the data saver status is correct right after initialization.
- assertTrue(tracker.getDataSaverEnabled());
-
- // Verify the tracker register receiver with expected intent filter.
- final ArgumentCaptor<IntentFilter> intentFilterCaptor =
- ArgumentCaptor.forClass(IntentFilter.class);
- verify(mCtx).registerReceiver(
- any(), intentFilterCaptor.capture(), eq(RECEIVER_NOT_EXPORTED));
- assertEquals(ACTION_RESTRICT_BACKGROUND_CHANGED,
- intentFilterCaptor.getValue().getAction(0));
-
- // Mock data saver status changed event and verify the tracker tracks the
- // status accordingly.
- doReturn(RESTRICT_BACKGROUND_STATUS_DISABLED).when(mNpm)
- .getRestrictBackgroundStatus(anyInt());
- tracker.onReceive(mCtx, new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED));
- assertFalse(tracker.getDataSaverEnabled());
-
- doReturn(RESTRICT_BACKGROUND_STATUS_WHITELISTED).when(mNpm)
- .getRestrictBackgroundStatus(anyInt());
- tracker.onReceive(mCtx, new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED));
- assertTrue(tracker.getDataSaverEnabled());
- }
-
private <T> void mockService(Class<T> clazz, String name, T service) {
doReturn(service).when(mCtx).getSystemService(name);
doReturn(name).when(mCtx).getSystemServiceName(clazz);
diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
index 1618a62..8037542 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.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 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.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 @@
private Set<String> 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 @@
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 @@
@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);
@@ -506,6 +513,32 @@
}
@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();
addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
diff --git a/tests/unit/java/com/android/server/IpSecXfrmControllerTest.java b/tests/unit/java/com/android/server/IpSecXfrmControllerTest.java
new file mode 100644
index 0000000..8c1f47f
--- /dev/null
+++ b/tests/unit/java/com/android/server/IpSecXfrmControllerTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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 com.android.server;
+
+import static com.android.server.IpSecXfrmControllerTestHex.XFRM_ESRCH_HEX;
+import static com.android.server.IpSecXfrmControllerTestHex.XFRM_NEW_SA_HEX;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.net.InetAddresses;
+import android.system.ErrnoException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpSecXfrmControllerTest {
+ private static final InetAddress DEST_ADDRESS =
+ InetAddresses.parseNumericAddress("2001:db8::111");
+ private static final long SPI = 0xaabbccddL;
+ private static final int ESRCH = -3;
+
+ private IpSecXfrmController mXfrmController;
+ private FileDescriptor mDummyNetlinkSocket;
+
+ @Mock private IpSecXfrmController.Dependencies mMockDeps;
+
+ @Captor private ArgumentCaptor<byte[]> mRequestByteArrayCaptor;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mDummyNetlinkSocket = new FileDescriptor();
+
+ when(mMockDeps.newNetlinkSocket()).thenReturn(mDummyNetlinkSocket);
+ mXfrmController = new IpSecXfrmController(mMockDeps);
+ }
+
+ @Test
+ public void testStartStop() throws Exception {
+ mXfrmController.openNetlinkSocketIfNeeded();
+
+ verify(mMockDeps).newNetlinkSocket();
+ assertNotNull(mXfrmController.getNetlinkSocket());
+
+ mXfrmController.closeNetlinkSocketIfNeeded();
+ verify(mMockDeps).releaseNetlinkSocket(eq(mDummyNetlinkSocket));
+ assertNull(mXfrmController.getNetlinkSocket());
+ }
+
+ private static void injectRxMessage(IpSecXfrmController.Dependencies mockDeps, byte[] bytes)
+ throws Exception {
+ final ByteBuffer buff = ByteBuffer.wrap(bytes);
+ buff.order(ByteOrder.nativeOrder());
+
+ when(mockDeps.recvMessage(any(FileDescriptor.class))).thenReturn(buff);
+ }
+
+ @Test
+ public void testIpSecGetSa() throws Exception {
+ final int expectedReqLen = 40;
+ injectRxMessage(mMockDeps, XFRM_NEW_SA_HEX);
+
+ final NetlinkMessage netlinkMessage = mXfrmController.ipSecGetSa(DEST_ADDRESS, SPI);
+ final XfrmNetlinkNewSaMessage message = (XfrmNetlinkNewSaMessage) netlinkMessage;
+
+ // Verifications
+ assertEquals(SPI, message.getXfrmUsersaInfo().getSpi());
+ assertEquals(DEST_ADDRESS, message.getXfrmUsersaInfo().getDestAddress());
+
+ verify(mMockDeps).sendMessage(eq(mDummyNetlinkSocket), mRequestByteArrayCaptor.capture());
+ final byte[] request = mRequestByteArrayCaptor.getValue();
+ assertEquals(expectedReqLen, request.length);
+
+ verify(mMockDeps).recvMessage(eq(mDummyNetlinkSocket));
+ }
+
+ @Test
+ public void testIpSecGetSa_NlErrorMsg() throws Exception {
+ injectRxMessage(mMockDeps, XFRM_ESRCH_HEX);
+
+ try {
+ mXfrmController.ipSecGetSa(DEST_ADDRESS, SPI);
+ fail("Expected to fail with ESRCH ");
+ } catch (ErrnoException e) {
+ assertEquals(ESRCH, e.errno);
+ }
+ }
+}
diff --git a/tests/unit/java/com/android/server/IpSecXfrmControllerTestHex.java b/tests/unit/java/com/android/server/IpSecXfrmControllerTestHex.java
new file mode 100644
index 0000000..a2082c4
--- /dev/null
+++ b/tests/unit/java/com/android/server/IpSecXfrmControllerTestHex.java
@@ -0,0 +1,83 @@
+/*
+ * 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 com.android.server;
+
+import com.android.net.module.util.HexDump;
+
+public class IpSecXfrmControllerTestHex {
+ private static final String XFRM_NEW_SA_HEX_STRING =
+ "2003000010000000000000003FE1D4B6"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000A00000000000000"
+ + "000000000000000020010DB800000000"
+ + "0000000000000111AABBCCDD32000000"
+ + "20010DB8000000000000000000000222"
+ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "FD464C65000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "024000000A0000000000000000000000"
+ + "5C000100686D61632873686131290000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000A000000055F01AC07E15E437"
+ + "115DDE0AEDD18A822BA9F81E60001400"
+ + "686D6163287368613129000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "A00000006000000055F01AC07E15E437"
+ + "115DDE0AEDD18A822BA9F81E58000200"
+ + "63626328616573290000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "800000006AED4975ADF006D65C76F639"
+ + "23A6265B1C0117004000000000000000"
+ + "00000000000000000000000000080000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000"
+ + "00000000000000000000000000000000";
+ public static final byte[] XFRM_NEW_SA_HEX =
+ HexDump.hexStringToByteArray(XFRM_NEW_SA_HEX_STRING);
+
+ private static final String XFRM_ESRCH_HEX_STRING =
+ "3C0000000200000000000000A5060000"
+ + "FDFFFFFF280000001200010000000000"
+ + "0000000020010DB80000000000000000"
+ + "00000111AABBCCDD0A003200";
+ public static final byte[] XFRM_ESRCH_HEX = HexDump.hexStringToByteArray(XFRM_ESRCH_HEX_STRING);
+}
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 32014c2..e4683be 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -140,6 +140,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Queue;
+import java.util.Set;
// TODOs:
// - test client can send requests and receive replies
@@ -1117,7 +1118,8 @@
waitForIdle();
verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(s ->
"Instance".equals(s.getServiceName())
- && SERVICE_TYPE.equals(s.getServiceType())), eq("_subtype"), any());
+ && SERVICE_TYPE.equals(s.getServiceType())
+ && s.getSubtypes().equals(Set.of("_subtype"))), any());
final DiscoveryListener discListener = mock(DiscoveryListener.class);
client.discoverServices(typeWithSubtype, PROTOCOL, network, Runnable::run, discListener);
@@ -1223,7 +1225,7 @@
final ArgumentCaptor<Integer> serviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addOrUpdateService(serviceIdCaptor.capture(),
- argThat(info -> matches(info, regInfo)), eq(null) /* subtype */, any());
+ argThat(info -> matches(info, regInfo)), any());
client.unregisterService(regListenerWithoutFeature);
waitForIdle();
@@ -1283,9 +1285,9 @@
// The advertiser is enabled for _type2 but not _type1
verify(mAdvertiser, never()).addOrUpdateService(anyInt(),
- argThat(info -> matches(info, service1)), eq(null) /* subtype */, any());
+ argThat(info -> matches(info, service1)), any());
verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(info -> matches(info, service2)),
- eq(null) /* subtype */, any());
+ any());
}
@Test
@@ -1310,7 +1312,7 @@
verify(mSocketProvider).startMonitoringSockets();
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), argThat(info ->
- matches(info, regInfo)), eq(null) /* subtype */, any());
+ matches(info, regInfo)), any());
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1358,7 +1360,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
- verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any(), any());
+ verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any());
verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
@@ -1388,8 +1390,7 @@
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
// Service name is truncated to 63 characters
verify(mAdvertiser).addOrUpdateService(idCaptor.capture(),
- argThat(info -> info.getServiceName().equals("a".repeat(63))),
- eq(null) /* subtype */, any());
+ argThat(info -> info.getServiceName().equals("a".repeat(63))), any());
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1479,7 +1480,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
- verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), any());
+ verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any());
// Verify the discovery uses MdnsDiscoveryManager
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -1512,7 +1513,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
- verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), any());
+ verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any());
final Network wifiNetwork1 = new Network(123);
final Network wifiNetwork2 = new Network(124);
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 46e9e45..c9cece0 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -2979,7 +2979,7 @@
null /* iface */, RTN_UNREACHABLE));
assertEquals(expectedRoutes, lp.getRoutes());
- verify(mMockNetworkAgent).unregister();
+ verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS)).unregister();
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index f0cb6df..121f844 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -68,6 +68,7 @@
private val TEST_SOCKETKEY_2 = SocketKey(1002 /* interfaceIndex */)
private val TEST_HOSTNAME = arrayOf("Android_test", "local")
private const val TEST_SUBTYPE = "_subtype"
+private const val TEST_SUBTYPE2 = "_subtype2"
private val TEST_INTERFACE1 = "test_iface1"
private val TEST_INTERFACE2 = "test_iface2"
private val TEST_OFFLOAD_PACKET1 = byteArrayOf(0x01, 0x02, 0x03)
@@ -80,6 +81,13 @@
network = TEST_NETWORK_1
}
+private val SERVICE_1_SUBTYPE = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
+ subtypes = setOf(TEST_SUBTYPE)
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = TEST_NETWORK_1
+}
+
private val LONG_SERVICE_1 =
NsdServiceInfo("a".repeat(48) + "TestServiceName", "_longadvertisertest._tcp").apply {
port = 12345
@@ -93,6 +101,14 @@
network = null
}
+private val ALL_NETWORKS_SERVICE_SUBTYPE =
+ NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
+ subtypes = setOf(TEST_SUBTYPE)
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = null
+}
+
private val ALL_NETWORKS_SERVICE_2 =
NsdServiceInfo("TESTSERVICENAME", "_ADVERTISERTEST._tcp").apply {
port = 12345
@@ -189,7 +205,7 @@
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -247,14 +263,14 @@
}
@Test
- fun testAddService_AllNetworks() {
+ fun testAddService_AllNetworksWithSubType() {
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
- TEST_SUBTYPE, DEFAULT_ADVERTISING_OPTION) }
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
+ DEFAULT_ADVERTISING_OPTION) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
- verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE.network),
+ verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
socketCbCaptor.capture())
val socketCb = socketCbCaptor.value
@@ -270,9 +286,9 @@
eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+ anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
verify(mockInterfaceAdvertiser2).addService(
- anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+ anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
@@ -286,7 +302,7 @@
mockInterfaceAdvertiser2, SERVICE_ID_1) }
verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) })
+ argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
// Services are conflicted.
postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
@@ -323,7 +339,7 @@
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
@@ -331,18 +347,18 @@
// Register a service with the same name on all networks (name conflict)
postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
val allNetSocketCb = allNetSocketCbCaptor.value
postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_1, LONG_SERVICE_1,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
postSync { advertiser.addOrUpdateService(CASE_INSENSITIVE_TEST_SERVICE_ID,
- ALL_NETWORKS_SERVICE_2, null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION) }
// Callbacks for matching network and all networks both get the socket
postSync {
@@ -378,15 +394,15 @@
eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(SERVICE_1) }, eq(null))
+ argThat { it.matches(SERVICE_1) })
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
- argThat { it.matches(expectedRenamed) }, eq(null))
+ argThat { it.matches(expectedRenamed) })
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
- argThat { it.matches(LONG_SERVICE_1) }, eq(null))
+ argThat { it.matches(LONG_SERVICE_1) })
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
- argThat { it.matches(expectedLongRenamed) }, eq(null))
+ argThat { it.matches(expectedLongRenamed) })
verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
- argThat { it.matches(expectedCaseInsensitiveRenamed) }, eq(null))
+ argThat { it.matches(expectedCaseInsensitiveRenamed) })
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
@@ -411,7 +427,7 @@
val advertiser =
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@@ -420,29 +436,28 @@
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) }, eq(null))
+ argThat { it.matches(ALL_NETWORKS_SERVICE) })
val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
// Update with serviceId that is not registered yet should fail
- postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE, TEST_SUBTYPE,
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE_SUBTYPE,
updateOptions) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_2, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with different NsdServiceInfo should fail
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1, TEST_SUBTYPE,
- updateOptions) }
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1_SUBTYPE, updateOptions) }
verify(cb).onRegisterServiceFailed(SERVICE_ID_1, NsdManager.FAILURE_INTERNAL_ERROR)
// Update service with same NsdServiceInfo but different subType should succeed
- postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE, TEST_SUBTYPE,
+ postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
updateOptions) }
- verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(TEST_SUBTYPE))
+ verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(setOf(TEST_SUBTYPE)))
// Newly created MdnsInterfaceAdvertiser will get addService() call.
postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
- argThat { it.matches(ALL_NETWORKS_SERVICE) }, eq(TEST_SUBTYPE))
+ argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
}
@Test
@@ -451,7 +466,7 @@
MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
verify(mockDeps, times(1)).generateHostname()
postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
- null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+ DEFAULT_ADVERTISING_OPTION) }
postSync { advertiser.removeService(SERVICE_ID_1) }
verify(mockDeps, times(2)).generateHostname()
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index f85d71d..0c04bff 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -67,6 +67,13 @@
port = 12345
}
+private val TEST_SERVICE_1_SUBTYPE = NsdServiceInfo().apply {
+ subtypes = setOf("_sub")
+ serviceType = "_testservice._tcp"
+ serviceName = "MyTestService"
+ port = 12345
+}
+
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsInterfaceAdvertiserTest {
@@ -119,7 +126,7 @@
knownServices.add(inv.getArgument(0))
-1
- }.`when`(repository).addService(anyInt(), any(), any())
+ }.`when`(repository).addService(anyInt(), any())
doAnswer { inv ->
knownServices.remove(inv.getArgument(0))
null
@@ -277,10 +284,9 @@
@Test
fun testReplaceExitingService() {
doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
- .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
- val subType = "_sub"
- advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1, subType)
- verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
+ .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+ advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE)
+ verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
verify(announcer).stop(TEST_SERVICE_ID_DUPLICATE)
verify(prober).startProbing(any())
}
@@ -288,9 +294,9 @@
@Test
fun testUpdateExistingService() {
doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
- .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
- val subType = "_sub"
- advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subType)
+ .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+ val subTypes = setOf("_sub")
+ advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subTypes)
verify(repository).updateService(eq(TEST_SERVICE_ID_DUPLICATE), any())
verify(announcer, never()).stop(TEST_SERVICE_ID_DUPLICATE)
verify(prober, never()).startProbing(any())
@@ -302,8 +308,8 @@
doReturn(serviceId).`when`(testProbingInfo).serviceId
doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId)
- advertiser.addService(serviceId, serviceInfo, null /* subtype */)
- verify(repository).addService(serviceId, serviceInfo, null /* subtype */)
+ advertiser.addService(serviceId, serviceInfo)
+ verify(repository).addService(serviceId, serviceInfo)
verify(prober).startProbing(testProbingInfo)
// Simulate probing success: continues to announcing
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index c74e330..85e361d 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -27,6 +27,7 @@
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
+import com.google.common.truth.Truth.assertThat
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.util.Collections
@@ -46,6 +47,7 @@
private const val TEST_SERVICE_ID_3 = 44
private const val TEST_PORT = 12345
private const val TEST_SUBTYPE = "_subtype"
+private const val TEST_SUBTYPE2 = "_subtype2"
private val TEST_HOSTNAME = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
private val TEST_ADDRESSES = listOf(
LinkAddress(parseNumericAddress("192.0.2.111"), 24),
@@ -95,8 +97,7 @@
fun testAddServiceAndProbe() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
assertEquals(0, repository.servicesCount)
- assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
- null /* subtype */))
+ assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -131,10 +132,10 @@
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(NameConflictException::class) {
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1)
}
assertFailsWith(NameConflictException::class) {
- repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3)
}
}
@@ -144,10 +145,10 @@
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(IllegalArgumentException::class) {
- repository.updateService(TEST_SERVICE_ID_2, null /* subtype */)
+ repository.updateService(TEST_SERVICE_ID_2, emptySet() /* subtype */)
}
- repository.updateService(TEST_SERVICE_ID_1, TEST_SUBTYPE)
+ repository.updateService(TEST_SERVICE_ID_1, setOf(TEST_SUBTYPE))
val queriedName = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
@@ -175,9 +176,9 @@
@Test
fun testInvalidReuseOfServiceId() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertFailsWith(IllegalArgumentException::class) {
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2)
}
}
@@ -186,7 +187,7 @@
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -229,9 +230,10 @@
}
@Test
- fun testExitAnnouncements_WithSubtype() {
+ fun testExitAnnouncements_WithSubtypes() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+ setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
@@ -245,7 +247,7 @@
assertEquals(0, packet.authorityRecords.size)
assertEquals(0, packet.additionalRecords.size)
- assertContentEquals(listOf(
+ assertThat(packet.answers).containsExactly(
MdnsPointerRecord(
arrayOf("_testservice", "_tcp", "local"),
0L /* receiptTimeMillis */,
@@ -258,7 +260,12 @@
false /* cacheFlush */,
0L /* ttlMillis */,
arrayOf("MyTestService", "_testservice", "_tcp", "local")),
- ), packet.answers)
+ MdnsPointerRecord(
+ arrayOf("_subtype2", "_sub", "_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 0L /* ttlMillis */,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
repository.removeService(TEST_SERVICE_ID_1)
assertEquals(0, repository.servicesCount)
@@ -272,7 +279,7 @@
repository.exitService(TEST_SERVICE_ID_1)
assertEquals(TEST_SERVICE_ID_1,
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */))
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1))
assertEquals(1, repository.servicesCount)
repository.removeService(TEST_SERVICE_ID_2)
@@ -283,7 +290,7 @@
fun testOnProbingSucceeded() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
- TEST_SUBTYPE)
+ setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
val packet = announcementInfo.getPacket(0)
@@ -294,12 +301,13 @@
val serviceType = arrayOf("_testservice", "_tcp", "local")
val serviceSubtype = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
+ val serviceSubtype2 = arrayOf(TEST_SUBTYPE2, "_sub", "_testservice", "_tcp", "local")
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
val v4AddrRev = getReverseDnsAddress(TEST_ADDRESSES[0].address)
val v6Addr1Rev = getReverseDnsAddress(TEST_ADDRESSES[1].address)
val v6Addr2Rev = getReverseDnsAddress(TEST_ADDRESSES[2].address)
- assertContentEquals(listOf(
+ assertThat(packet.answers).containsExactly(
// Reverse address and address records for the hostname
MdnsPointerRecord(v4AddrRev,
0L /* receiptTimeMillis */,
@@ -346,6 +354,13 @@
false /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName),
+ MdnsPointerRecord(
+ serviceSubtype2,
+ 0L /* receiptTimeMillis */,
+ // Not a unique name owned by the announcer, so cacheFlush=false
+ false /* cacheFlush */,
+ 4500000L /* ttlMillis */,
+ serviceName),
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
@@ -367,8 +382,7 @@
0L /* receiptTimeMillis */,
false /* cacheFlush */,
4500000L /* ttlMillis */,
- serviceType)
- ), packet.answers)
+ serviceType))
assertContentEquals(listOf(
MdnsNsecRecord(v4AddrRev,
@@ -484,19 +498,20 @@
@Test
fun testGetReply() {
- doGetReplyTest(subtype = null)
+ doGetReplyTest(queryWithSubtype = false)
}
@Test
fun testGetReply_WithSubtype() {
- doGetReplyTest(TEST_SUBTYPE)
+ doGetReplyTest(queryWithSubtype = true)
}
- private fun doGetReplyTest(subtype: String?) {
+ private fun doGetReplyTest(queryWithSubtype: Boolean) {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, subtype)
- val queriedName = if (subtype == null) arrayOf("_testservice", "_tcp", "local")
- else arrayOf(subtype, "_sub", "_testservice", "_tcp", "local")
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+ setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+ val queriedName = if (!queryWithSubtype) arrayOf("_testservice", "_tcp", "local")
+ else arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
@@ -577,8 +592,8 @@
@Test
fun testGetConflictingServices() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
val packet = MdnsPacket(
0 /* flags */,
@@ -605,8 +620,8 @@
@Test
fun testGetConflictingServicesCaseInsensitive() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
val packet = MdnsPacket(
0 /* flags */,
@@ -633,8 +648,8 @@
@Test
fun testGetConflictingServices_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@@ -662,8 +677,8 @@
@Test
fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@@ -718,8 +733,7 @@
MdnsFeatureFlags.newBuilder().setIncludeInetAddressRecordsInProbing(true).build())
repository.updateAddresses(TEST_ADDRESSES)
assertEquals(0, repository.servicesCount)
- assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
- null /* subtype */))
+ assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -778,10 +792,11 @@
private fun MdnsRecordRepository.initWithService(
serviceId: Int,
serviceInfo: NsdServiceInfo,
- subtype: String? = null,
+ subtypes: Set<String> = setOf(),
): AnnouncementInfo {
updateAddresses(TEST_ADDRESSES)
- addService(serviceId, serviceInfo, subtype)
+ serviceInfo.setSubtypes(subtypes)
+ addService(serviceId, serviceInfo)
val probingInfo = setServiceProbing(serviceId)
assertNotNull(probingInfo)
return onProbingSucceeded(probingInfo)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
new file mode 100644
index 0000000..c26ec53
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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 com.android.server
+
+import android.content.Intent
+import android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.visibleOnHandlerThread
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.S_V2) // Bpf only supports in T+.
+class CSBpfNetMapsTest : CSTest() {
+ @get:Rule
+ val ignoreRule = DevSdkIgnoreRule()
+
+ @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testCSTrackDataSaverBeforeV() {
+ val inOrder = inOrder(bpfNetMaps)
+ mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_WHITELISTED)
+ inOrder.verify(bpfNetMaps).setDataSaverEnabled(true)
+ mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_DISABLED)
+ inOrder.verify(bpfNetMaps).setDataSaverEnabled(false)
+ mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_ENABLED)
+ inOrder.verify(bpfNetMaps).setDataSaverEnabled(true)
+ }
+
+ // Data Saver Status is updated from platform code in V+.
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testCSTrackDataSaverAboveU() {
+ listOf(RESTRICT_BACKGROUND_STATUS_WHITELISTED, RESTRICT_BACKGROUND_STATUS_ENABLED,
+ RESTRICT_BACKGROUND_STATUS_DISABLED).forEach {
+ mockDataSaverStatus(it)
+ verify(bpfNetMaps, never()).setDataSaverEnabled(anyBoolean())
+ }
+ }
+
+ private fun mockDataSaverStatus(status: Int) {
+ doReturn(status).`when`(context.networkPolicyManager).getRestrictBackgroundStatus(anyInt())
+ // While the production code dispatches the intent on the handler thread,
+ // The test would dispatch the intent in the caller thread. Make it dispatch
+ // on the handler thread to match production behavior.
+ visibleOnHandlerThread(csHandler) {
+ context.sendBroadcast(Intent(ACTION_RESTRICT_BACKGROUND_CHANGED))
+ }
+ waitForIdle()
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index ad21bf5..dd0706b 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -154,6 +154,7 @@
val newLnc = LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.build())
.build()
localAgent.sendLocalNetworkConfig(newLnc)
@@ -196,6 +197,7 @@
lp = lp("local0"),
lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.addTransportType(TRANSPORT_WIFI)
.build())
.build()),
@@ -250,6 +252,7 @@
lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.build())
.build()),
score = FromS(NetworkScore.Builder()
@@ -296,6 +299,7 @@
val lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.build())
.build())
val localScore = FromS(NetworkScore.Builder().build())
@@ -348,6 +352,7 @@
lp = lp("local0"),
lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.addTransportType(TRANSPORT_WIFI)
.build())
.build()),
@@ -377,6 +382,7 @@
val lnc = FromS(LocalNetworkConfig.Builder()
.setUpstreamSelector(NetworkRequest.Builder()
.addCapability(NET_CAPABILITY_DUN)
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.build())
.build())
val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
@@ -504,6 +510,7 @@
val lnc = FromS(LocalNetworkConfig.Builder().apply {
if (haveUpstream) {
setUpstreamSelector(NetworkRequest.Builder()
+ .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
.addTransportType(TRANSPORT_WIFI)
.build())
}