Merge "Ignores vim temporary files from git" into main
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index a0b2434..7b52694 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -62,6 +62,7 @@
test_suites: [
"cts",
"general-tests",
- "mts-tethering"
+ "mts-tethering",
+ "mcts-tethering",
],
}
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 414e50a..73c11ba 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -94,14 +94,17 @@
"ConnectivityNextEnableDefaults",
"TetheringAndroidLibraryDefaults",
"TetheringApiLevel",
- "TetheringReleaseTargetSdk"
+ "TetheringReleaseTargetSdk",
],
static_libs: [
"NetworkStackApiCurrentShims",
"net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
+ },
}
android_library {
@@ -109,14 +112,17 @@
defaults: [
"TetheringAndroidLibraryDefaults",
"TetheringApiLevel",
- "TetheringReleaseTargetSdk"
+ "TetheringReleaseTargetSdk",
],
static_libs: [
"NetworkStackApiStableShims",
"net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
+ },
}
// Due to b/143733063, APK can't access a jni lib that is in APEX (but not in the APK).
@@ -189,20 +195,28 @@
optimize: {
proguard_flags_files: ["proguard.flags"],
},
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
// Updatable tethering packaged for finalized API
android_app {
name: "Tethering",
- defaults: ["TetheringAppDefaults", "TetheringApiLevel"],
+ defaults: [
+ "TetheringAppDefaults",
+ "TetheringApiLevel",
+ ],
static_libs: ["TetheringApiStableLib"],
certificate: "networkstack",
manifest: "AndroidManifest.xml",
use_embedded_native_libs: true,
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
+ },
}
android_app {
@@ -221,6 +235,7 @@
lint: {
strict_updatability_linting: true,
error_checks: ["NewApi"],
+ baseline_filename: "lint-baseline.xml",
},
}
@@ -239,19 +254,24 @@
java_library_static {
name: "tetheringstatsprotos",
- proto: {type: "lite"},
+ proto: {
+ type: "lite",
+ },
srcs: [
"src/com/android/networkstack/tethering/metrics/stats.proto",
],
static_libs: ["tetheringprotos"],
apex_available: ["com.android.tethering"],
min_sdk_version: "30",
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
genrule {
name: "statslog-tethering-java-gen",
tools: ["stats-log-api-gen"],
cmd: "$(location stats-log-api-gen) --java $(out) --module network_tethering" +
- " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
+ " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
out: ["com/android/networkstack/tethering/metrics/TetheringStatsLog.java"],
}
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 6e8d0c9..bcea425 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -43,6 +43,7 @@
"//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
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/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index 5e9bbcb..50d6c4b 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -18,7 +18,6 @@
import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.IPPROTO_ICMPV6;
-import static android.system.OsConstants.SOCK_NONBLOCK;
import static android.system.OsConstants.SOCK_RAW;
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
@@ -39,21 +38,12 @@
import android.net.MacAddress;
import android.net.TrafficStats;
import android.net.util.SocketUtils;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructTimeval;
import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.internal.annotations.GuardedBy;
-import com.android.net.module.util.FdEventsReader;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.structs.Icmpv6Header;
import com.android.net.module.util.structs.LlaOption;
@@ -113,11 +103,6 @@
private static final int DAY_IN_SECONDS = 86_400;
- // Commands for IpServer to control RouterAdvertisementDaemon
- private static final int CMD_START = 1;
- private static final int CMD_STOP = 2;
- private static final int CMD_BUILD_NEW_RA = 3;
-
private final InterfaceParams mInterface;
private final InetSocketAddress mAllNodes;
@@ -135,13 +120,9 @@
@GuardedBy("mLock")
private RaParams mRaParams;
- // To be accessed only from RaMessageHandler
- private RsPacketListener mRsPacketListener;
-
private volatile FileDescriptor mSocket;
private volatile MulticastTransmitter mMulticastTransmitter;
- private volatile RaMessageHandler mRaMessageHandler;
- private volatile HandlerThread mRaHandlerThread;
+ private volatile UnicastResponder mUnicastResponder;
/** Encapsulate the RA parameters for RouterAdvertisementDaemon.*/
public static class RaParams {
@@ -263,94 +244,6 @@
}
}
- private class RaMessageHandler extends Handler {
- RaMessageHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case CMD_START:
- mRsPacketListener = new RsPacketListener(this);
- mRsPacketListener.start();
- break;
- case CMD_STOP:
- if (mRsPacketListener != null) {
- mRsPacketListener.stop();
- mRsPacketListener = null;
- }
- break;
- case CMD_BUILD_NEW_RA:
- synchronized (mLock) {
- // raInfo.first is deprecatedParams and raInfo.second is newParams.
- final Pair<RaParams, RaParams> raInfo = (Pair<RaParams, RaParams>) msg.obj;
- if (raInfo.first != null) {
- mDeprecatedInfoTracker.putPrefixes(raInfo.first.prefixes);
- mDeprecatedInfoTracker.putDnses(raInfo.first.dnses);
- }
-
- if (raInfo.second != null) {
- // Process information that is no longer deprecated.
- mDeprecatedInfoTracker.removePrefixes(raInfo.second.prefixes);
- mDeprecatedInfoTracker.removeDnses(raInfo.second.dnses);
- }
- mRaParams = raInfo.second;
- assembleRaLocked();
- }
-
- maybeNotifyMulticastTransmitter();
- break;
- default:
- Log.e(TAG, "Unknown message, cmd = " + String.valueOf(msg.what));
- break;
- }
- }
- }
-
- private class RsPacketListener extends FdEventsReader<RsPacketListener.RecvBuffer> {
- private static final class RecvBuffer {
- // The recycled buffer for receiving Router Solicitations from clients.
- // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
- // This is fine since currently only byte 0 is examined anyway.
- final byte[] mBytes = new byte[IPV6_MIN_MTU];
- final InetSocketAddress mSrcAddr = new InetSocketAddress(0);
- }
-
- RsPacketListener(@NonNull Handler handler) {
- super(handler, new RecvBuffer());
- }
-
- @Override
- protected int recvBufSize(@NonNull RecvBuffer buffer) {
- return buffer.mBytes.length;
- }
-
- @Override
- protected FileDescriptor createFd() {
- return mSocket;
- }
-
- @Override
- protected int readPacket(@NonNull FileDescriptor fd, @NonNull RecvBuffer buffer)
- throws Exception {
- return Os.recvfrom(
- fd, buffer.mBytes, 0, buffer.mBytes.length, 0 /* flags */, buffer.mSrcAddr);
- }
-
- @Override
- protected final void handlePacket(@NonNull RecvBuffer buffer, int length) {
- // Do the least possible amount of validations.
- if (buffer.mSrcAddr == null
- || length <= 0
- || buffer.mBytes[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
- return;
- }
-
- maybeSendRA(buffer.mSrcAddr);
- }
- }
-
public RouterAdvertisementDaemon(InterfaceParams ifParams) {
mInterface = ifParams;
mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
@@ -359,43 +252,48 @@
/** Build new RA.*/
public void buildNewRa(RaParams deprecatedParams, RaParams newParams) {
- final Pair<RaParams, RaParams> raInfo = new Pair<>(deprecatedParams, newParams);
- sendMessage(CMD_BUILD_NEW_RA, raInfo);
+ synchronized (mLock) {
+ if (deprecatedParams != null) {
+ mDeprecatedInfoTracker.putPrefixes(deprecatedParams.prefixes);
+ mDeprecatedInfoTracker.putDnses(deprecatedParams.dnses);
+ }
+
+ if (newParams != null) {
+ // Process information that is no longer deprecated.
+ mDeprecatedInfoTracker.removePrefixes(newParams.prefixes);
+ mDeprecatedInfoTracker.removeDnses(newParams.dnses);
+ }
+
+ mRaParams = newParams;
+ assembleRaLocked();
+ }
+
+ maybeNotifyMulticastTransmitter();
}
/** Start router advertisement daemon. */
public boolean start() {
if (!createSocket()) {
- Log.e(TAG, "Failed to start RouterAdvertisementDaemon.");
return false;
}
mMulticastTransmitter = new MulticastTransmitter();
mMulticastTransmitter.start();
- mRaHandlerThread = new HandlerThread(TAG);
- mRaHandlerThread.start();
- mRaMessageHandler = new RaMessageHandler(mRaHandlerThread.getLooper());
+ mUnicastResponder = new UnicastResponder();
+ mUnicastResponder.start();
- return sendMessage(CMD_START);
+ return true;
}
/** Stop router advertisement daemon. */
public void stop() {
- if (!sendMessage(CMD_STOP)) {
- Log.e(TAG, "RouterAdvertisementDaemon has been stopped or was never started.");
- return;
- }
-
- mRaHandlerThread.quitSafely();
- mRaHandlerThread = null;
- mRaMessageHandler = null;
-
closeSocket();
// Wake up mMulticastTransmitter thread to interrupt a potential 1 day sleep before
// the thread's termination.
maybeNotifyMulticastTransmitter();
mMulticastTransmitter = null;
+ mUnicastResponder = null;
}
@GuardedBy("mLock")
@@ -605,7 +503,7 @@
final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_NEIGHBOR);
try {
- mSocket = Os.socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_ICMPV6);
+ mSocket = Os.socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
// Setting SNDTIMEO is purely for defensive purposes.
Os.setsockoptTimeval(
mSocket, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(send_timout_ms));
@@ -667,17 +565,34 @@
}
}
- private boolean sendMessage(int cmd) {
- return sendMessage(cmd, null);
- }
+ private final class UnicastResponder extends Thread {
+ private final InetSocketAddress mSolicitor = new InetSocketAddress(0);
+ // The recycled buffer for receiving Router Solicitations from clients.
+ // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
+ // This is fine since currently only byte 0 is examined anyway.
+ private final byte[] mSolicitation = new byte[IPV6_MIN_MTU];
- private boolean sendMessage(int cmd, @Nullable Object obj) {
- if (mRaMessageHandler == null) {
- return false;
+ @Override
+ public void run() {
+ while (isSocketValid()) {
+ try {
+ // Blocking receive.
+ final int rval = Os.recvfrom(
+ mSocket, mSolicitation, 0, mSolicitation.length, 0, mSolicitor);
+ // Do the least possible amount of validation.
+ if (rval < 1 || mSolicitation[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
+ continue;
+ }
+ } catch (ErrnoException | SocketException e) {
+ if (isSocketValid()) {
+ Log.e(TAG, "recvfrom error: " + e);
+ }
+ continue;
+ }
+
+ maybeSendRA(mSolicitor);
+ }
}
-
- return mRaMessageHandler.sendMessage(
- Message.obtain(mRaMessageHandler, cmd, obj));
}
// TODO: Consider moving this to run on a provided Looper as a Handler,
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/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 377da91..c232697 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -31,12 +31,14 @@
import static android.net.TetheringTester.isExpectedIcmpPacket;
import static android.net.TetheringTester.isExpectedTcpPacket;
import static android.net.TetheringTester.isExpectedUdpPacket;
+
import static com.android.net.module.util.HexDump.dumpHexString;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -46,7 +48,6 @@
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
-import android.app.UiAutomation;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.EthernetManager.TetheredInterfaceCallback;
@@ -56,8 +57,6 @@
import android.net.TetheringManager.TetheringRequest;
import android.net.TetheringTester.TetheredDevice;
import android.net.cts.util.CtsNetUtils;
-import android.net.cts.util.CtsTetheringUtils;
-import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
@@ -141,11 +140,12 @@
protected static final ByteBuffer TX_PAYLOAD =
ByteBuffer.wrap(new byte[] { (byte) 0x56, (byte) 0x78 });
- private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
- private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
- private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
- private final PackageManager mPackageManager = mContext.getPackageManager();
- private final CtsNetUtils mCtsNetUtils = new CtsNetUtils(mContext);
+ private static final Context sContext =
+ InstrumentationRegistry.getInstrumentation().getContext();
+ private static final EthernetManager sEm = sContext.getSystemService(EthernetManager.class);
+ private static final TetheringManager sTm = sContext.getSystemService(TetheringManager.class);
+ private static final PackageManager sPackageManager = sContext.getPackageManager();
+ private static final CtsNetUtils sCtsNetUtils = new CtsNetUtils(sContext);
// Late initialization in setUp()
private boolean mRunTests;
@@ -161,7 +161,7 @@
private MyTetheringEventCallback mTetheringEventCallback;
public Context getContext() {
- return mContext;
+ return sContext;
}
@BeforeClass
@@ -170,19 +170,24 @@
// Tethering would cache the last upstreams so that the next enabled tethering avoids
// picking up the address that is in conflict with the upstreams. To protect subsequent
// tests, turn tethering on and off before running them.
- final Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
- final CtsTetheringUtils utils = new CtsTetheringUtils(ctx);
- final TestTetheringEventCallback callback = utils.registerTetheringEventCallback();
+ MyTetheringEventCallback callback = null;
+ TestNetworkInterface testIface = null;
try {
- if (!callback.isWifiTetheringSupported(ctx)) return;
+ // If the physical ethernet interface is available, do nothing.
+ if (isInterfaceForTetheringAvailable()) return;
- callback.expectNoTetheringActive();
+ testIface = createTestInterface();
+ setIncludeTestInterfaces(true);
- utils.startWifiTethering(callback);
- callback.getCurrentValidUpstream();
- utils.stopWifiTethering(callback);
+ callback = enableEthernetTethering(testIface.getInterfaceName(), null);
+ callback.awaitUpstreamChanged(true /* throwTimeoutException */);
+ } catch (TimeoutException e) {
+ Log.d(TAG, "WARNNING " + e);
} finally {
- utils.unregisterTetheringEventCallback(callback);
+ maybeCloseTestInterface(testIface);
+ maybeUnregisterTetheringEventCallback(callback);
+
+ setIncludeTestInterfaces(false);
}
}
@@ -195,13 +200,13 @@
mRunTests = isEthernetTetheringSupported();
assumeTrue(mRunTests);
- mTetheredInterfaceRequester = new TetheredInterfaceRequester(mHandler, mEm);
+ mTetheredInterfaceRequester = new TetheredInterfaceRequester();
}
private boolean isEthernetTetheringSupported() throws Exception {
- if (mEm == null) return false;
+ if (sEm == null) return false;
- return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> mTm.isTetheringSupported());
+ return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> sTm.isTetheringSupported());
}
protected void maybeStopTapPacketReader(final TapPacketReader tapPacketReader)
@@ -212,7 +217,7 @@
}
}
- protected void maybeCloseTestInterface(final TestNetworkInterface testInterface)
+ protected static void maybeCloseTestInterface(final TestNetworkInterface testInterface)
throws Exception {
if (testInterface != null) {
testInterface.getFileDescriptor().close();
@@ -220,8 +225,8 @@
}
}
- protected void maybeUnregisterTetheringEventCallback(final MyTetheringEventCallback callback)
- throws Exception {
+ protected static void maybeUnregisterTetheringEventCallback(
+ final MyTetheringEventCallback callback) throws Exception {
if (callback != null) {
callback.awaitInterfaceUntethered();
callback.unregister();
@@ -230,7 +235,7 @@
protected void stopEthernetTethering(final MyTetheringEventCallback callback) {
runAsShell(TETHER_PRIVILEGED, () -> {
- mTm.stopTethering(TETHERING_ETHERNET);
+ sTm.stopTethering(TETHERING_ETHERNET);
maybeUnregisterTetheringEventCallback(callback);
});
}
@@ -277,18 +282,18 @@
}
}
- protected boolean isInterfaceForTetheringAvailable() throws Exception {
+ protected static boolean isInterfaceForTetheringAvailable() throws Exception {
// Before T, all ethernet interfaces could be used for server mode. Instead of
// waiting timeout, just checking whether the system currently has any
// ethernet interface is more reliable.
if (!SdkLevel.isAtLeastT()) {
- return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> mEm.isAvailable());
+ return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> sEm.isAvailable());
}
// If previous test case doesn't release tethering interface successfully, the other tests
// after that test may be skipped as unexcepted.
// TODO: figure out a better way to check default tethering interface existenion.
- final TetheredInterfaceRequester requester = new TetheredInterfaceRequester(mHandler, mEm);
+ final TetheredInterfaceRequester requester = new TetheredInterfaceRequester();
try {
// Use short timeout (200ms) for requesting an existing interface, if any, because
// it should reurn faster than requesting a new tethering interface. Using default
@@ -306,15 +311,15 @@
}
}
- protected void setIncludeTestInterfaces(boolean include) {
+ protected static void setIncludeTestInterfaces(boolean include) {
runAsShell(NETWORK_SETTINGS, () -> {
- mEm.setIncludeTestInterfaces(include);
+ sEm.setIncludeTestInterfaces(include);
});
}
- protected void setPreferTestNetworks(boolean prefer) {
+ protected static void setPreferTestNetworks(boolean prefer) {
runAsShell(NETWORK_SETTINGS, () -> {
- mTm.setPreferTestNetworks(prefer);
+ sTm.setPreferTestNetworks(prefer);
});
}
@@ -344,7 +349,6 @@
protected static final class MyTetheringEventCallback implements TetheringEventCallback {
- private final TetheringManager mTm;
private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1);
private final CountDownLatch mTetheringStoppedLatch = new CountDownLatch(1);
private final CountDownLatch mLocalOnlyStartedLatch = new CountDownLatch(1);
@@ -355,7 +359,7 @@
private final TetheringInterface mIface;
private final Network mExpectedUpstream;
- private boolean mAcceptAnyUpstream = false;
+ private final boolean mAcceptAnyUpstream;
private volatile boolean mInterfaceWasTethered = false;
private volatile boolean mInterfaceWasLocalOnly = false;
@@ -368,19 +372,21 @@
// seconds. See b/289881008.
private static final int EXPANDED_TIMEOUT_MS = 30000;
- MyTetheringEventCallback(TetheringManager tm, String iface) {
- this(tm, iface, null);
+ MyTetheringEventCallback(String iface) {
+ mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+ mExpectedUpstream = null;
mAcceptAnyUpstream = true;
}
- MyTetheringEventCallback(TetheringManager tm, String iface, Network expectedUpstream) {
- mTm = tm;
+ MyTetheringEventCallback(String iface, @NonNull Network expectedUpstream) {
+ Objects.requireNonNull(expectedUpstream);
mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
mExpectedUpstream = expectedUpstream;
+ mAcceptAnyUpstream = false;
}
public void unregister() {
- mTm.unregisterTetheringEventCallback(this);
+ sTm.unregisterTetheringEventCallback(this);
mUnregistered = true;
}
@Override
@@ -504,6 +510,11 @@
Log.d(TAG, "Got upstream changed: " + network);
mUpstream = network;
+ // The callback always updates the current tethering status when it's first registered.
+ // If the caller registers the callback before tethering starts, the null upstream
+ // would be updated. Filtering out the null case because it's not a valid upstream that
+ // we care about.
+ if (mUpstream == null) return;
if (mAcceptAnyUpstream || Objects.equals(mUpstream, mExpectedUpstream)) {
mUpstreamLatch.countDown();
}
@@ -525,18 +536,18 @@
}
}
- protected MyTetheringEventCallback enableEthernetTethering(String iface,
+ protected static MyTetheringEventCallback enableEthernetTethering(String iface,
TetheringRequest request, Network expectedUpstream) throws Exception {
// Enable ethernet tethering with null expectedUpstream means the test accept any upstream
// after etherent tethering started.
final MyTetheringEventCallback callback;
if (expectedUpstream != null) {
- callback = new MyTetheringEventCallback(mTm, iface, expectedUpstream);
+ callback = new MyTetheringEventCallback(iface, expectedUpstream);
} else {
- callback = new MyTetheringEventCallback(mTm, iface);
+ callback = new MyTetheringEventCallback(iface);
}
runAsShell(NETWORK_SETTINGS, () -> {
- mTm.registerTetheringEventCallback(mHandler::post, callback);
+ sTm.registerTetheringEventCallback(c -> c.run() /* executor */, callback);
// Need to hold the shell permission until callback is registered. This helps to avoid
// the test become flaky.
callback.awaitCallbackRegistered();
@@ -556,7 +567,7 @@
};
Log.d(TAG, "Starting Ethernet tethering");
runAsShell(TETHER_PRIVILEGED, () -> {
- mTm.startTethering(request, mHandler::post /* executor */, startTetheringCallback);
+ sTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
// Binder call is an async call. Need to hold the shell permission until tethering
// started. This helps to avoid the test become flaky.
if (!tetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
@@ -579,7 +590,7 @@
return callback;
}
- protected MyTetheringEventCallback enableEthernetTethering(String iface,
+ protected static MyTetheringEventCallback enableEthernetTethering(String iface,
Network expectedUpstream) throws Exception {
return enableEthernetTethering(iface,
new TetheringRequest.Builder(TETHERING_ETHERNET)
@@ -605,17 +616,9 @@
}
protected static final class TetheredInterfaceRequester implements TetheredInterfaceCallback {
- private final Handler mHandler;
- private final EthernetManager mEm;
-
private TetheredInterfaceRequest mRequest;
private final CompletableFuture<String> mFuture = new CompletableFuture<>();
- TetheredInterfaceRequester(Handler handler, EthernetManager em) {
- mHandler = handler;
- mEm = em;
- }
-
@Override
public void onAvailable(String iface) {
Log.d(TAG, "Ethernet interface available: " + iface);
@@ -631,7 +634,7 @@
assertNull("BUG: more than one tethered interface request", mRequest);
Log.d(TAG, "Requesting tethered interface");
mRequest = runAsShell(NETWORK_SETTINGS, () ->
- mEm.requestTetheredInterface(mHandler::post, this));
+ sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
return mFuture;
}
@@ -652,9 +655,9 @@
}
}
- protected TestNetworkInterface createTestInterface() throws Exception {
+ protected static TestNetworkInterface createTestInterface() throws Exception {
TestNetworkManager tnm = runAsShell(MANAGE_TEST_NETWORKS, () ->
- mContext.getSystemService(TestNetworkManager.class));
+ sContext.getSystemService(TestNetworkManager.class));
TestNetworkInterface iface = runAsShell(MANAGE_TEST_NETWORKS, () ->
tnm.createTapInterface());
Log.d(TAG, "Created test interface " + iface.getInterfaceName());
@@ -669,7 +672,7 @@
lp.setLinkAddresses(addresses);
lp.setDnsServers(dnses);
- return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(mContext, lp, TIMEOUT_MS));
+ return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, TIMEOUT_MS));
}
protected void sendDownloadPacketUdp(@NonNull final InetAddress srcIp,
@@ -851,7 +854,7 @@
private void maybeRetryTestedUpstreamChanged(final Network expectedUpstream,
final TimeoutException fallbackException) throws Exception {
// Fall back original exception because no way to reselect if there is no WIFI feature.
- assertTrue(fallbackException.toString(), mPackageManager.hasSystemFeature(FEATURE_WIFI));
+ assertTrue(fallbackException.toString(), sPackageManager.hasSystemFeature(FEATURE_WIFI));
// Try to toggle wifi network, if any, to reselect upstream network via default network
// switching. Because test network has higher priority than internet network, this can
@@ -867,7 +870,7 @@
// See Tethering#chooseUpstreamType, CtsNetUtils#toggleWifi.
// TODO: toggle cellular network if the device has no WIFI feature.
Log.d(TAG, "Toggle WIFI to retry upstream selection");
- mCtsNetUtils.toggleWifi();
+ sCtsNetUtils.toggleWifi();
// Wait for expected upstream.
final CompletableFuture<Network> future = new CompletableFuture<>();
@@ -881,14 +884,14 @@
}
};
try {
- mTm.registerTetheringEventCallback(mHandler::post, callback);
+ sTm.registerTetheringEventCallback(mHandler::post, callback);
assertEquals("onUpstreamChanged for unexpected network", expectedUpstream,
future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (TimeoutException e) {
throw new AssertionError("Did not receive upstream " + expectedUpstream
+ " callback after " + TIMEOUT_MS + "ms");
} finally {
- mTm.unregisterTetheringEventCallback(callback);
+ sTm.unregisterTetheringEventCallback(callback);
}
}
@@ -925,7 +928,7 @@
mDownstreamReader = makePacketReader(mDownstreamIface);
mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
- final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+ final ConnectivityManager cm = sContext.getSystemService(ConnectivityManager.class);
// Currently tethering don't have API to tell when ipv6 tethering is available. Thus, make
// sure tethering already have ipv6 connectivity before testing.
if (cm.getLinkProperties(mUpstreamTracker.getNetwork()).hasGlobalIpv6Address()) {
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/common/Android.bp b/common/Android.bp
index 1d73a46..f2f3929 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -19,12 +19,6 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-build = ["TrunkStable.bp"]
-
-// This is a placeholder comment to avoid merge conflicts
-// as the above target may not exist
-// depending on the branch
-
// The library requires the final artifact to contain net-utils-device-common-struct.
java_library {
name: "connectivity-net-module-utils-bpf",
diff --git a/common/TrunkStable.bp b/common/TrunkStable.bp
deleted file mode 100644
index 59874c2..0000000
--- a/common/TrunkStable.bp
+++ /dev/null
@@ -1,16 +0,0 @@
-//
-// 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.
-//
-
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index deda74e..c31dcf5 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -57,6 +57,10 @@
"app-compat-annotations",
"androidx.annotation_annotation",
],
+ static_libs: [
+ // Cannot go to framework-connectivity because mid_sdk checks require 31.
+ "modules-utils-binary-xml",
+ ],
impl_only_libs: [
// The build system will use framework-bluetooth module_current stubs, because
// of sdk_version: "module_current" above.
@@ -182,6 +186,7 @@
"//packages/modules/Connectivity/staticlibs/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
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/ConnectivityFrameworkInitializerTiramisu.java b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
index d89964d..d7cff2c 100644
--- a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
+++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
@@ -27,6 +27,8 @@
import android.net.thread.IThreadNetworkManager;
import android.net.thread.ThreadNetworkManager;
+import com.android.modules.utils.build.SdkLevel;
+
/**
* Class for performing registration for Connectivity services which are exposed via updatable APIs
* since Android T.
@@ -83,14 +85,17 @@
}
);
- SystemServiceRegistry.registerStaticService(
- MDnsManager.MDNS_SERVICE,
- MDnsManager.class,
- (serviceBinder) -> {
- IMDns service = IMDns.Stub.asInterface(serviceBinder);
- return new MDnsManager(service);
- }
- );
+ // mdns service is removed from Netd from Android V.
+ if (!SdkLevel.isAtLeastV()) {
+ SystemServiceRegistry.registerStaticService(
+ MDnsManager.MDNS_SERVICE,
+ MDnsManager.class,
+ (serviceBinder) -> {
+ IMDns service = IMDns.Stub.asInterface(serviceBinder);
+ return new MDnsManager(service);
+ }
+ );
+ }
SystemServiceRegistry.registerContextAwareService(
ThreadNetworkManager.SERVICE_NAME,
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/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
index b6f6dbb..934b4c6 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -60,6 +60,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FileRotator;
+import com.android.modules.utils.FastDataInput;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.NetworkStatsUtils;
@@ -116,15 +117,28 @@
private long mEndMillis;
private long mTotalBytes;
private boolean mDirty;
+ private final boolean mUseFastDataInput;
/**
* Construct a {@link NetworkStatsCollection} object.
*
- * @param bucketDuration duration of the buckets in this object, in milliseconds.
+ * @param bucketDurationMillis duration of the buckets in this object, in milliseconds.
* @hide
*/
public NetworkStatsCollection(long bucketDurationMillis) {
+ this(bucketDurationMillis, false /* useFastDataInput */);
+ }
+
+ /**
+ * Construct a {@link NetworkStatsCollection} object.
+ *
+ * @param bucketDurationMillis duration of the buckets in this object, in milliseconds.
+ * @param useFastDataInput true if using {@link FastDataInput} is preferred. Otherwise, false.
+ * @hide
+ */
+ public NetworkStatsCollection(long bucketDurationMillis, boolean useFastDataInput) {
mBucketDurationMillis = bucketDurationMillis;
+ mUseFastDataInput = useFastDataInput;
reset();
}
@@ -483,7 +497,11 @@
/** @hide */
@Override
public void read(InputStream in) throws IOException {
- read((DataInput) new DataInputStream(in));
+ if (mUseFastDataInput) {
+ read(FastDataInput.obtain(in));
+ } else {
+ read((DataInput) new DataInputStream(in));
+ }
}
private void read(DataInput in) throws IOException {
@@ -967,8 +985,8 @@
* @hide
*/
@Nullable
- public static String compareStats(
- NetworkStatsCollection migrated, NetworkStatsCollection legacy) {
+ public static String compareStats(NetworkStatsCollection migrated,
+ NetworkStatsCollection legacy, boolean allowKeyChange) {
final Map<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
migrated.getEntries();
final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
@@ -980,7 +998,7 @@
final NetworkStatsHistory legHistory = legEntries.get(legKey);
final NetworkStatsHistory migHistory = migEntries.get(legKey);
- if (migHistory == null && couldKeyChangeOnImport(legKey)) {
+ if (migHistory == null && allowKeyChange && couldKeyChangeOnImport(legKey)) {
unmatchedLegKeys.remove(legKey);
continue;
}
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
new file mode 100644
index 0000000..b1ef98f
--- /dev/null
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.nsd;
+
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Encapsulates parameters for {@link NsdManager#registerService}.
+ * @hide
+ */
+//@FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+public final class AdvertisingRequest implements Parcelable {
+
+ /**
+ * Only update the registration without sending exit and re-announcement.
+ */
+ public static final int NSD_ADVERTISING_UPDATE_ONLY = 1;
+
+
+ @NonNull
+ public static final Creator<AdvertisingRequest> CREATOR =
+ new Creator<>() {
+ @Override
+ public AdvertisingRequest createFromParcel(Parcel in) {
+ final NsdServiceInfo serviceInfo = in.readParcelable(
+ NsdServiceInfo.class.getClassLoader(), NsdServiceInfo.class);
+ final int protocolType = in.readInt();
+ final long advertiseConfig = in.readLong();
+ return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig);
+ }
+
+ @Override
+ public AdvertisingRequest[] newArray(int size) {
+ return new AdvertisingRequest[size];
+ }
+ };
+ @NonNull
+ private final NsdServiceInfo mServiceInfo;
+ private final int mProtocolType;
+ // Bitmask of @AdvertisingConfig flags. Uses a long to allow 64 possible flags in the future.
+ private final long mAdvertisingConfig;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef(flag = true, prefix = {"NSD_ADVERTISING"}, value = {
+ NSD_ADVERTISING_UPDATE_ONLY,
+ })
+ @interface AdvertisingConfig {}
+
+ /**
+ * The constructor for the advertiseRequest
+ */
+ private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
+ long advertisingConfig) {
+ mServiceInfo = serviceInfo;
+ mProtocolType = protocolType;
+ mAdvertisingConfig = advertisingConfig;
+ }
+
+ /**
+ * Returns the {@link NsdServiceInfo}
+ */
+ @NonNull
+ public NsdServiceInfo getServiceInfo() {
+ return mServiceInfo;
+ }
+
+ /**
+ * Returns the service advertise protocol
+ */
+ public int getProtocolType() {
+ return mProtocolType;
+ }
+
+ /**
+ * Returns the advertising config.
+ */
+ public long getAdvertisingConfig() {
+ return mAdvertisingConfig;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("serviceInfo: ").append(mServiceInfo)
+ .append(", protocolType: ").append(mProtocolType)
+ .append(", advertisingConfig: ").append(mAdvertisingConfig);
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof AdvertisingRequest)) {
+ return false;
+ } else {
+ final AdvertisingRequest otherRequest = (AdvertisingRequest) other;
+ return mServiceInfo.equals(otherRequest.mServiceInfo)
+ && mProtocolType == otherRequest.mProtocolType
+ && mAdvertisingConfig == otherRequest.mAdvertisingConfig;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mServiceInfo, flags);
+ dest.writeInt(mProtocolType);
+ dest.writeLong(mAdvertisingConfig);
+ }
+
+// @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+ /**
+ * The builder for creating new {@link AdvertisingRequest} objects.
+ * @hide
+ */
+ public static final class Builder {
+ @NonNull
+ private final NsdServiceInfo mServiceInfo;
+ private final int mProtocolType;
+ private long mAdvertisingConfig;
+ /**
+ * Creates a new {@link Builder} object.
+ */
+ public Builder(@NonNull NsdServiceInfo serviceInfo, int protocolType) {
+ mServiceInfo = serviceInfo;
+ mProtocolType = protocolType;
+ }
+
+ /**
+ * Sets advertising configuration flags.
+ *
+ * @param advertisingConfigFlags Bitmask of {@code AdvertisingConfig} flags.
+ */
+ @NonNull
+ public Builder setAdvertisingConfig(long advertisingConfigFlags) {
+ mAdvertisingConfig = advertisingConfigFlags;
+ return this;
+ }
+
+
+ /** Creates a new {@link AdvertisingRequest} object. */
+ @NonNull
+ public AdvertisingRequest build() {
+ return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ }
+ }
+}
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
index e671db1..b03eb29 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -16,6 +16,7 @@
package android.net.nsd;
+import android.net.nsd.AdvertisingRequest;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.IOffloadEngine;
import android.net.nsd.NsdServiceInfo;
@@ -27,7 +28,7 @@
* {@hide}
*/
interface INsdServiceConnector {
- void registerService(int listenerKey, in NsdServiceInfo serviceInfo);
+ void registerService(int listenerKey, in AdvertisingRequest advertisingRequest);
void unregisterService(int listenerKey);
void discoverServices(int listenerKey, in NsdServiceInfo serviceInfo);
void stopDiscovery(int listenerKey);
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index bf01a9d..b4f2be9 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -46,10 +46,12 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import java.lang.annotation.Retention;
@@ -57,6 +59,8 @@
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.Executor;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* The Network Service Discovery Manager class provides the API to discover services
@@ -150,9 +154,40 @@
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";
+ static final String ADVERTISE_REQUEST_API =
+ "com.android.net.flags.advertise_request_api";
}
/**
+ * A regex for the acceptable format of a type or subtype label.
+ * @hide
+ */
+ public static final String TYPE_SUBTYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
+ /**
+ * A regex for the acceptable format of a service type specification.
+ *
+ * When it matches, matcher group 1 is an optional leading subtype when using legacy dot syntax
+ * (_subtype._type._tcp). Matcher group 2 is the actual type, and matcher group 3 contains
+ * optional comma-separated subtypes.
+ * @hide
+ */
+ public static final String TYPE_REGEX =
+ // Optional leading subtype (_subtype._type._tcp)
+ // (?: xxx) is a non-capturing parenthesis, don't capture the dot
+ "^(?:(" + TYPE_SUBTYPE_LABEL_REGEX + ")\\.)?"
+ // Actual type (_type._tcp.local)
+ + "(" + 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,_subtype1,_subtype2" format
+ + "((?:," + TYPE_SUBTYPE_LABEL_REGEX + ")*)"
+ + "$";
+
+ /**
* Broadcast intent action to indicate whether network service discovery is
* enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state
* information as int.
@@ -654,9 +689,12 @@
throw new RuntimeException("Failed to connect to NsdService");
}
- // Only proactively start the daemon if the target SDK < S, otherwise the internal service
- // would automatically start/stop the native daemon as needed.
- if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)) {
+ // Only proactively start the daemon if the target SDK < S AND platform < V, For target
+ // SDK >= S AND platform < V, the internal service would automatically start/stop the native
+ // daemon as needed. For platform >= V, no action is required because the native daemon is
+ // completely removed.
+ if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
+ && !SdkLevel.isAtLeastV()) {
try {
mService.startDaemon();
} catch (RemoteException e) {
@@ -1096,6 +1134,16 @@
return key;
}
+ private int updateRegisteredListener(Object listener, Executor e, NsdServiceInfo s) {
+ final int key;
+ synchronized (mMapLock) {
+ key = getListenerKey(listener);
+ mServiceMap.put(key, s);
+ mExecutorMap.put(key, e);
+ }
+ return key;
+ }
+
private void removeListener(int key) {
synchronized (mMapLock) {
mListenerMap.remove(key);
@@ -1160,14 +1208,111 @@
*/
public void registerService(@NonNull NsdServiceInfo serviceInfo, int protocolType,
@NonNull Executor executor, @NonNull RegistrationListener listener) {
+ checkServiceInfo(serviceInfo);
+ checkProtocol(protocolType);
+ final AdvertisingRequest.Builder builder = new AdvertisingRequest.Builder(serviceInfo,
+ protocolType);
+ // Optionally assume that the request is an update request if it uses subtypes and the same
+ // listener. This is not documented behavior as support for advertising subtypes via
+ // "_servicename,_sub1,_sub2" has never been documented in the first place, and using
+ // multiple subtypes was broken in T until a later module update. Subtype registration is
+ // documented in the NsdServiceInfo.setSubtypes API instead, but this provides a limited
+ // option for users of the older undocumented behavior, only for subtype changes.
+ if (isSubtypeUpdateRequest(serviceInfo, listener)) {
+ builder.setAdvertisingConfig(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY);
+ }
+ registerService(builder.build(), executor, listener);
+ }
+
+ private boolean isSubtypeUpdateRequest(@NonNull NsdServiceInfo serviceInfo, @NonNull
+ RegistrationListener listener) {
+ // If the listener is the same object, serviceInfo is for the same service name and
+ // type (outside of subtypes), and either of them use subtypes, treat the request as a
+ // subtype update request.
+ synchronized (mMapLock) {
+ int valueIndex = mListenerMap.indexOfValue(listener);
+ if (valueIndex == -1) {
+ return false;
+ }
+ final int key = mListenerMap.keyAt(valueIndex);
+ NsdServiceInfo existingService = mServiceMap.get(key);
+ if (existingService == null) {
+ return false;
+ }
+ final Pair<String, String> existingTypeSubtype = getTypeAndSubtypes(
+ existingService.getServiceType());
+ final Pair<String, String> newTypeSubtype = getTypeAndSubtypes(
+ serviceInfo.getServiceType());
+ if (existingTypeSubtype == null || newTypeSubtype == null) {
+ return false;
+ }
+ final boolean existingHasNoSubtype = TextUtils.isEmpty(existingTypeSubtype.second);
+ final boolean updatedHasNoSubtype = TextUtils.isEmpty(newTypeSubtype.second);
+ if (existingHasNoSubtype && updatedHasNoSubtype) {
+ // Only allow subtype changes when subtypes are used. This ensures that this
+ // behavior does not affect most requests.
+ return false;
+ }
+
+ return Objects.equals(existingService.getServiceName(), serviceInfo.getServiceName())
+ && Objects.equals(existingTypeSubtype.first, newTypeSubtype.first);
+ }
+ }
+
+ /**
+ * Get the base type from a type specification with "_type._tcp,sub1,sub2" syntax.
+ *
+ * <p>This rejects specifications using dot syntax to specify subtypes ("_sub1._type._tcp").
+ *
+ * @return Type and comma-separated list of subtypes, or null if invalid format.
+ */
+ @Nullable
+ private static Pair<String, String> getTypeAndSubtypes(@NonNull String typeWithSubtype) {
+ final Matcher matcher = Pattern.compile(TYPE_REGEX).matcher(typeWithSubtype);
+ if (!matcher.matches()) return null;
+ // Reject specifications using leading subtypes with a dot
+ if (!TextUtils.isEmpty(matcher.group(1))) return null;
+ return new Pair<>(matcher.group(2), matcher.group(3));
+ }
+
+ /**
+ * Register a service to be discovered by other services.
+ *
+ * <p> The function call immediately returns after sending a request to register service
+ * to the framework. The application is notified of a successful registration
+ * through the callback {@link RegistrationListener#onServiceRegistered} or a failure
+ * through {@link RegistrationListener#onRegistrationFailed}.
+ *
+ * <p> The application should call {@link #unregisterService} when the service
+ * registration is no longer required, and/or whenever the application is stopped.
+ * @param advertisingRequest service being registered
+ * @param executor Executor to run listener callbacks with
+ * @param listener The listener notifies of a successful registration and is used to
+ * unregister this service through a call on {@link #unregisterService}. Cannot be null.
+ *
+ * @hide
+ */
+// @FlaggedApi(Flags.ADVERTISE_REQUEST_API)
+ public void registerService(@NonNull AdvertisingRequest advertisingRequest,
+ @NonNull Executor executor,
+ @NonNull RegistrationListener listener) {
+ final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
+ final int protocolType = advertisingRequest.getProtocolType();
if (serviceInfo.getPort() <= 0) {
throw new IllegalArgumentException("Invalid port number");
}
checkServiceInfo(serviceInfo);
checkProtocol(protocolType);
- int key = putListener(listener, executor, serviceInfo);
+ final int key;
+ // For update only request, the old listener has to be reused
+ if ((advertisingRequest.getAdvertisingConfig()
+ & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0) {
+ key = updateRegisteredListener(listener, executor, serviceInfo);
+ } else {
+ key = putListener(listener, executor, serviceInfo);
+ }
try {
- mService.registerService(key, serviceInfo);
+ mService.registerService(key, advertisingRequest);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
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/Android.bp b/framework/Android.bp
index 1e6262d..f3d8689 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -105,7 +105,9 @@
apex_available: [
"com.android.tethering",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ },
}
java_library {
@@ -134,7 +136,10 @@
"framework-tethering.impl",
"framework-wifi.stubs.module_lib",
],
- visibility: ["//packages/modules/Connectivity:__subpackages__"]
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
java_defaults {
@@ -185,10 +190,14 @@
"//packages/modules/Connectivity/Cronet/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
"//packages/modules/NetworkStack/tests:__subpackages__",
"//packages/modules/Wifi/service/tests/wifitests",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
platform_compat_config {
@@ -248,6 +257,9 @@
apex_available: [
"com.android.tethering",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
java_genrule {
@@ -293,9 +305,9 @@
],
flags: [
"--show-for-stub-purposes-annotation android.annotation.SystemApi" +
- "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
+ "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
"--show-for-stub-purposes-annotation android.annotation.SystemApi" +
- "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
+ "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
],
aidl: {
include_dirs: [
@@ -308,6 +320,9 @@
java_library {
name: "framework-connectivity-module-api-stubs-including-flagged",
srcs: [":framework-connectivity-module-api-stubs-including-flagged-droidstubs"],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
// Library providing limited APIs within the connectivity module, so that R+ components like
@@ -332,4 +347,7 @@
visibility: [
"//packages/modules/Connectivity/Tethering:__subpackages__",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
diff --git a/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
new file mode 100644
index 0000000..2848074
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.nsd;
+
+@JavaOnlyStableParcelable parcelable AdvertisingRequest;
\ No newline at end of file
diff --git a/framework/src/android/net/BpfNetMapsReader.java b/framework/src/android/net/BpfNetMapsReader.java
index 4ab6d3e..ee422ab 100644
--- a/framework/src/android/net/BpfNetMapsReader.java
+++ b/framework/src/android/net/BpfNetMapsReader.java
@@ -36,7 +36,6 @@
import android.os.ServiceSpecificException;
import android.system.ErrnoException;
import android.system.Os;
-import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
@@ -278,13 +277,6 @@
public boolean getDataSaverEnabled() {
throwIfPreT("getDataSaverEnabled is not available on pre-T devices");
- // Note that this is not expected to be called until V given that it relies on the
- // counterpart platform solution to set data saver status to bpf.
- // See {@code NetworkManagementService#setDataSaverModeEnabled}.
- if (!SdkLevel.isAtLeastV()) {
- Log.wtf(TAG, "getDataSaverEnabled is not expected to be called on pre-V devices");
- }
-
try {
return mDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val == DATA_SAVER_ENABLED;
} catch (ErrnoException e) {
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 2731b3e..8dac61c 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -47,8 +47,18 @@
## Build and Install
```sh
-Build unbundled module using banchan
+For master on AOSP (Android) host
+$ source build/envsetup.sh
+$ lunch aosp_oriole-trunk_staging-userdebug
+$ m com.android.tethering
+$ $ANDROID_BUILD_TOP/out/host/linux-x86/bin/deapexer decompress --input $ANDROID_PRODUCT_OUT/system/apex/com.android.tethering.capex --output /tmp/tethering.apex
+$ adb install /tmp/tethering.apex
+$ adb reboot
+NOTE: Developers should use AOSP by default, udc-mainline-prod should not be used unless for Google internal features.
+For udc-mainline-prod on Google internal host
+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
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/Android.bp b/nearby/service/Android.bp
index 4630902..17b80b0 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -30,7 +30,7 @@
srcs: [":nearby-service-srcs"],
defaults: [
- "framework-system-server-module-defaults"
+ "framework-system-server-module-defaults",
],
libs: [
"androidx.annotation_annotation",
@@ -66,13 +66,16 @@
apex_available: [
"com.android.tethering",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
genrule {
name: "statslog-nearby-java-gen",
tools: ["stats-log-api-gen"],
cmd: "$(location stats-log-api-gen) --java $(out) --module nearby " +
- " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
- " --minApiLevel 33",
+ " --javaPackage com.android.server.nearby.proto --javaClass NearbyStatsLog" +
+ " --minApiLevel 33",
out: ["com/android/server/nearby/proto/NearbyStatsLog.java"],
}
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/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
index 1f9f70b..ac1e18f 100644
--- a/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
+++ b/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
@@ -17,11 +17,13 @@
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;
@@ -40,7 +42,7 @@
// 1st byte : encryption scheme
// 2nd to 17th bytes: salt
- private static final int ENCRYPTION_INFO_LENGTH = 17;
+ 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
@@ -69,4 +71,20 @@
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 72edad0..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.");
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 bb5461c..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.
@@ -65,4 +69,54 @@
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/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 66a1ffe..4309d7e 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -39,6 +39,7 @@
"cts",
"general-tests",
"mts-tethering",
+ "mcts-tethering",
],
certificate: "platform",
sdk_version: "module_current",
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
index a1eb57b..6ec7c57 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
@@ -20,6 +20,7 @@
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;
@@ -50,7 +51,7 @@
public void test_getMethods_signature() {
byte[] data = ArrayUtils.append((byte) 0b10001000, SALT);
EncryptionInfo info = new EncryptionInfo(data);
- assertThat(info.getEncodingScheme()).isEqualTo(EncryptionInfo.EncodingScheme.SIGNATURE);
+ assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.SIGNATURE);
assertThat(info.getSalt()).isEqualTo(SALT);
}
@@ -58,7 +59,14 @@
public void test_getMethods_mic() {
byte[] data = ArrayUtils.append((byte) 0b10000000, SALT);
EncryptionInfo info = new EncryptionInfo(data);
- assertThat(info.getEncodingScheme()).isEqualTo(EncryptionInfo.EncodingScheme.MIC);
+ 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/netd/Android.bp b/netd/Android.bp
index 4325d89..3cdbc97 100644
--- a/netd/Android.bp
+++ b/netd/Android.bp
@@ -69,10 +69,10 @@
"BpfBaseTest.cpp"
],
static_libs: [
+ "libbase",
"libnetd_updatable",
],
shared_libs: [
- "libbase",
"libcutils",
"liblog",
"libnetdutils",
diff --git a/netd/NetdUpdatable.cpp b/netd/NetdUpdatable.cpp
index 8b9e5a7..3b15916 100644
--- a/netd/NetdUpdatable.cpp
+++ b/netd/NetdUpdatable.cpp
@@ -31,8 +31,8 @@
android::netdutils::Status ret = sBpfHandler.init(cg2_path);
if (!android::netdutils::isOk(ret)) {
- LOG(ERROR) << __func__ << ": Failed. " << ret.code() << " " << ret.msg();
- return -ret.code();
+ LOG(ERROR) << __func__ << ": Failed: (" << ret.code() << ") " << ret.msg();
+ abort();
}
return 0;
}
diff --git a/service-t/Android.bp b/service-t/Android.bp
index bc49f0e..de879f3 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -31,6 +31,7 @@
],
visibility: ["//visibility:private"],
}
+
// The above filegroup can be used to specify different sources depending
// on the branch, while minimizing merge conflicts in the rest of the
// build rules.
@@ -78,6 +79,9 @@
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/IPsec/tests/iketests",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
// Test building mDNS as a standalone, so that it can be imported into other repositories as-is.
@@ -94,11 +98,12 @@
min_sdk_version: "21",
lint: {
error_checks: ["NewApi"],
+ baseline_filename: "lint-baseline.xml",
},
srcs: [
"src/com/android/server/connectivity/mdns/**/*.java",
":framework-connectivity-t-mdns-standalone-build-sources",
- ":service-mdns-droidstubs"
+ ":service-mdns-droidstubs",
],
exclude_srcs: [
"src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java",
@@ -127,7 +132,7 @@
srcs: ["src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java"],
libs: [
"net-utils-device-common-mdns-standalone-build-test",
- "service-connectivity-tiramisu-pre-jarjar"
+ "service-connectivity-tiramisu-pre-jarjar",
],
visibility: [
"//visibility:private",
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index 0dfd0af..b9f3adb 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -73,6 +73,7 @@
"-Wthread-safety",
],
static_libs: [
+ "libbase",
"libgmock",
"libnetworkstats",
"libperfetto_client_experimental",
@@ -80,7 +81,6 @@
"perfetto_trace_protos",
],
shared_libs: [
- "libbase",
"liblog",
"libcutils",
"libandroid_net",
diff --git a/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java b/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
new file mode 100644
index 0000000..3ed21a2
--- /dev/null
+++ b/service-t/src/com/android/metrics/NetworkStatsMetricsLogger.java
@@ -0,0 +1,171 @@
+/*
+ * 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.metrics;
+
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_DISABLED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_ENABLED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__OPERATION_TYPE__ROT_READ;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UNKNOWN;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ConnectivityStatsLog;
+
+import java.io.File;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class to log NetworkStats related metrics.
+ *
+ * This class does not provide thread-safe.
+ */
+public class NetworkStatsMetricsLogger {
+ final Dependencies mDeps;
+ int mReadIndex = 1;
+
+ /** Dependency class */
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * Writes a NETWORK_STATS_RECORDER_FILE_OPERATION_REPORTED event to ConnectivityStatsLog.
+ */
+ public void writeRecorderFileReadingStats(int recorderType, int readIndex,
+ int readLatencyMillis,
+ int fileCount, int totalFileSize,
+ int keys, int uids, int totalHistorySize,
+ boolean useFastDataInput) {
+ ConnectivityStatsLog.write(NETWORK_STATS_RECORDER_FILE_OPERATED,
+ NETWORK_STATS_RECORDER_FILE_OPERATED__OPERATION_TYPE__ROT_READ,
+ recorderType,
+ readIndex,
+ readLatencyMillis,
+ fileCount,
+ totalFileSize,
+ keys,
+ uids,
+ totalHistorySize,
+ useFastDataInput
+ ? NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_ENABLED
+ : NETWORK_STATS_RECORDER_FILE_OPERATED__FAST_DATA_INPUT_STATE__FDIS_DISABLED);
+ }
+ }
+
+ public NetworkStatsMetricsLogger() {
+ mDeps = new Dependencies();
+ }
+
+ @VisibleForTesting
+ public NetworkStatsMetricsLogger(Dependencies deps) {
+ mDeps = deps;
+ }
+
+ private static int prefixToRecorderType(@NonNull String prefix) {
+ switch (prefix) {
+ case PREFIX_XT:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
+ case PREFIX_UID:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+ case PREFIX_UID_TAG:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+ default:
+ return NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UNKNOWN;
+ }
+ }
+
+ /**
+ * Get file count and total byte count for the given directory and prefix.
+ *
+ * @return File count and total byte count as a pair, or 0s if met errors.
+ */
+ private static Pair<Integer, Integer> getStatsFilesAttributes(
+ @Nullable File statsDir, @NonNull String prefix) {
+ if (statsDir == null) return new Pair<>(0, 0);
+
+ // Only counts the matching files.
+ // The files are named in the following format:
+ // <prefix>.<startTimestamp>-[<endTimestamp>]
+ // e.g. uid_tag.12345-
+ // See FileRotator#FileInfo for more detail.
+ final Pattern pattern = Pattern.compile("^" + prefix + "\\.[0-9]+-[0-9]*$");
+
+ // Ensure that base path exists.
+ statsDir.mkdirs();
+
+ int totalFiles = 0;
+ int totalBytes = 0;
+ for (String name : emptyIfNull(statsDir.list())) {
+ if (!pattern.matcher(name).matches()) continue;
+
+ totalFiles++;
+ // Cast to int is safe since stats persistent files are several MBs in total.
+ totalBytes += (int) (new File(statsDir, name).length());
+
+ }
+ return new Pair<>(totalFiles, totalBytes);
+ }
+
+ private static String [] emptyIfNull(@Nullable String [] array) {
+ return (array == null) ? new String[0] : array;
+ }
+
+ /**
+ * Log statistics from the NetworkStatsRecorder file reading process into statsd.
+ */
+ public void logRecorderFileReading(@NonNull String prefix, int readLatencyMillis,
+ @Nullable File statsDir, @NonNull NetworkStatsCollection collection,
+ boolean useFastDataInput) {
+ final Set<Integer> uids = new HashSet<>();
+ final Map<NetworkStatsCollection.Key, NetworkStatsHistory> entries =
+ collection.getEntries();
+
+ for (final NetworkStatsCollection.Key key : entries.keySet()) {
+ uids.add(key.uid);
+ }
+
+ int totalHistorySize = 0;
+ for (final NetworkStatsHistory history : entries.values()) {
+ totalHistorySize += history.size();
+ }
+
+ final Pair<Integer, Integer> fileAttributes = getStatsFilesAttributes(statsDir, prefix);
+ mDeps.writeRecorderFileReadingStats(prefixToRecorderType(prefix),
+ mReadIndex++,
+ readLatencyMillis,
+ fileAttributes.first /* fileCount */,
+ fileAttributes.second /* totalFileSize */,
+ entries.size(),
+ uids.size(),
+ totalHistorySize,
+ useFastDataInput);
+ }
+}
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/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 2640332..76481c8 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -26,6 +26,8 @@
import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT;
import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
+import static android.net.nsd.NsdManager.TYPE_REGEX;
+import static android.net.nsd.NsdManager.TYPE_SUBTYPE_LABEL_REGEX;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -51,6 +53,7 @@
import android.net.mdns.aidl.IMDnsEventListener;
import android.net.mdns.aidl.RegistrationInfo;
import android.net.mdns.aidl.ResolutionInfo;
+import android.net.nsd.AdvertisingRequest;
import android.net.nsd.INsdManager;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
@@ -111,7 +114,10 @@
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -170,6 +176,8 @@
"mdns_advertiser_allowlist_";
private static final String MDNS_ALLOWLIST_FLAG_SUFFIX = "_version";
+
+
@VisibleForTesting
static final String MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF =
"mdns_config_running_app_active_importance_cutoff";
@@ -186,11 +194,13 @@
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;
private final NsdStateMachine mNsdStateMachine;
- private final MDnsManager mMDnsManager;
+ // It can be null on V+ device since mdns native service provided by netd is removed.
+ private final @Nullable MDnsManager mMDnsManager;
private final MDnsEventCallback mMDnsEventCallback;
@NonNull
private final Dependencies mDeps;
@@ -532,6 +542,11 @@
}
private void maybeStartDaemon() {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "maybeStartDaemon: mMDnsManager is null");
+ return;
+ }
+
if (mIsDaemonStarted) {
if (DBG) Log.d(TAG, "Daemon is already started.");
return;
@@ -544,6 +559,11 @@
}
private void maybeStopDaemon() {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "maybeStopDaemon: mMDnsManager is null");
+ return;
+ }
+
if (!mIsDaemonStarted) {
if (DBG) Log.d(TAG, "Daemon has not been started.");
return;
@@ -688,17 +708,45 @@
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;
final int transactionId;
final int clientRequestId = msg.arg2;
- final ListenerArgs args;
final OffloadEngineInfo offloadEngineInfo;
switch (msg.what) {
case NsdManager.DISCOVER_SERVICES: {
if (DBG) Log.d(TAG, "Discover services");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -716,14 +764,14 @@
final NsdServiceInfo info = args.serviceInfo;
transactionId = getUniqueId();
- final Pair<String, String> typeAndSubtype =
+ final Pair<String, List<String>> typeAndSubtype =
parseTypeAndSubtype(info.getServiceType());
final String serviceType = typeAndSubtype == null
? null : typeAndSubtype.first;
if (clientInfo.mUseJavaBackend
|| mDeps.isMdnsDiscoveryManagerEnabled(mContext)
|| useDiscoveryManagerForType(serviceType)) {
- if (serviceType == null) {
+ if (serviceType == null || typeAndSubtype.second.size() > 1) {
clientInfo.onDiscoverServicesFailedImmediately(clientRequestId,
NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
break;
@@ -738,10 +786,11 @@
.setNetwork(info.getNetwork())
.setRemoveExpiredService(true)
.setIsPassiveMode(true);
- if (typeAndSubtype.second != null) {
+ if (!typeAndSubtype.second.isEmpty()) {
// The parsing ensures subtype starts with an underscore.
// MdnsSearchOptions expects the underscore to not be present.
- optionsBuilder.addSubtype(typeAndSubtype.second.substring(1));
+ optionsBuilder.addSubtype(
+ typeAndSubtype.second.get(0).substring(1));
}
mMdnsDiscoveryManager.registerListener(
listenServiceType, listener, optionsBuilder.build());
@@ -773,7 +822,7 @@
}
case NsdManager.STOP_DISCOVERY: {
if (DBG) Log.d(TAG, "Stop service discovery");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -811,7 +860,7 @@
}
case NsdManager.REGISTER_SERVICE: {
if (DBG) Log.d(TAG, "Register service");
- args = (ListenerArgs) msg.obj;
+ final AdvertisingArgs args = (AdvertisingArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -826,11 +875,15 @@
NsdManager.FAILURE_MAX_LIMIT, true /* isLegacy */);
break;
}
-
- transactionId = getUniqueId();
- final NsdServiceInfo serviceInfo = args.serviceInfo;
+ final AdvertisingRequest advertisingRequest = args.advertisingRequest;
+ if (advertisingRequest == null) {
+ Log.e(TAG, "Unknown advertisingRequest in registration");
+ break;
+ }
+ final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
final String serviceType = serviceInfo.getServiceType();
- final Pair<String, String> typeSubtype = parseTypeAndSubtype(serviceType);
+ final Pair<String, List<String>> typeSubtype = parseTypeAndSubtype(
+ serviceType);
final String registerServiceType = typeSubtype == null
? null : typeSubtype.first;
if (clientInfo.mUseJavaBackend
@@ -842,22 +895,53 @@
NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
break;
}
+ boolean isUpdateOnly = (advertisingRequest.getAdvertisingConfig()
+ & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0;
+ // If it is an update request, then reuse the old transactionId
+ if (isUpdateOnly) {
+ final ClientRequest existingClientRequest =
+ clientInfo.mClientRequests.get(clientRequestId);
+ if (existingClientRequest == null) {
+ Log.e(TAG, "Invalid update on requestId: " + clientRequestId);
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_INTERNAL_ERROR,
+ false /* isLegacy */);
+ break;
+ }
+ transactionId = existingClientRequest.mTransactionId;
+ } else {
+ transactionId = getUniqueId();
+ }
serviceInfo.setServiceType(registerServiceType);
serviceInfo.setServiceName(truncateServiceName(
serviceInfo.getServiceName()));
+ Set<String> subtypes = new ArraySet<>(serviceInfo.getSubtypes());
+ for (String subType: typeSubtype.second) {
+ if (!TextUtils.isEmpty(subType)) {
+ subtypes.add(subType);
+ }
+ }
+ 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).
+ final MdnsAdvertisingOptions mdnsAdvertisingOptions =
+ MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(
+ isUpdateOnly).build();
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
- typeSubtype.second,
- MdnsAdvertisingOptions.newBuilder().build());
+ mdnsAdvertisingOptions);
storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
serviceInfo.getNetwork());
} else {
maybeStartDaemon();
+ transactionId = getUniqueId();
if (registerService(transactionId, serviceInfo)) {
if (DBG) {
Log.d(TAG, "Register " + clientRequestId
@@ -877,7 +961,7 @@
}
case NsdManager.UNREGISTER_SERVICE: {
if (DBG) Log.d(TAG, "unregister service");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -920,7 +1004,7 @@
}
case NsdManager.RESOLVE_SERVICE: {
if (DBG) Log.d(TAG, "Resolve service");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -932,7 +1016,7 @@
final NsdServiceInfo info = args.serviceInfo;
transactionId = getUniqueId();
- final Pair<String, String> typeSubtype =
+ final Pair<String, List<String>> typeSubtype =
parseTypeAndSubtype(info.getServiceType());
final String serviceType = typeSubtype == null
? null : typeSubtype.first;
@@ -982,7 +1066,7 @@
}
case NsdManager.STOP_RESOLUTION: {
if (DBG) Log.d(TAG, "Stop service resolution");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1021,7 +1105,7 @@
}
case NsdManager.REGISTER_SERVICE_CALLBACK: {
if (DBG) Log.d(TAG, "Register a service callback");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1033,7 +1117,7 @@
final NsdServiceInfo info = args.serviceInfo;
transactionId = getUniqueId();
- final Pair<String, String> typeAndSubtype =
+ final Pair<String, List<String>> typeAndSubtype =
parseTypeAndSubtype(info.getServiceType());
final String serviceType = typeAndSubtype == null
? null : typeAndSubtype.first;
@@ -1064,7 +1148,7 @@
}
case NsdManager.UNREGISTER_SERVICE_CALLBACK: {
if (DBG) Log.d(TAG, "Unregister a service callback");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1388,6 +1472,7 @@
servInfo,
network == null ? INetd.LOCAL_NET_ID : network.netId,
serviceInfo.getInterfaceIndex());
+ servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
return servInfo;
}
@@ -1581,34 +1666,36 @@
* underscore; they are alphanumerical characters or dashes or underscore, except the
* last one that is just alphanumerical. The last label must be _tcp or _udp.
*
- * <p>The subtype may also be specified with a comma after the service type, for example
- * _type._tcp,_subtype.
+ * <p>The subtypes may also be specified with a comma after the service type, for example
+ * _type._tcp,_subtype1,_subtype2
*
* @param serviceType the request service type for discovery / resolution service
* @return constructed service type or null if the given service type is invalid.
*/
@Nullable
- public static Pair<String, String> parseTypeAndSubtype(String serviceType) {
+ public static Pair<String, List<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 + ")\\.)?"
- // Actual type (_type._tcp.local)
- + "(" + typeOrSubtypePattern + "\\._(?: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 + "))?"
- + "$");
+ final Pattern serviceTypePattern = Pattern.compile(TYPE_REGEX);
final Matcher matcher = serviceTypePattern.matcher(serviceType);
if (!matcher.matches()) return null;
- // Use the subtype either at the beginning or after the comma
- final String subtype = matcher.group(1) != null ? matcher.group(1) : matcher.group(3);
- return new Pair<>(matcher.group(2), subtype);
+ final String queryType = matcher.group(2);
+ // Use the subtype at the beginning
+ if (matcher.group(1) != null) {
+ return new Pair<>(queryType, List.of(matcher.group(1)));
+ }
+ // Use the subtypes at the end
+ final String subTypesStr = matcher.group(3);
+ if (subTypesStr != null && !subTypesStr.isEmpty()) {
+ final String[] subTypes = subTypesStr.substring(1).split(",");
+ return new Pair<>(queryType, List.of(subTypes));
+ }
+
+ return new Pair<>(queryType, Collections.emptyList());
+ }
+
+ /** 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
@@ -1622,7 +1709,8 @@
mContext = ctx;
mNsdStateMachine = new NsdStateMachine(TAG, handler);
mNsdStateMachine.start();
- mMDnsManager = ctx.getSystemService(MDnsManager.class);
+ // It can fail on V+ device since mdns native service provided by netd is removed.
+ mMDnsManager = SdkLevel.isAtLeastV() ? null : ctx.getSystemService(MDnsManager.class);
mMDnsEventCallback = new MDnsEventCallback(mNsdStateMachine);
mDeps = deps;
@@ -1653,6 +1741,8 @@
mContext, MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
.setIsLabelCountLimitEnabled(mDeps.isTetheringFeatureNotChickenedOut(
mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
+ .setIsKnownAnswerSuppressionEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_KNOWN_ANSWER_SUPPRESSION))
.build();
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
@@ -2014,20 +2104,33 @@
}
}
+ private static class AdvertisingArgs {
+ public final NsdServiceConnector connector;
+ public final AdvertisingRequest advertisingRequest;
+
+ AdvertisingArgs(NsdServiceConnector connector, AdvertisingRequest advertisingRequest) {
+ this.connector = connector;
+ this.advertisingRequest = advertisingRequest;
+ }
+ }
+
private class NsdServiceConnector extends INsdServiceConnector.Stub
implements IBinder.DeathRecipient {
+
@Override
- public void registerService(int listenerKey, NsdServiceInfo serviceInfo) {
+ public void registerService(int listenerKey, AdvertisingRequest advertisingRequest)
+ throws RemoteException {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.REGISTER_SERVICE, 0, listenerKey,
- new ListenerArgs(this, serviceInfo)));
+ new AdvertisingArgs(this, advertisingRequest)
+ ));
}
@Override
public void unregisterService(int listenerKey) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.UNREGISTER_SERVICE, 0, listenerKey,
- new ListenerArgs(this, null)));
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2039,8 +2142,8 @@
@Override
public void stopDiscovery(int listenerKey) {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.STOP_DISCOVERY, 0, listenerKey, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_DISCOVERY,
+ 0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2052,8 +2155,8 @@
@Override
public void stopResolution(int listenerKey) {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.STOP_RESOLUTION, 0, listenerKey, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_RESOLUTION,
+ 0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2067,13 +2170,13 @@
public void unregisterServiceInfoCallback(int listenerKey) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.UNREGISTER_SERVICE_CALLBACK, 0, listenerKey,
- new ListenerArgs(this, null)));
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
public void startDaemon() {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.DAEMON_STARTUP,
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2109,25 +2212,24 @@
throw new SecurityException("API is not available in before API level 33");
}
- // REGISTER_NSD_OFFLOAD_ENGINE was only added to the SDK in V.
- if (SdkLevel.isAtLeastV() && PermissionUtils.checkAnyPermissionOf(context,
- REGISTER_NSD_OFFLOAD_ENGINE)) {
- return;
+ final ArrayList<String> permissionsList = new ArrayList<>(Arrays.asList(NETWORK_STACK,
+ PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS));
+
+ if (SdkLevel.isAtLeastV()) {
+ // REGISTER_NSD_OFFLOAD_ENGINE was only added to the SDK in V.
+ permissionsList.add(REGISTER_NSD_OFFLOAD_ENGINE);
+ } else if (SdkLevel.isAtLeastU()) {
+ // REGISTER_NSD_OFFLOAD_ENGINE cannot be backport to U. In U, check the DEVICE_POWER
+ // permission instead.
+ permissionsList.add(DEVICE_POWER);
}
- // REGISTER_NSD_OFFLOAD_ENGINE cannot be backport to U. In U, check the DEVICE_POWER
- // permission instead.
- if (!SdkLevel.isAtLeastV() && SdkLevel.isAtLeastU()
- && PermissionUtils.checkAnyPermissionOf(context, DEVICE_POWER)) {
- return;
- }
- if (PermissionUtils.checkAnyPermissionOf(context, NETWORK_STACK,
- PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) {
+ if (PermissionUtils.checkAnyPermissionOf(context,
+ permissionsList.toArray(new String[0]))) {
return;
}
throw new SecurityException("Requires one of the following permissions: "
- + String.join(", ", List.of(REGISTER_NSD_OFFLOAD_ENGINE, NETWORK_STACK,
- PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) + ".");
+ + String.join(", ", permissionsList) + ".");
}
}
@@ -2145,6 +2247,11 @@
}
private boolean registerService(int transactionId, NsdServiceInfo service) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "registerService: mMDnsManager is null");
+ return false;
+ }
+
if (DBG) {
Log.d(TAG, "registerService: " + transactionId + " " + service);
}
@@ -2162,10 +2269,19 @@
}
private boolean unregisterService(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "unregisterService: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
private boolean discoverServices(int transactionId, NsdServiceInfo serviceInfo) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "discoverServices: mMDnsManager is null");
+ return false;
+ }
+
final String type = serviceInfo.getServiceType();
final int discoverInterface = getNetworkInterfaceIndex(serviceInfo);
if (serviceInfo.getNetwork() != null && discoverInterface == IFACE_IDX_ANY) {
@@ -2176,10 +2292,18 @@
}
private boolean stopServiceDiscovery(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "stopServiceDiscovery: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
private boolean resolveService(int transactionId, NsdServiceInfo service) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "resolveService: mMDnsManager is null");
+ return false;
+ }
final String name = service.getServiceName();
final String type = service.getServiceType();
final int resolveInterface = getNetworkInterfaceIndex(service);
@@ -2253,14 +2377,26 @@
}
private boolean stopResolveService(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "stopResolveService: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
private boolean getAddrInfo(int transactionId, String hostname, int interfaceIdx) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "getAddrInfo: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.getServiceAddress(transactionId, hostname, interfaceIdx);
}
private boolean stopGetAddrInfo(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "stopGetAddrInfo: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
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/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 0a6d8c1..1ad47a3 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -41,6 +41,11 @@
*/
public static final String NSD_LIMIT_LABEL_COUNT = "nsd_limit_label_count";
+ /**
+ * A feature flag to control whether the known-answer suppression should be enabled.
+ */
+ public static final String NSD_KNOWN_ANSWER_SUPPRESSION = "nsd_known_answer_suppression";
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
@@ -53,17 +58,22 @@
// Flag for label count limit
public final boolean mIsLabelCountLimitEnabled;
+ // Flag for known-answer suppression
+ public final boolean mIsKnownAnswerSuppressionEnabled;
+
/**
* The constructor for {@link MdnsFeatureFlags}.
*/
public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
boolean includeInetAddressRecordsInProbing,
boolean isExpiredServicesRemovalEnabled,
- boolean isLabelCountLimitEnabled) {
+ boolean isLabelCountLimitEnabled,
+ boolean isKnownAnswerSuppressionEnabled) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
+ mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
}
@@ -79,6 +89,7 @@
private boolean mIncludeInetAddressRecordsInProbing;
private boolean mIsExpiredServicesRemovalEnabled;
private boolean mIsLabelCountLimitEnabled;
+ private boolean mIsKnownAnswerSuppressionEnabled;
/**
* The constructor for {@link Builder}.
@@ -88,6 +99,7 @@
mIncludeInetAddressRecordsInProbing = false;
mIsExpiredServicesRemovalEnabled = false;
mIsLabelCountLimitEnabled = true; // Default enabled.
+ mIsKnownAnswerSuppressionEnabled = false;
}
/**
@@ -132,13 +144,24 @@
}
/**
+ * Set whether the known-answer suppression is enabled.
+ *
+ * @see #NSD_KNOWN_ANSWER_SUPPRESSION
+ */
+ public Builder setIsKnownAnswerSuppressionEnabled(boolean isKnownAnswerSuppressionEnabled) {
+ mIsKnownAnswerSuppressionEnabled = isKnownAnswerSuppressionEnabled;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
return new MdnsFeatureFlags(mIsMdnsOffloadFeatureEnabled,
mIncludeInetAddressRecordsInProbing,
mIsExpiredServicesRemovalEnabled,
- mIsLabelCountLimitEnabled);
+ mIsLabelCountLimitEnabled,
+ mIsKnownAnswerSuppressionEnabled);
}
}
}
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..f9951a9 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -46,6 +46,7 @@
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -74,8 +75,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 =
@@ -92,6 +91,7 @@
private final Looper mLooper;
@NonNull
private final String[] mDeviceHostname;
+ @NonNull
private final MdnsFeatureFlags mMdnsFeatureFlags;
public MdnsRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostname,
@@ -141,6 +141,9 @@
* Last time (as per SystemClock.elapsedRealtime) when sent via unicast or multicast,
* 0 if never
*/
+ // FIXME: the `lastSentTimeMs` and `lastAdvertisedTimeMs` should be maintained separately
+ // for IPv4 and IPv6, because neither IPv4 nor and IPv6 clients can receive replies in
+ // different address space.
public long lastSentTimeMs;
RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName) {
@@ -161,8 +164,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 +186,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 +215,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 +276,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 +320,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 +344,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 +357,7 @@
}
final ServiceRegistration registration = new ServiceRegistration(
- mDeviceHostname, serviceInfo, subtype, NO_PACKET /* repliedServiceCount */,
+ mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
NO_PACKET /* sentPacketCount */);
mServices.put(serviceId, registration);
@@ -510,13 +501,17 @@
public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
final long now = SystemClock.elapsedRealtime();
final boolean replyUnicast = (packet.flags & MdnsConstants.QCLASS_UNICAST) != 0;
- final ArrayList<MdnsRecord> additionalAnswerRecords = new ArrayList<>();
- final ArrayList<RecordInfo<?>> answerInfo = new ArrayList<>();
+
+ // Use LinkedHashSet for preserving the insert order of the RRs, so that RRs of the same
+ // service or host are grouped together (which is more developer-friendly).
+ final Set<RecordInfo<?>> answerInfo = new LinkedHashSet<>();
+ final Set<RecordInfo<?>> additionalAnswerInfo = new LinkedHashSet<>();
+
for (MdnsRecord question : packet.questions) {
// Add answers from general records
addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
null /* serviceSrvRecord */, null /* serviceTxtRecord */, replyUnicast, now,
- answerInfo, additionalAnswerRecords);
+ answerInfo, additionalAnswerInfo, Collections.emptyList());
// Add answers from each service
for (int i = 0; i < mServices.size(); i++) {
@@ -524,13 +519,33 @@
if (registration.exiting || registration.isProbing) continue;
if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
registration.srvRecord, registration.txtRecord, replyUnicast, now,
- answerInfo, additionalAnswerRecords)) {
+ answerInfo, additionalAnswerInfo, packet.answers)) {
registration.repliedServiceCount++;
registration.sentPacketCount++;
}
}
}
+ // If any record was already in the answer section, remove it from the additional answer
+ // section. This can typically happen when there are both queries for
+ // SRV / TXT / A / AAAA and PTR (which can cause SRV / TXT / A / AAAA records being added
+ // to the additional answer section).
+ additionalAnswerInfo.removeAll(answerInfo);
+
+ final List<MdnsRecord> additionalAnswerRecords =
+ new ArrayList<>(additionalAnswerInfo.size());
+ for (RecordInfo<?> info : additionalAnswerInfo) {
+ additionalAnswerRecords.add(info.record);
+ }
+
+ // RFC6762 6.1: negative responses
+ // "On receipt of a question for a particular name, rrtype, and rrclass, for which a
+ // responder does have one or more unique answers, the responder MAY also include an NSEC
+ // record in the Additional Record Section indicating the nonexistence of other rrtypes
+ // for that name and rrclass."
+ addNsecRecordsForUniqueNames(additionalAnswerRecords,
+ answerInfo.iterator(), additionalAnswerInfo.iterator());
+
if (answerInfo.size() == 0 && additionalAnswerRecords.size() == 0) {
return null;
}
@@ -577,6 +592,15 @@
return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
}
+ private boolean isKnownAnswer(MdnsRecord answer, @NonNull List<MdnsRecord> knownAnswerRecords) {
+ for (MdnsRecord knownAnswer : knownAnswerRecords) {
+ if (answer.equals(knownAnswer) && knownAnswer.getTtl() > (answer.getTtl() / 2)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Add answers and additional answers for a question, from a ServiceRegistration.
*/
@@ -585,14 +609,15 @@
@Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords,
@Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
@Nullable RecordInfo<MdnsTextRecord> serviceTxtRecord,
- boolean replyUnicast, long now, @NonNull List<RecordInfo<?>> answerInfo,
- @NonNull List<MdnsRecord> additionalAnswerRecords) {
+ boolean replyUnicast, long now, @NonNull Set<RecordInfo<?>> answerInfo,
+ @NonNull Set<RecordInfo<?>> additionalAnswerInfo,
+ @NonNull List<MdnsRecord> knownAnswerRecords) {
boolean hasDnsSdPtrRecordAnswer = false;
boolean hasDnsSdSrvRecordAnswer = false;
boolean hasFullyOwnedNameMatch = false;
boolean hasKnownAnswer = false;
- final int answersStartIndex = answerInfo.size();
+ final int answersStartSize = answerInfo.size();
for (RecordInfo<?> info : serviceRecords) {
/* RFC6762 6.: the record name must match the question name, the record rrtype
@@ -615,6 +640,20 @@
}
hasKnownAnswer = true;
+
+ // RFC6762 7.1. Known-Answer Suppression:
+ // A Multicast DNS responder MUST NOT answer a Multicast DNS query if
+ // the answer it would give is already included in the Answer Section
+ // with an RR TTL at least half the correct value. If the RR TTL of the
+ // answer as given in the Answer Section is less than half of the true
+ // RR TTL as known by the Multicast DNS responder, the responder MUST
+ // send an answer so as to update the querier's cache before the record
+ // becomes in danger of expiration.
+ if (mMdnsFeatureFlags.mIsKnownAnswerSuppressionEnabled
+ && isKnownAnswer(info.record, knownAnswerRecords)) {
+ continue;
+ }
+
hasDnsSdPtrRecordAnswer |= (servicePtrRecords != null
&& CollectionUtils.any(servicePtrRecords, r -> info == r));
hasDnsSdSrvRecordAnswer |= (info == serviceSrvRecord);
@@ -626,8 +665,6 @@
continue;
}
- // TODO: Don't reply if in known answers of the querier (7.1) if TTL is > half
-
answerInfo.add(info);
}
@@ -636,7 +673,7 @@
// ownership, for a type for which that name has no records, the responder MUST [...]
// respond asserting the nonexistence of that record"
if (hasFullyOwnedNameMatch && !hasKnownAnswer) {
- additionalAnswerRecords.add(new MdnsNsecRecord(
+ MdnsNsecRecord nsecRecord = new MdnsNsecRecord(
question.getName(),
0L /* receiptTimeMillis */,
true /* cacheFlush */,
@@ -644,13 +681,14 @@
// be the same as the TTL that the record would have had, had it existed."
NAME_RECORDS_TTL_MILLIS,
question.getName(),
- new int[] { question.getType() }));
+ new int[] { question.getType() });
+ additionalAnswerInfo.add(
+ new RecordInfo<>(null /* serviceInfo */, nsecRecord, false /* isSharedName */));
}
// No more records to add if no answer
- if (answerInfo.size() == answersStartIndex) return false;
+ if (answerInfo.size() == answersStartSize) return false;
- final List<RecordInfo<?>> additionalAnswerInfo = new ArrayList<>();
// RFC6763 12.1: if including PTR record, include the SRV and TXT records it names
if (hasDnsSdPtrRecordAnswer) {
if (serviceTxtRecord != null) {
@@ -669,15 +707,6 @@
}
}
}
-
- for (RecordInfo<?> info : additionalAnswerInfo) {
- additionalAnswerRecords.add(info.record);
- }
-
- // RFC6762 6.1: negative responses
- addNsecRecordsForUniqueNames(additionalAnswerRecords,
- answerInfo.listIterator(answersStartIndex),
- additionalAnswerInfo.listIterator());
return true;
}
@@ -694,7 +723,7 @@
* answer and additionalAnswer sections)
*/
@SafeVarargs
- private static void addNsecRecordsForUniqueNames(
+ private void addNsecRecordsForUniqueNames(
List<MdnsRecord> destinationList,
Iterator<RecordInfo<?>>... answerRecords) {
// Group unique records by name. Use a TreeMap with comparator as arrays don't implement
@@ -710,6 +739,12 @@
for (String[] nsecName : namesInAddedOrder) {
final List<MdnsRecord> entryRecords = nsecByName.get(nsecName);
+
+ // Add NSEC records only when the answers include all unique records of this name
+ if (entryRecords.size() != countUniqueRecords(nsecName)) {
+ continue;
+ }
+
long minTtl = Long.MAX_VALUE;
final Set<Integer> types = new ArraySet<>(entryRecords.size());
for (MdnsRecord record : entryRecords) {
@@ -727,6 +762,27 @@
}
}
+ /** Returns the number of unique records on this device for a given {@code name}. */
+ private int countUniqueRecords(String[] name) {
+ int cnt = countUniqueRecords(mGeneralRecords, name);
+
+ for (int i = 0; i < mServices.size(); i++) {
+ final ServiceRegistration registration = mServices.valueAt(i);
+ cnt += countUniqueRecords(registration.allRecords, name);
+ }
+ return cnt;
+ }
+
+ private static int countUniqueRecords(List<RecordInfo<?>> records, String[] name) {
+ int cnt = 0;
+ for (RecordInfo<?> record : records) {
+ if (!record.isSharedName && Arrays.equals(name, record.record.getName())) {
+ cnt++;
+ }
+ }
+ return cnt;
+ }
+
/**
* Add non-shared records to a map listing them by record name, and to a list of names that
* remembers the adding order.
@@ -741,10 +797,10 @@
private static void addNonSharedRecordsToMap(
Iterator<RecordInfo<?>> records,
Map<String[], List<MdnsRecord>> dest,
- List<String[]> namesInAddedOrder) {
+ @Nullable List<String[]> namesInAddedOrder) {
while (records.hasNext()) {
final RecordInfo<?> record = records.next();
- if (record.isSharedName) continue;
+ if (record.isSharedName || record.record instanceof MdnsNsecRecord) continue;
final List<MdnsRecord> recordsForName = dest.computeIfAbsent(record.record.name,
key -> {
namesInAddedOrder.add(key);
@@ -929,7 +985,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/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index ea3af5e..651b643 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -25,6 +25,7 @@
import android.os.Looper;
import android.os.Message;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -57,15 +58,46 @@
@NonNull
private final SharedLog mSharedLog;
private final boolean mEnableDebugLog;
+ @NonNull
+ private final Dependencies mDependencies;
+
+ /**
+ * Dependencies of MdnsReplySender, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * @see Handler#sendMessageDelayed(Message, long)
+ */
+ public void sendMessageDelayed(@NonNull Handler handler, @NonNull Message message,
+ long delayMillis) {
+ handler.sendMessageDelayed(message, delayMillis);
+ }
+
+ /**
+ * @see Handler#removeMessages(int)
+ */
+ public void removeMessages(@NonNull Handler handler, int what) {
+ handler.removeMessages(what);
+ }
+ }
public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
@NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
boolean enableDebugLog) {
+ this(looper, socket, packetCreationBuffer, sharedLog, enableDebugLog, new Dependencies());
+ }
+
+ @VisibleForTesting
+ public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
+ @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
+ boolean enableDebugLog, @NonNull Dependencies dependencies) {
mHandler = new SendHandler(looper);
mSocket = socket;
mPacketCreationBuffer = packetCreationBuffer;
mSharedLog = sharedLog;
mEnableDebugLog = enableDebugLog;
+ mDependencies = dependencies;
}
/**
@@ -74,7 +106,8 @@
public void queueReply(@NonNull MdnsReplyInfo reply) {
ensureRunningOnHandlerThread(mHandler);
// TODO: implement response aggregation (RFC 6762 6.4)
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
+ mDependencies.sendMessageDelayed(
+ mHandler, mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
if (mEnableDebugLog) {
mSharedLog.v("Scheduling " + reply);
@@ -104,7 +137,7 @@
*/
public void cancelAll() {
ensureRunningOnHandlerThread(mHandler);
- mHandler.removeMessages(MSG_SEND);
+ mDependencies.removeMessages(mHandler, MSG_SEND);
}
private class SendHandler extends Handler {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 32f604e..df0a040 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -541,6 +541,9 @@
}
if (response.isComplete()) {
+ // There is a bug here: the newServiceFound is global right now. The state needs
+ // to be per listener because of the responseMatchesOptions() filter.
+ // Otherwise, it won't handle the subType update properly.
if (newServiceFound || serviceBecomesComplete) {
sharedLog.log("onServiceFound: " + serviceInfo);
listener.onServiceFound(serviceInfo, false /* isServiceFromCache */);
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-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 48e86d8..458d64f 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -48,6 +48,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
@@ -237,7 +238,18 @@
mDeps = deps;
// Interface match regex.
- mIfaceMatch = mDeps.getInterfaceRegexFromResource(mContext);
+ String ifaceMatchRegex = mDeps.getInterfaceRegexFromResource(mContext);
+ // "*" is a magic string to indicate "pick the default".
+ if (ifaceMatchRegex.equals("*")) {
+ if (SdkLevel.isAtLeastV()) {
+ // On V+, include both usb%d and eth%d interfaces.
+ ifaceMatchRegex = "(usb|eth)\\d+";
+ } else {
+ // On T and U, include only eth%d interfaces.
+ ifaceMatchRegex = "eth\\d+";
+ }
+ }
+ mIfaceMatch = ifaceMatchRegex;
// Read default Ethernet interface configuration from resources
final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context);
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index 3da1585..8ee8591 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -22,6 +22,7 @@
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.net.NetworkIdentitySet;
import android.net.NetworkStats;
import android.net.NetworkStats.NonMonotonicObserver;
@@ -32,17 +33,20 @@
import android.net.TrafficStats;
import android.os.Binder;
import android.os.DropBoxManager;
+import android.os.SystemClock;
import android.service.NetworkStatsRecorderProto;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
import com.android.internal.util.FileRotator;
+import com.android.metrics.NetworkStatsMetricsLogger;
import com.android.net.module.util.NetworkStatsUtils;
import libcore.io.IoUtils;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -79,6 +83,7 @@
private final long mBucketDuration;
private final boolean mOnlyTags;
private final boolean mWipeOnError;
+ private final boolean mUseFastDataInput;
private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
private NetworkStats mLastSnapshot;
@@ -89,6 +94,9 @@
private final CombiningRewriter mPendingRewriter;
private WeakReference<NetworkStatsCollection> mComplete;
+ private final NetworkStatsMetricsLogger mMetricsLogger = new NetworkStatsMetricsLogger();
+ @Nullable
+ private final File mStatsDir;
/**
* Non-persisted recorder, with only one bucket. Used by {@link NetworkStatsObservers}.
@@ -104,11 +112,13 @@
mBucketDuration = YEAR_IN_MILLIS;
mOnlyTags = false;
mWipeOnError = true;
+ mUseFastDataInput = false;
mPending = null;
mSinceBoot = new NetworkStatsCollection(mBucketDuration);
mPendingRewriter = null;
+ mStatsDir = null;
}
/**
@@ -116,7 +126,7 @@
*/
public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags,
- boolean wipeOnError) {
+ boolean wipeOnError, boolean useFastDataInput, @Nullable File statsDir) {
mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
@@ -125,11 +135,13 @@
mBucketDuration = bucketDuration;
mOnlyTags = onlyTags;
mWipeOnError = wipeOnError;
+ mUseFastDataInput = useFastDataInput;
mPending = new NetworkStatsCollection(bucketDuration);
mSinceBoot = new NetworkStatsCollection(bucketDuration);
mPendingRewriter = new CombiningRewriter(mPending);
+ mStatsDir = statsDir;
}
public void setPersistThreshold(long thresholdBytes) {
@@ -179,8 +191,16 @@
Objects.requireNonNull(mRotator, "missing FileRotator");
NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
if (res == null) {
+ final long readStart = SystemClock.elapsedRealtime();
res = loadLocked(Long.MIN_VALUE, Long.MAX_VALUE);
mComplete = new WeakReference<NetworkStatsCollection>(res);
+ final long readEnd = SystemClock.elapsedRealtime();
+ // For legacy recorders which are used for data integrity check, which
+ // have wipeOnError flag unset, skip reporting metrics.
+ if (mWipeOnError) {
+ mMetricsLogger.logRecorderFileReading(mCookie, (int) (readEnd - readStart),
+ mStatsDir, res, mUseFastDataInput);
+ }
}
return res;
}
@@ -195,8 +215,12 @@
}
private NetworkStatsCollection loadLocked(long start, long end) {
- if (LOGD) Log.d(TAG, "loadLocked() reading from disk for " + mCookie);
- final NetworkStatsCollection res = new NetworkStatsCollection(mBucketDuration);
+ if (LOGD) {
+ Log.d(TAG, "loadLocked() reading from disk for " + mCookie
+ + " useFastDataInput: " + mUseFastDataInput);
+ }
+ final NetworkStatsCollection res =
+ new NetworkStatsCollection(mBucketDuration, mUseFastDataInput);
try {
mRotator.readMatching(res, start, end);
res.recordCollection(mPending);
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 2c9f30c..3ac5e29 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -44,7 +44,6 @@
import static android.net.NetworkStats.TAG_ALL;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
-import static android.net.NetworkStatsCollection.compareStats;
import static android.net.NetworkStatsHistory.FIELD_ALL;
import static android.net.NetworkTemplate.MATCH_MOBILE;
import static android.net.NetworkTemplate.MATCH_TEST;
@@ -295,6 +294,11 @@
static final String CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER =
"enable_network_stats_event_logger";
+ static final String NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS =
+ "netstats_fastdatainput_target_attempts";
+ static final String NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME = "fastdatainput.successes";
+ static final String NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME = "fastdatainput.fallbacks";
+
private final Context mContext;
private final NetworkStatsFactory mStatsFactory;
private final AlarmManager mAlarmManager;
@@ -318,6 +322,8 @@
private PersistentInt mImportLegacyAttemptsCounter = null;
private PersistentInt mImportLegacySuccessesCounter = null;
private PersistentInt mImportLegacyFallbacksCounter = null;
+ private PersistentInt mFastDataInputSuccessesCounter = null;
+ private PersistentInt mFastDataInputFallbacksCounter = null;
@VisibleForTesting
public static final String ACTION_NETWORK_STATS_POLL =
@@ -695,6 +701,24 @@
}
/**
+ * Get the count of using FastDataInput target attempts.
+ */
+ public int getUseFastDataInputTargetAttempts() {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(
+ DeviceConfig.NAMESPACE_TETHERING,
+ NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS, 0);
+ }
+
+ /**
+ * Compare two {@link NetworkStatsCollection} instances and returning a human-readable
+ * string description of difference for debugging purpose.
+ */
+ public String compareStats(@NonNull NetworkStatsCollection a,
+ @NonNull NetworkStatsCollection b, boolean allowKeyChange) {
+ return NetworkStatsCollection.compareStats(a, b, allowKeyChange);
+ }
+
+ /**
* Create a persistent counter for given directory and name.
*/
public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name)
@@ -892,13 +916,7 @@
synchronized (mStatsLock) {
mSystemReady = true;
- // create data recorders along with historical rotators
- mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
- true /* wipeOnError */);
- mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
- true /* wipeOnError */);
- mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
- mStatsDir, true /* wipeOnError */);
+ makeRecordersLocked();
updatePersistThresholdsLocked();
@@ -963,13 +981,106 @@
private NetworkStatsRecorder buildRecorder(
String prefix, NetworkStatsSettings.Config config, boolean includeTags,
- File baseDir, boolean wipeOnError) {
+ File baseDir, boolean wipeOnError, boolean useFastDataInput) {
final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
Context.DROPBOX_SERVICE);
return new NetworkStatsRecorder(new FileRotator(
baseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags,
- wipeOnError);
+ wipeOnError, useFastDataInput, baseDir);
+ }
+
+ @GuardedBy("mStatsLock")
+ private void makeRecordersLocked() {
+ boolean useFastDataInput = true;
+ try {
+ mFastDataInputSuccessesCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME);
+ mFastDataInputFallbacksCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME);
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to create persistent counters, skip.", e);
+ useFastDataInput = false;
+ }
+
+ final int targetAttempts = mDeps.getUseFastDataInputTargetAttempts();
+ int successes = 0;
+ int fallbacks = 0;
+ try {
+ successes = mFastDataInputSuccessesCounter.get();
+ // Fallbacks counter would be set to non-zero value to indicate the reading was
+ // not successful.
+ fallbacks = mFastDataInputFallbacksCounter.get();
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to read counters, skip.", e);
+ useFastDataInput = false;
+ }
+
+ final boolean doComparison;
+ if (useFastDataInput) {
+ // Use FastDataInput if it needs to be evaluated or at least one success.
+ doComparison = targetAttempts > successes + fallbacks;
+ // Set target attempt to -1 as the kill switch to disable the feature.
+ useFastDataInput = targetAttempts >= 0 && (doComparison || successes > 0);
+ } else {
+ // useFastDataInput is false due to previous failures.
+ doComparison = false;
+ }
+
+ // create data recorders along with historical rotators.
+ // Don't wipe on error if comparison is needed.
+ mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+ !doComparison /* wipeOnError */, useFastDataInput);
+ mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+ !doComparison /* wipeOnError */, useFastDataInput);
+ mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
+ mStatsDir, !doComparison /* wipeOnError */, useFastDataInput);
+
+ if (!doComparison) return;
+
+ final MigrationInfo[] migrations = new MigrationInfo[]{
+ new MigrationInfo(mXtRecorder),
+ new MigrationInfo(mUidRecorder),
+ new MigrationInfo(mUidTagRecorder)
+ };
+ // Set wipeOnError flag false so the recorder won't damage persistent data if reads
+ // failed and calling deleteAll.
+ final NetworkStatsRecorder[] legacyRecorders = new NetworkStatsRecorder[]{
+ buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+ false /* wipeOnError */, false /* useFastDataInput */),
+ buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+ false /* wipeOnError */, false /* useFastDataInput */),
+ buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, mStatsDir,
+ false /* wipeOnError */, false /* useFastDataInput */)};
+ boolean success = true;
+ for (int i = 0; i < migrations.length; i++) {
+ try {
+ migrations[i].collection = migrations[i].recorder.getOrLoadCompleteLocked();
+ } catch (Throwable t) {
+ Log.wtf(TAG, "Failed to load collection, skip.", t);
+ success = false;
+ break;
+ }
+ if (!compareImportedToLegacyStats(migrations[i], legacyRecorders[i],
+ false /* allowKeyChange */)) {
+ success = false;
+ break;
+ }
+ }
+
+ try {
+ if (success) {
+ mFastDataInputSuccessesCounter.set(successes + 1);
+ } else {
+ // Fallback.
+ mXtRecorder = legacyRecorders[0];
+ mUidRecorder = legacyRecorders[1];
+ mUidTagRecorder = legacyRecorders[2];
+ mFastDataInputFallbacksCounter.set(fallbacks + 1);
+ }
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to update counters. success = " + success, e);
+ }
}
@GuardedBy("mStatsLock")
@@ -1068,7 +1179,7 @@
new NetworkStatsSettings.Config(HOUR_IN_MILLIS,
15 * DAY_IN_MILLIS, 90 * DAY_IN_MILLIS);
final NetworkStatsRecorder devRecorder = buildRecorder(PREFIX_DEV, devConfig,
- false, mStatsDir, true /* wipeOnError */);
+ false, mStatsDir, true /* wipeOnError */, false /* useFastDataInput */);
final MigrationInfo[] migrations = new MigrationInfo[]{
new MigrationInfo(devRecorder), new MigrationInfo(mXtRecorder),
new MigrationInfo(mUidRecorder), new MigrationInfo(mUidTagRecorder)
@@ -1085,11 +1196,11 @@
legacyRecorders = new NetworkStatsRecorder[]{
null /* dev Recorder */,
buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir,
- false /* wipeOnError */),
+ false /* wipeOnError */, false /* useFastDataInput */),
buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir,
- false /* wipeOnError */),
+ false /* wipeOnError */, false /* useFastDataInput */),
buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir,
- false /* wipeOnError */)};
+ false /* wipeOnError */, false /* useFastDataInput */)};
} else {
legacyRecorders = null;
}
@@ -1120,7 +1231,8 @@
if (runComparison) {
final boolean success =
- compareImportedToLegacyStats(migration, legacyRecorders[i]);
+ compareImportedToLegacyStats(migration, legacyRecorders[i],
+ true /* allowKeyChange */);
if (!success && !dryRunImportOnly) {
tryIncrementLegacyFallbacksCounter();
}
@@ -1243,7 +1355,7 @@
* does not match or throw with exceptions.
*/
private boolean compareImportedToLegacyStats(@NonNull MigrationInfo migration,
- @Nullable NetworkStatsRecorder legacyRecorder) {
+ @Nullable NetworkStatsRecorder legacyRecorder, boolean allowKeyChange) {
final NetworkStatsCollection legacyStats;
// Skip the recorder that doesn't need to be compared.
if (legacyRecorder == null) return true;
@@ -1258,7 +1370,8 @@
// The result of comparison is only for logging.
try {
- final String error = compareStats(migration.collection, legacyStats);
+ final String error = mDeps.compareStats(migration.collection, legacyStats,
+ allowKeyChange);
if (error != null) {
Log.wtf(TAG, "Unexpected comparison result for recorder "
+ legacyRecorder.getCookie() + ": " + error);
@@ -2639,6 +2752,17 @@
}
}
pw.println(CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER + ": " + mSupportEventLogger);
+ pw.print(NETSTATS_FASTDATAINPUT_TARGET_ATTEMPTS,
+ mDeps.getUseFastDataInputTargetAttempts());
+ pw.println();
+ try {
+ pw.print("FastDataInput successes", mFastDataInputSuccessesCounter.get());
+ pw.println();
+ pw.print("FastDataInput fallbacks", mFastDataInputFallbacksCounter.get());
+ pw.println();
+ } catch (IOException e) {
+ pw.println("(failed to dump FastDataInput counters)");
+ }
pw.decreaseIndent();
diff --git a/service/Android.bp b/service/Android.bp
index e2dab9e..7c5da0d 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -70,6 +70,9 @@
apex_available: [
"com.android.tethering",
],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
// The library name match the service-connectivity jarjar rules that put the JNI utils in the
@@ -200,7 +203,10 @@
apex_available: [
"com.android.tethering",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
+ },
visibility: [
"//packages/modules/Connectivity/service-t",
"//packages/modules/Connectivity/tests:__subpackages__",
@@ -225,6 +231,7 @@
],
lint: {
strict_updatability_linting: true,
+ baseline_filename: "lint-baseline.xml",
},
}
@@ -283,12 +290,18 @@
java_library {
name: "service-connectivity-for-tests",
defaults: ["service-connectivity-defaults"],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
java_library {
name: "service-connectivity",
defaults: ["service-connectivity-defaults"],
installable: true,
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
java_library_static {
@@ -303,6 +316,9 @@
],
static_libs: ["ConnectivityServiceprotos"],
apex_available: ["com.android.tethering"],
+ lint: {
+ baseline_filename: "lint-baseline.xml",
+ },
}
genrule {
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index f30abc6..6f9d46f 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -194,8 +194,11 @@
-->
</string-array>
- <!-- Regex of wired ethernet ifaces -->
- <string translatable="false" name="config_ethernet_iface_regex">eth\\d</string>
+ <!-- Regex of wired ethernet ifaces. Network interfaces that match this regex will be tracked
+ by ethernet service.
+ If set to "*", ethernet service uses "(eth|usb)\\d+" on Android V+ and eth\\d+ on
+ Android T and U. -->
+ <string translatable="false" name="config_ethernet_iface_regex">*</string>
<!-- Ignores Wi-Fi validation failures after roam.
If validation fails on a Wi-Fi network after a roam to a new BSSID,
diff --git a/service/src/com/android/metrics/NetworkRequestStateInfo.java b/service/src/com/android/metrics/NetworkRequestStateInfo.java
new file mode 100644
index 0000000..e3e172a
--- /dev/null
+++ b/service/src/com/android/metrics/NetworkRequestStateInfo.java
@@ -0,0 +1,99 @@
+/*
+ * 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.metrics;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.SystemClock;
+
+import com.android.net.module.util.BitUtils;
+
+
+class NetworkRequestStateInfo {
+ private final NetworkRequest mNetworkRequest;
+ private final long mNetworkRequestReceivedTime;
+
+ private enum NetworkRequestState {
+ RECEIVED,
+ REMOVED
+ }
+ private NetworkRequestState mNetworkRequestState;
+ private int mNetworkRequestDurationMillis;
+ private final Dependencies mDependencies;
+
+ NetworkRequestStateInfo(NetworkRequest networkRequest,
+ Dependencies deps) {
+ mDependencies = deps;
+ mNetworkRequest = networkRequest;
+ mNetworkRequestReceivedTime = mDependencies.getElapsedRealtime();
+ mNetworkRequestDurationMillis = 0;
+ mNetworkRequestState = NetworkRequestState.RECEIVED;
+ }
+
+ public void setNetworkRequestRemoved() {
+ mNetworkRequestState = NetworkRequestState.REMOVED;
+ mNetworkRequestDurationMillis = (int) (
+ mDependencies.getElapsedRealtime() - mNetworkRequestReceivedTime);
+ }
+
+ public int getNetworkRequestStateStatsType() {
+ if (mNetworkRequestState == NetworkRequestState.RECEIVED) {
+ return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+ } else if (mNetworkRequestState == NetworkRequestState.REMOVED) {
+ return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+ } else {
+ return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
+ }
+ }
+
+ public int getRequestId() {
+ return mNetworkRequest.requestId;
+ }
+
+ public int getPackageUid() {
+ return mNetworkRequest.networkCapabilities.getRequestorUid();
+ }
+
+ public int getTransportTypes() {
+ return (int) BitUtils.packBits(mNetworkRequest.networkCapabilities.getTransportTypes());
+ }
+
+ public boolean getNetCapabilityNotMetered() {
+ return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ }
+
+ public boolean getNetCapabilityInternet() {
+ return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ }
+
+ public int getNetworkRequestDurationMillis() {
+ return mNetworkRequestDurationMillis;
+ }
+
+ /** Dependency class */
+ public static class Dependencies {
+ // Returns a timestamp with the time base of SystemClock.elapsedRealtime to keep durations
+ // relative to start time and avoid timezone change, including time spent in deep sleep.
+ public long getElapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+}
diff --git a/service/src/com/android/metrics/NetworkRequestStateStatsMetrics.java b/service/src/com/android/metrics/NetworkRequestStateStatsMetrics.java
new file mode 100644
index 0000000..361ad22
--- /dev/null
+++ b/service/src/com/android/metrics/NetworkRequestStateStatsMetrics.java
@@ -0,0 +1,192 @@
+/*
+ * 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.metrics;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED;
+
+import android.annotation.NonNull;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ConnectivityStatsLog;
+
+/**
+ * A Connectivity Service helper class to push atoms capturing network requests have been received
+ * and removed and its metadata.
+ *
+ * Atom events are logged in the ConnectivityStatsLog. Network request id: network request metadata
+ * hashmap is stored to calculate network request duration when it is removed.
+ *
+ * Note that this class is not thread-safe. The instance of the class needs to be
+ * synchronized in the callers when being used in multiple threads.
+ */
+public class NetworkRequestStateStatsMetrics {
+
+ private static final String TAG = "NetworkRequestStateStatsMetrics";
+ private static final int MSG_NETWORK_REQUEST_STATE_CHANGED = 0;
+
+ // 1 second internal is suggested by experiment team
+ private static final int ATOM_INTERVAL_MS = 1000;
+ private final SparseArray<NetworkRequestStateInfo> mNetworkRequestsActive;
+
+ private final Handler mStatsLoggingHandler;
+
+ private final Dependencies mDependencies;
+
+ private final NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
+
+ public NetworkRequestStateStatsMetrics() {
+ this(new Dependencies(), new NetworkRequestStateInfo.Dependencies());
+ }
+
+ @VisibleForTesting
+ NetworkRequestStateStatsMetrics(Dependencies deps,
+ NetworkRequestStateInfo.Dependencies nrStateInfoDeps) {
+ mNetworkRequestsActive = new SparseArray<>();
+ mDependencies = deps;
+ mNRStateInfoDeps = nrStateInfoDeps;
+ HandlerThread handlerThread = mDependencies.makeHandlerThread(TAG);
+ handlerThread.start();
+ mStatsLoggingHandler = new StatsLoggingHandler(handlerThread.getLooper());
+ }
+
+ /**
+ * Register network request receive event, push RECEIVE atom
+ *
+ * @param networkRequest network request received
+ */
+ public void onNetworkRequestReceived(NetworkRequest networkRequest) {
+ if (mNetworkRequestsActive.contains(networkRequest.requestId)) {
+ Log.w(TAG, "Received already registered network request, id = "
+ + networkRequest.requestId);
+ } else {
+ Log.d(TAG, "Registered nr with ID = " + networkRequest.requestId
+ + ", package_uid = " + networkRequest.networkCapabilities.getRequestorUid());
+ NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
+ networkRequest, mNRStateInfoDeps);
+ mNetworkRequestsActive.put(networkRequest.requestId, networkRequestStateInfo);
+ mStatsLoggingHandler.sendMessage(
+ Message.obtain(
+ mStatsLoggingHandler,
+ MSG_NETWORK_REQUEST_STATE_CHANGED,
+ networkRequestStateInfo));
+ }
+ }
+
+ /**
+ * Register network request remove event, push REMOVE atom
+ *
+ * @param networkRequest network request removed
+ */
+ public void onNetworkRequestRemoved(NetworkRequest networkRequest) {
+ NetworkRequestStateInfo networkRequestStateInfo = mNetworkRequestsActive.get(
+ networkRequest.requestId);
+ if (networkRequestStateInfo == null) {
+ Log.w(TAG, "This NR hasn't been registered. NR id = " + networkRequest.requestId);
+ } else {
+ Log.d(TAG, "Removed nr with ID = " + networkRequest.requestId);
+
+ mNetworkRequestsActive.remove(networkRequest.requestId);
+ networkRequestStateInfo.setNetworkRequestRemoved();
+ mStatsLoggingHandler.sendMessage(
+ Message.obtain(
+ mStatsLoggingHandler,
+ MSG_NETWORK_REQUEST_STATE_CHANGED,
+ networkRequestStateInfo));
+
+ }
+ }
+
+ /** Dependency class */
+ public static class Dependencies {
+ /**
+ * Creates a thread with provided tag.
+ *
+ * @param tag for the thread.
+ */
+ public HandlerThread makeHandlerThread(@NonNull final String tag) {
+ return new HandlerThread(tag);
+ }
+
+ /**
+ * Sleeps the thread for provided intervalMs millis.
+ *
+ * @param intervalMs number of millis for the thread sleep.
+ */
+ public void threadSleep(int intervalMs) {
+ try {
+ Thread.sleep(intervalMs);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Cool down interrupted!", e);
+ }
+ }
+
+ /**
+ * Writes a NETWORK_REQUEST_STATE_CHANGED event to ConnectivityStatsLog.
+ *
+ * @param networkRequestStateInfo NetworkRequestStateInfo containing network request info.
+ */
+ public void writeStats(NetworkRequestStateInfo networkRequestStateInfo) {
+ ConnectivityStatsLog.write(
+ NETWORK_REQUEST_STATE_CHANGED,
+ networkRequestStateInfo.getPackageUid(),
+ networkRequestStateInfo.getTransportTypes(),
+ networkRequestStateInfo.getNetCapabilityNotMetered(),
+ networkRequestStateInfo.getNetCapabilityInternet(),
+ networkRequestStateInfo.getNetworkRequestStateStatsType(),
+ networkRequestStateInfo.getNetworkRequestDurationMillis());
+ }
+ }
+
+ private class StatsLoggingHandler extends Handler {
+ private static final String TAG = "NetworkRequestsStateStatsLoggingHandler";
+ private long mLastLogTime = 0;
+
+ StatsLoggingHandler(Looper looper) {
+ super(looper);
+ }
+
+ private void checkStatsLoggingTimeout() {
+ // Cool down before next execution. Required by atom logging frequency.
+ long now = SystemClock.elapsedRealtime();
+ if (now - mLastLogTime < ATOM_INTERVAL_MS) {
+ mDependencies.threadSleep(ATOM_INTERVAL_MS);
+ }
+ mLastLogTime = now;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ NetworkRequestStateInfo loggingInfo;
+ switch (msg.what) {
+ case MSG_NETWORK_REQUEST_STATE_CHANGED:
+ checkStatsLoggingTimeout();
+ loggingInfo = (NetworkRequestStateInfo) msg.obj;
+ mDependencies.writeStats(loggingInfo);
+ break;
+ default: // fall out
+ }
+ }
+ }
+}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0ec0f13..6b47654 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -281,6 +281,7 @@
import com.android.metrics.NetworkDescription;
import com.android.metrics.NetworkList;
import com.android.metrics.NetworkRequestCount;
+import com.android.metrics.NetworkRequestStateStatsMetrics;
import com.android.metrics.RequestCountForType;
import com.android.modules.utils.BasicShellCommandHandler;
import com.android.modules.utils.build.SdkLevel;
@@ -941,6 +942,8 @@
private final IpConnectivityLog mMetricsLog;
+ private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+
@GuardedBy("mBandwidthRequests")
private final SparseArray<Integer> mBandwidthRequests = new SparseArray<>(10);
@@ -1422,6 +1425,19 @@
}
/**
+ * @see NetworkRequestStateStatsMetrics
+ */
+ public NetworkRequestStateStatsMetrics makeNetworkRequestStateStatsMetrics(
+ Context context) {
+ // We currently have network requests metric for Watch devices only
+ if (context.getPackageManager().hasSystemFeature(FEATURE_WATCH)) {
+ return new NetworkRequestStateStatsMetrics();
+ } else {
+ return null;
+ }
+ }
+
+ /**
* @see BatteryStatsManager
*/
public void reportNetworkInterfaceForTransports(Context context, String iface,
@@ -1654,6 +1670,7 @@
new RequestInfoPerUidCounter(MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - 1);
mMetricsLog = logger;
+ mNetworkRequestStateStatsMetrics = mDeps.makeNetworkRequestStateStatsMetrics(mContext);
final NetworkRequest defaultInternetRequest = createDefaultRequest();
mDefaultRequest = new NetworkRequestInfo(
Process.myUid(), defaultInternetRequest, null,
@@ -5324,6 +5341,8 @@
updateSignalStrengthThresholds(network, "REGISTER", req);
}
}
+ } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(req);
}
}
@@ -5541,6 +5560,8 @@
}
if (req.isListen()) {
removeListenRequestFromNetworks(req);
+ } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(req);
}
}
nri.unlinkDeathRecipient();
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 8036ae9..94ba9de 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -25,9 +25,6 @@
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
-import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
import android.annotation.IntDef;
@@ -90,6 +87,7 @@
*/
public class AutomaticOnOffKeepaliveTracker {
private static final String TAG = "AutomaticOnOffKeepaliveTracker";
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET};
private static final long LOW_TCP_POLLING_INTERVAL_MS = 1_000L;
private static final int ADJUST_TCP_POLLING_DELAY_MS = 2000;
@@ -794,22 +792,18 @@
try {
while (NetlinkUtils.enoughBytesRemainForValidNlMsg(bytes)) {
- final int startPos = bytes.position();
+ // NetlinkMessage.parse() will move the byte buffer position.
+ // TODO: Parse dst address information to filter socket.
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(
+ bytes, OsConstants.NETLINK_INET_DIAG);
+ if (!(nlMsg instanceof InetDiagMessage)) {
+ if (DBG) Log.e(TAG, "Not a SOCK_DIAG_BY_FAMILY msg");
+ return false;
+ }
- final int nlmsgLen = bytes.getInt();
- final int nlmsgType = bytes.getShort();
- if (isEndOfMessageOrError(nlmsgType)) return false;
- // TODO: Parse InetDiagMessage to get uid and dst address information to filter
- // socket via NetlinkMessage.parse.
-
- // Skip the header to move to data part.
- bytes.position(startPos + SOCKDIAG_MSG_HEADER_SIZE);
-
- if (isTargetTcpSocket(bytes, nlmsgLen, networkMark, networkMask)) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- bytes.position(startPos);
- final InetDiagMessage diagMsg = (InetDiagMessage) NetlinkMessage.parse(
- bytes, OsConstants.NETLINK_INET_DIAG);
+ final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
+ if (isTargetTcpSocket(diagMsg, networkMark, networkMask, vpnUidRanges)) {
+ if (DBG) {
Log.d(TAG, String.format("Found open TCP connection by uid %d to %s"
+ " cookie %d",
diagMsg.inetDiagMsg.idiag_uid,
@@ -834,26 +828,31 @@
return false;
}
- private boolean isEndOfMessageOrError(int nlmsgType) {
- return nlmsgType == NLMSG_DONE || nlmsgType != SOCK_DIAG_BY_FAMILY;
+ private static boolean containsUid(Set<Range<Integer>> ranges, int uid) {
+ for (final Range<Integer> range: ranges) {
+ if (range.contains(uid)) {
+ return true;
+ }
+ }
+ return false;
}
- private boolean isTargetTcpSocket(@NonNull ByteBuffer bytes, int nlmsgLen, int networkMark,
- int networkMask) {
- final int mark = readSocketDataAndReturnMark(bytes, nlmsgLen);
+ private boolean isTargetTcpSocket(@NonNull InetDiagMessage diagMsg,
+ int networkMark, int networkMask, @NonNull Set<Range<Integer>> vpnUidRanges) {
+ if (!containsUid(vpnUidRanges, diagMsg.inetDiagMsg.idiag_uid)) return false;
+
+ final int mark = readSocketDataAndReturnMark(diagMsg);
return (mark & networkMask) == networkMark;
}
- private int readSocketDataAndReturnMark(@NonNull ByteBuffer bytes, int nlmsgLen) {
- final int nextMsgOffset = bytes.position() + nlmsgLen - SOCKDIAG_MSG_HEADER_SIZE;
+ private int readSocketDataAndReturnMark(@NonNull InetDiagMessage diagMsg) {
int mark = NetlinkUtils.INIT_MARK_VALUE;
// Get socket mark
- // TODO: Add a parsing method in NetlinkMessage.parse to support this to skip the remaining
- // data.
- while (bytes.position() < nextMsgOffset) {
- final StructNlAttr nlattr = StructNlAttr.parse(bytes);
- if (nlattr != null && nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
- mark = nlattr.getValueAsInteger();
+ for (StructNlAttr attr : diagMsg.nlAttrs) {
+ if (attr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
+ // The netlink attributes should contain only one INET_DIAG_MARK for each socket.
+ mark = attr.getValueAsInteger();
+ break;
}
}
return mark;
diff --git a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
index 3350d2d..742a2cc 100644
--- a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
+++ b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
@@ -171,7 +171,8 @@
}
final ForwardingPair fwp = new ForwardingPair(fromIface, toIface);
if (mForwardedInterfaces.contains(fwp)) {
- throw new IllegalStateException("Forward already exists between ifaces "
+ // TODO: remove if no reports are observed from the below log
+ Log.wtf(TAG, "Forward already exists between ifaces "
+ fromIface + " → " + toIface);
}
mForwardedInterfaces.add(fwp);
diff --git a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
index d538221..497b8cb 100644
--- a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
+++ b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java
@@ -166,6 +166,24 @@
}
/**
+ * Build an ICMPv6 Router Solicitation packet from the required specified parameters without
+ * ethernet header.
+ */
+ public static ByteBuffer buildRsPacket(
+ final Inet6Address srcIp, final Inet6Address dstIp, final ByteBuffer... options) {
+ final RsHeader rsHeader = new RsHeader((int) 0 /* reserved */);
+ final ByteBuffer[] payload =
+ buildIcmpv6Payload(
+ ByteBuffer.wrap(rsHeader.writeToBytes(ByteOrder.BIG_ENDIAN)), options);
+ return buildIcmpv6Packet(
+ srcIp,
+ dstIp,
+ (byte) ICMPV6_ROUTER_SOLICITATION /* type */,
+ (byte) 0 /* code */,
+ payload);
+ }
+
+ /**
* Build an ICMPv6 Echo Request packet from the required specified parameters.
*/
public static ByteBuffer buildEchoRequestPacket(final MacAddress srcMac,
@@ -176,11 +194,21 @@
}
/**
- * Build an ICMPv6 Echo Reply packet without ethernet header.
+ * Build an ICMPv6 Echo Request packet from the required specified parameters without ethernet
+ * header.
*/
- public static ByteBuffer buildEchoReplyPacket(final Inet6Address srcIp,
+ public static ByteBuffer buildEchoRequestPacket(final Inet6Address srcIp,
final Inet6Address dstIp) {
final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
+ return buildIcmpv6Packet(srcIp, dstIp, (byte) ICMPV6_ECHO_REQUEST_TYPE /* type */,
+ (byte) 0 /* code */,
+ payload);
+ }
+
+ /** Build an ICMPv6 Echo Reply packet without ethernet header. */
+ public static ByteBuffer buildEchoReplyPacket(
+ final Inet6Address srcIp, final Inet6Address dstIp) {
+ final ByteBuffer payload = ByteBuffer.allocate(4); // ID and Sequence number may be zero.
return buildIcmpv6Packet(srcIp, dstIp, (byte) ICMPV6_ECHO_REPLY_TYPE /* type */,
(byte) 0 /* code */, payload);
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
index 4f76577..dbd83d0 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -27,6 +27,7 @@
import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DESTROY;
import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
import static com.android.net.module.util.netlink.NetlinkConstants.stringForAddressFamily;
import static com.android.net.module.util.netlink.NetlinkConstants.stringForProtocol;
@@ -59,8 +60,11 @@
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
import java.util.function.Predicate;
/**
@@ -154,7 +158,8 @@
}
public StructInetDiagMsg inetDiagMsg;
-
+ // The netlink attributes.
+ public List<StructNlAttr> nlAttrs = new ArrayList<>();
@VisibleForTesting
public InetDiagMessage(@NonNull StructNlMsgHdr header) {
super(header);
@@ -172,6 +177,16 @@
if (msg.inetDiagMsg == null) {
return null;
}
+ final int payloadLength = header.nlmsg_len - SOCKDIAG_MSG_HEADER_SIZE;
+ final ByteBuffer payload = byteBuffer.slice();
+ while (payload.position() < payloadLength) {
+ final StructNlAttr attr = StructNlAttr.parse(payload);
+ // Stop parsing for truncated or malformed attribute
+ if (attr == null) return null;
+
+ msg.nlAttrs.add(attr);
+ }
+
return msg;
}
@@ -307,9 +322,8 @@
NetlinkUtils.receiveNetlinkAck(fd);
}
- private static void sendNetlinkDumpRequest(FileDescriptor fd, int proto, int states, int family)
- throws InterruptedIOException, ErrnoException {
- final byte[] dumpMsg = InetDiagMessage.inetDiagReqV2(
+ private static byte [] makeNetlinkDumpRequest(int proto, int states, int family) {
+ return InetDiagMessage.inetDiagReqV2(
proto,
null /* id */,
family,
@@ -318,51 +332,29 @@
0 /* pad */,
0 /* idiagExt */,
states);
- NetlinkUtils.sendMessage(fd, dumpMsg, 0, dumpMsg.length, IO_TIMEOUT_MS);
}
- private static int processNetlinkDumpAndDestroySockets(FileDescriptor dumpFd,
+ private static int processNetlinkDumpAndDestroySockets(byte[] dumpReq,
FileDescriptor destroyFd, int proto, Predicate<InetDiagMessage> filter)
- throws InterruptedIOException, ErrnoException {
- int destroyedSockets = 0;
-
- while (true) {
- final ByteBuffer buf = NetlinkUtils.recvMessage(
- dumpFd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
-
- while (buf.remaining() > 0) {
- final int position = buf.position();
- final NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_INET_DIAG);
- 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));
- break;
- }
-
- if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
- return destroyedSockets;
- }
-
- if (!(nlMsg instanceof InetDiagMessage)) {
- Log.wtf(TAG, "Received unexpected netlink message: " + nlMsg);
- continue;
- }
-
- final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
- if (filter.test(diagMsg)) {
- try {
- sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
- destroyedSockets++;
- } catch (InterruptedIOException | ErrnoException e) {
- if (!(e instanceof ErrnoException
- && ((ErrnoException) e).errno == ENOENT)) {
- Log.e(TAG, "Failed to destroy socket: diagMsg=" + diagMsg + ", " + e);
- }
+ throws SocketException, InterruptedIOException, ErrnoException {
+ AtomicInteger destroyedSockets = new AtomicInteger(0);
+ Consumer<InetDiagMessage> handleNlDumpMsg = (diagMsg) -> {
+ if (filter.test(diagMsg)) {
+ try {
+ sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
+ destroyedSockets.getAndIncrement();
+ } catch (InterruptedIOException | ErrnoException e) {
+ if (!(e instanceof ErrnoException
+ && ((ErrnoException) e).errno == ENOENT)) {
+ Log.e(TAG, "Failed to destroy socket: diagMsg=" + diagMsg + ", " + e);
}
}
}
- }
+ };
+
+ NetlinkUtils.<InetDiagMessage>getAndProcessNetlinkDumpMessages(dumpReq,
+ NETLINK_INET_DIAG, InetDiagMessage.class, handleNlDumpMsg);
+ return destroyedSockets.get();
}
/**
@@ -420,31 +412,28 @@
private static void destroySockets(int proto, int states, Predicate<InetDiagMessage> filter)
throws ErrnoException, SocketException, InterruptedIOException {
- FileDescriptor dumpFd = null;
FileDescriptor destroyFd = null;
try {
- dumpFd = NetlinkUtils.createNetLinkInetDiagSocket();
destroyFd = NetlinkUtils.createNetLinkInetDiagSocket();
- connectToKernel(dumpFd);
connectToKernel(destroyFd);
for (int family : List.of(AF_INET, AF_INET6)) {
+ byte[] req = makeNetlinkDumpRequest(proto, states, family);
+
try {
- sendNetlinkDumpRequest(dumpFd, proto, states, family);
- } catch (InterruptedIOException | ErrnoException e) {
- Log.e(TAG, "Failed to send netlink dump request: " + e);
- continue;
- }
- final int destroyedSockets = processNetlinkDumpAndDestroySockets(
- dumpFd, destroyFd, proto, filter);
- Log.d(TAG, "Destroyed " + destroyedSockets + " sockets"
+ final int destroyedSockets = processNetlinkDumpAndDestroySockets(
+ req, destroyFd, proto, filter);
+ Log.d(TAG, "Destroyed " + destroyedSockets + " sockets"
+ ", proto=" + stringForProtocol(proto)
+ ", family=" + stringForAddressFamily(family)
+ ", states=" + states);
+ } catch (SocketException | InterruptedIOException | ErrnoException e) {
+ Log.e(TAG, "Failed to send netlink dump request or receive messages: " + e);
+ continue;
+ }
}
} finally {
- closeSocketQuietly(dumpFd);
closeSocketQuietly(destroyFd);
}
}
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..7c2be2c 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -29,6 +29,11 @@
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.NetlinkConstants.RTNL_FAMILY_IP6MR;
+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.util.SocketUtils;
import android.system.ErrnoException;
@@ -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,139 @@
}
private NetlinkUtils() {}
+
+ private static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessagesWithFd(
+ FileDescriptor fd, byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
+ Consumer<T> func)
+ throws SocketException, InterruptedIOException, ErrnoException {
+ // connecToKernel throws ErrnoException and SocketException, should be handled by caller
+ connectToKernel(fd);
+
+ // sendMessage throws InterruptedIOException and ErrnoException,
+ // should be handled by caller
+ sendMessage(fd, dumpRequestMessage, 0, dumpRequestMessage.length, IO_TIMEOUT_MS);
+
+ while (true) {
+ // recvMessage throws ErrnoException, InterruptedIOException
+ // should be handled by caller
+ 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));
+ break;
+ }
+
+ if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
+ return;
+ }
+
+ if (!msgClass.isInstance(nlMsg)) {
+ Log.wtf(TAG, "Received unexpected netlink message: " + nlMsg);
+ continue;
+ }
+
+ final T msg = (T) nlMsg;
+ func.accept(msg);
+ }
+ }
+ }
+ /**
+ * 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 connect socket to kernel
+ * @throws InterruptedIOException when fails to read the dumpFd
+ * @throws ErrnoException when fails to create dump fd, send dump request
+ * or receive messages
+ */
+ public static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessages(
+ byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
+ Consumer<T> func)
+ throws SocketException, InterruptedIOException, ErrnoException {
+ // Create socket
+ final FileDescriptor fd = netlinkSocketForProto(nlFamily);
+ try {
+ getAndProcessNetlinkDumpMessagesWithFd(fd, dumpRequestMessage, nlFamily,
+ msgClass, func);
+ } finally {
+ closeSocketQuietly(fd);
+ }
+ }
+
+ /**
+ * Construct a RTM_GETROUTE message for dumping multicast IPv6 routes from kernel.
+ */
+ private static byte[] newIpv6MulticastRouteDumpRequest() {
+ final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+ nlmsghdr.nlmsg_type = NetlinkConstants.RTM_GETROUTE;
+ nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+ final short shortZero = 0;
+
+ // family must be RTNL_FAMILY_IP6MR to dump IPv6 multicast routes.
+ // dstLen, srcLen, tos and scope must be zero in FIB dump request.
+ // protocol, flags must be 0, and type must be RTN_MULTICAST (if not 0) for multicast
+ // dump request.
+ // table or RTA_TABLE attributes can be used to dump a specific routing table.
+ // RTA_OIF attribute can be used to dump only routes containing given oif.
+ // Here no attributes are set so the kernel can return all multicast routes.
+ final StructRtMsg rtMsg =
+ new StructRtMsg(RTNL_FAMILY_IP6MR /* family */, shortZero /* dstLen */,
+ shortZero /* srcLen */, shortZero /* tos */, shortZero /* table */,
+ shortZero /* protocol */, shortZero /* scope */, shortZero /* type */,
+ 0L /* flags */);
+ final RtNetlinkRouteMessage msg =
+ new RtNetlinkRouteMessage(nlmsghdr, rtMsg);
+
+ final int spaceRequired = StructNlMsgHdr.STRUCT_SIZE + StructRtMsg.STRUCT_SIZE;
+ nlmsghdr.nlmsg_len = spaceRequired;
+ final byte[] bytes = new byte[NetlinkConstants.alignedLengthOf(spaceRequired)];
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+ byteBuffer.order(ByteOrder.nativeOrder());
+ msg.pack(byteBuffer);
+ return bytes;
+ }
+
+ /**
+ * Get the list of IPv6 multicast route messages from kernel.
+ */
+ public static List<RtNetlinkRouteMessage> getIpv6MulticastRoutes() {
+ final byte[] dumpMsg = newIpv6MulticastRouteDumpRequest();
+ List<RtNetlinkRouteMessage> routes = new ArrayList<>();
+ Consumer<RtNetlinkRouteMessage> handleNlDumpMsg = (msg) -> {
+ if (msg.getRtmFamily() == RTNL_FAMILY_IP6MR) {
+ // Sent rtmFamily RTNL_FAMILY_IP6MR in dump request to make sure ipv6
+ // multicast routes are included in netlink reply messages, the kernel
+ // may also reply with other kind of routes, so we filter them out here.
+ routes.add(msg);
+ }
+ };
+ try {
+ NetlinkUtils.<RtNetlinkRouteMessage>getAndProcessNetlinkDumpMessages(
+ dumpMsg, NETLINK_ROUTE, RtNetlinkRouteMessage.class,
+ handleNlDumpMsg);
+ } catch (SocketException | InterruptedIOException | ErrnoException e) {
+ Log.e(TAG, "Failed to dump multicast routes");
+ return routes;
+ }
+
+ return routes;
+ }
+
+ 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/structs/StructMf6cctl.java b/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java
new file mode 100644
index 0000000..24e0a97
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java
@@ -0,0 +1,99 @@
+/*
+ * 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.net.module.util.structs;
+
+import static android.system.OsConstants.AF_INET6;
+
+import com.android.net.module.util.Struct;
+import java.net.Inet6Address;
+import java.util.Set;
+
+/*
+ * Implements the mf6cctl structure which is used to add a multicast forwarding
+ * cache, see /usr/include/linux/mroute6.h
+ */
+public class StructMf6cctl extends Struct {
+ // struct sockaddr_in6 mf6cc_origin, added the fields directly as Struct
+ // doesn't support nested Structs
+ @Field(order = 0, type = Type.U16)
+ public final int originFamily; // AF_INET6
+ @Field(order = 1, type = Type.U16)
+ public final int originPort; // Transport layer port # of origin
+ @Field(order = 2, type = Type.U32)
+ public final long originFlowinfo; // IPv6 flow information
+ @Field(order = 3, type = Type.ByteArray, arraysize = 16)
+ public final byte[] originAddress; //the IPv6 address of origin
+ @Field(order = 4, type = Type.U32)
+ public final long originScopeId; // scope id, not used
+
+ // struct sockaddr_in6 mf6cc_mcastgrp
+ @Field(order = 5, type = Type.U16)
+ public final int groupFamily; // AF_INET6
+ @Field(order = 6, type = Type.U16)
+ public final int groupPort; // Transport layer port # of multicast group
+ @Field(order = 7, type = Type.U32)
+ public final long groupFlowinfo; // IPv6 flow information
+ @Field(order = 8, type = Type.ByteArray, arraysize = 16)
+ public final byte[] groupAddress; //the IPv6 address of multicast group
+ @Field(order = 9, type = Type.U32)
+ public final long groupScopeId; // scope id, not used
+
+ @Field(order = 10, type = Type.U16, padding = 2)
+ public final int mf6ccParent; // incoming interface
+ @Field(order = 11, type = Type.ByteArray, arraysize = 32)
+ public final byte[] mf6ccIfset; // outgoing interfaces
+
+ public StructMf6cctl(final Inet6Address origin, final Inet6Address group,
+ final int mf6ccParent, final Set<Integer> oifset) {
+ this(AF_INET6, 0, (long) 0, origin.getAddress(), (long) 0, AF_INET6,
+ 0, (long) 0, group.getAddress(), (long) 0, mf6ccParent,
+ getMf6ccIfsetBytes(oifset));
+ }
+
+ private StructMf6cctl(int originFamily, int originPort, long originFlowinfo,
+ byte[] originAddress, long originScopeId, int groupFamily, int groupPort,
+ long groupFlowinfo, byte[] groupAddress, long groupScopeId, int mf6ccParent,
+ byte[] mf6ccIfset) {
+ this.originFamily = originFamily;
+ this.originPort = originPort;
+ this.originFlowinfo = originFlowinfo;
+ this.originAddress = originAddress;
+ this.originScopeId = originScopeId;
+ this.groupFamily = groupFamily;
+ this.groupPort = groupPort;
+ this.groupFlowinfo = groupFlowinfo;
+ this.groupAddress = groupAddress;
+ this.groupScopeId = groupScopeId;
+ this.mf6ccParent = mf6ccParent;
+ this.mf6ccIfset = mf6ccIfset;
+ }
+
+ private static byte[] getMf6ccIfsetBytes(final Set<Integer> oifs)
+ throws IllegalArgumentException {
+ byte[] mf6ccIfset = new byte[32];
+ for (int oif : oifs) {
+ int idx = oif / 8;
+ if (idx >= 32) {
+ // invalid oif index, too big to fit in mf6ccIfset
+ throw new IllegalArgumentException("Invalid oif index" + oif);
+ }
+ int offset = oif % 8;
+ mf6ccIfset[idx] |= (byte) (1 << offset);
+ }
+ return mf6ccIfset;
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java b/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java
new file mode 100644
index 0000000..626a170
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMif6ctl.java
@@ -0,0 +1,46 @@
+/*
+ * 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.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+
+/*
+ * Implements the mif6ctl structure which is used to add a multicast routing
+ * interface, see /usr/include/linux/mroute6.h
+ */
+public class StructMif6ctl extends Struct {
+ @Field(order = 0, type = Type.U16)
+ public final int mif6cMifi; // Index of MIF
+ @Field(order = 1, type = Type.U8)
+ public final short mif6cFlags; // MIFF_ flags
+ @Field(order = 2, type = Type.U8)
+ public final short vifcThreshold; // ttl limit
+ @Field(order = 3, type = Type.U16)
+ public final int mif6cPifi; //the index of the physical IF
+ @Field(order = 4, type = Type.U32, padding = 2)
+ public final long vifcRateLimit; // Rate limiter values (NI)
+
+ public StructMif6ctl(final int mif6cMifi, final short mif6cFlags, final short vifcThreshold,
+ final int mif6cPifi, final long vifcRateLimit) {
+ this.mif6cMifi = mif6cMifi;
+ this.mif6cFlags = mif6cFlags;
+ this.vifcThreshold = vifcThreshold;
+ this.mif6cPifi = mif6cPifi;
+ this.vifcRateLimit = vifcRateLimit;
+ }
+}
+
diff --git a/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java b/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java
new file mode 100644
index 0000000..569e361
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/StructMrt6Msg.java
@@ -0,0 +1,52 @@
+/*
+ * 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.net.module.util.structs;
+
+import com.android.net.module.util.Struct;
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class StructMrt6Msg extends Struct {
+ public static final byte MRT6MSG_NOCACHE = 1;
+
+ @Field(order = 0, type = Type.S8)
+ public final byte mbz;
+ @Field(order = 1, type = Type.S8)
+ public final byte msgType; // message type
+ @Field(order = 2, type = Type.U16, padding = 4)
+ public final int mif; // mif received on
+ @Field(order = 3, type = Type.Ipv6Address)
+ public final Inet6Address src;
+ @Field(order = 4, type = Type.Ipv6Address)
+ public final Inet6Address dst;
+
+ public StructMrt6Msg(final byte mbz, final byte msgType, final int mif,
+ final Inet6Address source, final Inet6Address destination) {
+ this.mbz = mbz; // kernel should set it to 0
+ this.msgType = msgType;
+ this.mif = mif;
+ this.src = source;
+ this.dst = destination;
+ }
+
+ public static StructMrt6Msg parse(ByteBuffer byteBuffer) {
+ byteBuffer.order(ByteOrder.nativeOrder());
+ return Struct.parse(StructMrt6Msg.class, byteBuffer);
+ }
+}
+
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 637a938..2b7e620 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -241,5 +241,17 @@
min_sdk_version: "30",
},
},
- versions: ["1"],
+ versions_with_info: [
+ {
+ version: "1",
+ imports: [],
+ },
+ {
+ version: "2",
+ imports: [],
+ },
+
+ ],
+ frozen: true,
+
}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash
new file mode 100644
index 0000000..785d42d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/.hash
@@ -0,0 +1 @@
+0e5d9ad0664b8b3ec9d323534c42333cf6f6ed3d
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl
new file mode 100644
index 0000000..d31a327
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/DiscoveryInfo.aidl
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable DiscoveryInfo {
+ int id;
+ int result;
+ @utf8InCpp String serviceName;
+ @utf8InCpp String registrationType;
+ @utf8InCpp String domainName;
+ int interfaceIdx;
+ int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl
new file mode 100644
index 0000000..2049274
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/GetAddressInfo.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable GetAddressInfo {
+ int id;
+ int result;
+ @utf8InCpp String hostname;
+ @utf8InCpp String address;
+ int interfaceIdx;
+ int netId;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl
new file mode 100644
index 0000000..d84742b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDns.aidl
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+interface IMDns {
+ /**
+ * @deprecated unimplemented on V+.
+ */
+ void startDaemon();
+ /**
+ * @deprecated unimplemented on V+.
+ */
+ void stopDaemon();
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void registerService(in android.net.mdns.aidl.RegistrationInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void discover(in android.net.mdns.aidl.DiscoveryInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void resolve(in android.net.mdns.aidl.ResolutionInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void getServiceAddress(in android.net.mdns.aidl.GetAddressInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void stopOperation(int id);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void registerEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+ /**
+ * @deprecated unimplemented on U+.
+ */
+ void unregisterEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl
new file mode 100644
index 0000000..187a3d2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -0,0 +1,62 @@
+/**
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+interface IMDnsEventListener {
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onServiceRegistrationStatus(in android.net.mdns.aidl.RegistrationInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onServiceDiscoveryStatus(in android.net.mdns.aidl.DiscoveryInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onServiceResolutionStatus(in android.net.mdns.aidl.ResolutionInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
+ oneway void onGettingServiceAddressStatus(in android.net.mdns.aidl.GetAddressInfo status);
+ const int SERVICE_DISCOVERY_FAILED = 602;
+ const int SERVICE_FOUND = 603;
+ const int SERVICE_LOST = 604;
+ const int SERVICE_REGISTRATION_FAILED = 605;
+ const int SERVICE_REGISTERED = 606;
+ const int SERVICE_RESOLUTION_FAILED = 607;
+ const int SERVICE_RESOLVED = 608;
+ const int SERVICE_GET_ADDR_FAILED = 611;
+ const int SERVICE_GET_ADDR_SUCCESS = 612;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl
new file mode 100644
index 0000000..185111b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/RegistrationInfo.aidl
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable RegistrationInfo {
+ int id;
+ int result;
+ @utf8InCpp String serviceName;
+ @utf8InCpp String registrationType;
+ int port;
+ byte[] txtRecord;
+ int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl
new file mode 100644
index 0000000..4aa7d79
--- /dev/null
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/2/android/net/mdns/aidl/ResolutionInfo.aidl
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.mdns.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable ResolutionInfo {
+ int id;
+ int result;
+ @utf8InCpp String serviceName;
+ @utf8InCpp String registrationType;
+ @utf8InCpp String domain;
+ @utf8InCpp String serviceFullName;
+ @utf8InCpp String hostname;
+ int port;
+ byte[] txtRecord;
+ int interfaceIdx;
+}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
index ecbe966..d84742b 100644
--- a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDns.aidl
@@ -34,13 +34,40 @@
package android.net.mdns.aidl;
/* @hide */
interface IMDns {
+ /**
+ * @deprecated unimplemented on V+.
+ */
void startDaemon();
+ /**
+ * @deprecated unimplemented on V+.
+ */
void stopDaemon();
+ /**
+ * @deprecated unimplemented on U+.
+ */
void registerService(in android.net.mdns.aidl.RegistrationInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void discover(in android.net.mdns.aidl.DiscoveryInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void resolve(in android.net.mdns.aidl.ResolutionInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void getServiceAddress(in android.net.mdns.aidl.GetAddressInfo info);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void stopOperation(int id);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void registerEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
+ /**
+ * @deprecated unimplemented on U+.
+ */
void unregisterEventListener(in android.net.mdns.aidl.IMDnsEventListener listener);
}
diff --git a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
index 4625cac..187a3d2 100644
--- a/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
+++ b/staticlibs/netd/aidl_api/mdns_aidl_interface/current/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -34,9 +34,21 @@
package android.net.mdns.aidl;
/* @hide */
interface IMDnsEventListener {
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onServiceRegistrationStatus(in android.net.mdns.aidl.RegistrationInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onServiceDiscoveryStatus(in android.net.mdns.aidl.DiscoveryInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onServiceResolutionStatus(in android.net.mdns.aidl.ResolutionInfo status);
+ /**
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
+ */
oneway void onGettingServiceAddressStatus(in android.net.mdns.aidl.GetAddressInfo status);
const int SERVICE_DISCOVERY_FAILED = 602;
const int SERVICE_FOUND = 603;
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
index 255d70f..3bf1da8 100644
--- a/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/IMDns.aidl
@@ -28,6 +28,8 @@
* Start the MDNSResponder daemon.
*
* @throws ServiceSpecificException with unix errno EALREADY if daemon is already running.
+ * @throws UnsupportedOperationException on Android V and after.
+ * @deprecated unimplemented on V+.
*/
void startDaemon();
@@ -35,6 +37,8 @@
* Stop the MDNSResponder daemon.
*
* @throws ServiceSpecificException with unix errno EBUSY if daemon is still in use.
+ * @throws UnsupportedOperationException on Android V and after.
+ * @deprecated unimplemented on V+.
*/
void stopDaemon();
@@ -49,6 +53,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if registration fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void registerService(in RegistrationInfo info);
@@ -63,6 +69,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if discovery fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void discover(in DiscoveryInfo info);
@@ -77,6 +85,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if resolution fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void resolve(in ResolutionInfo info);
@@ -92,6 +102,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EBUSY if request id is already in use.
* - kDNSServiceErr_* list in dns_sd.h if getting address fail.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void getServiceAddress(in GetAddressInfo info);
@@ -101,6 +113,8 @@
* @param id the operation id to be stopped.
*
* @throws ServiceSpecificException with unix errno ESRCH if request id is not in use.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void stopOperation(int id);
@@ -112,6 +126,8 @@
* @throws ServiceSpecificException with one of the following error values:
* - Unix errno EINVAL if listener is null.
* - Unix errno EEXIST if register duplicated listener.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void registerEventListener(in IMDnsEventListener listener);
@@ -121,6 +137,8 @@
* @param listener The listener to be unregistered.
*
* @throws ServiceSpecificException with unix errno EINVAL if listener is null.
+ * @throws UnsupportedOperationException on Android U and after.
+ * @deprecated unimplemented on U+.
*/
void unregisterEventListener(in IMDnsEventListener listener);
}
diff --git a/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl b/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
index a202a26..f7f028b 100644
--- a/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
+++ b/staticlibs/netd/binder/android/net/mdns/aidl/IMDnsEventListener.aidl
@@ -31,8 +31,8 @@
oneway interface IMDnsEventListener {
/**
* Types for MDNS operation result.
- * These are in sync with frameworks/libs/net/common/netd/libnetdutils/include/netdutils/\
- * ResponseCode.h
+ * These are in sync with packages/modules/Connectivity/staticlibs/netd/libnetdutils/include/\
+ * netdutils/ResponseCode.h
*/
const int SERVICE_DISCOVERY_FAILED = 602;
const int SERVICE_FOUND = 603;
@@ -46,21 +46,29 @@
/**
* Notify service registration status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onServiceRegistrationStatus(in RegistrationInfo status);
/**
* Notify service discovery status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onServiceDiscoveryStatus(in DiscoveryInfo status);
/**
* Notify service resolution status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onServiceResolutionStatus(in ResolutionInfo status);
/**
* Notify getting service address status.
+ *
+ * @deprecated this is implemented for backward compatibility. Don't use it in new code.
*/
void onGettingServiceAddressStatus(in GetAddressInfo status);
}
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/InetDiagSocketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
index 65e99f8..b44e428 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
@@ -32,6 +32,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -345,28 +346,28 @@
// Hexadecimal representation of InetDiagMessage
private static final String INET_DIAG_MSG_HEX1 =
// struct nlmsghdr
- "58000000" + // length = 88
- "1400" + // type = SOCK_DIAG_BY_FAMILY
- "0200" + // flags = NLM_F_MULTI
- "00000000" + // seqno
- "f5220000" + // pid
+ "58000000" // length = 88
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
// struct inet_diag_msg
- "0a" + // family = AF_INET6
- "01" + // idiag_state = 1
- "02" + // idiag_timer = 2
- "ff" + // idiag_retrans = 255
+ + "0a" // family = AF_INET6
+ + "01" // idiag_state = 1
+ + "02" // idiag_timer = 2
+ + "ff" // idiag_retrans = 255
// inet_diag_sockid
- "a817" + // idiag_sport = 43031
- "960f" + // idiag_dport = 38415
- "20010db8000000000000000000000001" + // idiag_src = 2001:db8::1
- "20010db8000000000000000000000002" + // idiag_dst = 2001:db8::2
- "07000000" + // idiag_if = 7
- "5800000000000000" + // idiag_cookie = 88
- "04000000" + // idiag_expires = 4
- "05000000" + // idiag_rqueue = 5
- "06000000" + // idiag_wqueue = 6
- "a3270000" + // idiag_uid = 10147
- "a57e19f0"; // idiag_inode = 4028202661
+ + "a817" // idiag_sport = 43031
+ + "960f" // idiag_dport = 38415
+ + "20010db8000000000000000000000001" // idiag_src = 2001:db8::1
+ + "20010db8000000000000000000000002" // idiag_dst = 2001:db8::2
+ + "07000000" // idiag_if = 7
+ + "5800000000000000" // idiag_cookie = 88
+ + "04000000" // idiag_expires = 4
+ + "05000000" // idiag_rqueue = 5
+ + "06000000" // idiag_wqueue = 6
+ + "a3270000" // idiag_uid = 10147
+ + "a57e19f0"; // idiag_inode = 4028202661
private void assertInetDiagMsg1(final NetlinkMessage msg) {
assertNotNull(msg);
@@ -394,33 +395,45 @@
assertEquals(6, inetDiagMsg.inetDiagMsg.idiag_wqueue);
assertEquals(10147, inetDiagMsg.inetDiagMsg.idiag_uid);
assertEquals(4028202661L, inetDiagMsg.inetDiagMsg.idiag_inode);
+
+ // Verify the length of attribute list is 0 as expected since message doesn't
+ // take any attributes
+ assertEquals(0, inetDiagMsg.nlAttrs.size());
}
// Hexadecimal representation of InetDiagMessage
private static final String INET_DIAG_MSG_HEX2 =
// struct nlmsghdr
- "58000000" + // length = 88
- "1400" + // type = SOCK_DIAG_BY_FAMILY
- "0200" + // flags = NLM_F_MULTI
- "00000000" + // seqno
- "f5220000" + // pid
+ "6C000000" // length = 108
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
// struct inet_diag_msg
- "0a" + // family = AF_INET6
- "02" + // idiag_state = 2
- "10" + // idiag_timer = 16
- "20" + // idiag_retrans = 32
+ + "0a" // family = AF_INET6
+ + "02" // idiag_state = 2
+ + "10" // idiag_timer = 16
+ + "20" // idiag_retrans = 32
// inet_diag_sockid
- "a845" + // idiag_sport = 43077
- "01bb" + // idiag_dport = 443
- "20010db8000000000000000000000003" + // idiag_src = 2001:db8::3
- "20010db8000000000000000000000004" + // idiag_dst = 2001:db8::4
- "08000000" + // idiag_if = 8
- "6300000000000000" + // idiag_cookie = 99
- "30000000" + // idiag_expires = 48
- "40000000" + // idiag_rqueue = 64
- "50000000" + // idiag_wqueue = 80
- "39300000" + // idiag_uid = 12345
- "851a0000"; // idiag_inode = 6789
+ + "a845" // idiag_sport = 43077
+ + "01bb" // idiag_dport = 443
+ + "20010db8000000000000000000000003" // idiag_src = 2001:db8::3
+ + "20010db8000000000000000000000004" // idiag_dst = 2001:db8::4
+ + "08000000" // idiag_if = 8
+ + "6300000000000000" // idiag_cookie = 99
+ + "30000000" // idiag_expires = 48
+ + "40000000" // idiag_rqueue = 64
+ + "50000000" // idiag_wqueue = 80
+ + "39300000" // idiag_uid = 12345
+ + "851a0000" // idiag_inode = 6789
+ + "0500" // len = 5
+ + "0800" // type = 8
+ + "00000000" // data
+ + "0800" // len = 8
+ + "0F00" // type = 15(INET_DIAG_MARK)
+ + "850A0C00" // data, socket mark=789125
+ + "0400" // len = 4
+ + "0200"; // type = 2
private void assertInetDiagMsg2(final NetlinkMessage msg) {
assertNotNull(msg);
@@ -448,6 +461,104 @@
assertEquals(80, inetDiagMsg.inetDiagMsg.idiag_wqueue);
assertEquals(12345, inetDiagMsg.inetDiagMsg.idiag_uid);
assertEquals(6789, inetDiagMsg.inetDiagMsg.idiag_inode);
+
+ // Verify the number of nlAttr and their content.
+ assertEquals(3, inetDiagMsg.nlAttrs.size());
+
+ assertEquals(5, inetDiagMsg.nlAttrs.get(0).nla_len);
+ assertEquals(8, inetDiagMsg.nlAttrs.get(0).nla_type);
+ assertArrayEquals(
+ HexEncoding.decode("00".toCharArray(), false),
+ inetDiagMsg.nlAttrs.get(0).nla_value);
+ assertEquals(8, inetDiagMsg.nlAttrs.get(1).nla_len);
+ assertEquals(15, inetDiagMsg.nlAttrs.get(1).nla_type);
+ assertArrayEquals(
+ HexEncoding.decode("850A0C00".toCharArray(), false),
+ inetDiagMsg.nlAttrs.get(1).nla_value);
+ assertEquals(4, inetDiagMsg.nlAttrs.get(2).nla_len);
+ assertEquals(2, inetDiagMsg.nlAttrs.get(2).nla_type);
+ assertNull(inetDiagMsg.nlAttrs.get(2).nla_value);
+ }
+
+ // Hexadecimal representation of InetDiagMessage
+ private static final String INET_DIAG_MSG_HEX_MALFORMED =
+ // struct nlmsghdr
+ "6E000000" // length = 110
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
+ // struct inet_diag_msg
+ + "0a" // family = AF_INET6
+ + "02" // idiag_state = 2
+ + "10" // idiag_timer = 16
+ + "20" // idiag_retrans = 32
+ // inet_diag_sockid
+ + "a845" // idiag_sport = 43077
+ + "01bb" // idiag_dport = 443
+ + "20010db8000000000000000000000005" // idiag_src = 2001:db8::5
+ + "20010db8000000000000000000000006" // idiag_dst = 2001:db8::6
+ + "08000000" // idiag_if = 8
+ + "6300000000000000" // idiag_cookie = 99
+ + "30000000" // idiag_expires = 48
+ + "40000000" // idiag_rqueue = 64
+ + "50000000" // idiag_wqueue = 80
+ + "39300000" // idiag_uid = 12345
+ + "851a0000" // idiag_inode = 6789
+ + "0500" // len = 5
+ + "0800" // type = 8
+ + "00000000" // data
+ + "0800" // len = 8
+ + "0F00" // type = 15(INET_DIAG_MARK)
+ + "850A0C00" // data, socket mark=789125
+ + "0400" // len = 4
+ + "0200" // type = 2
+ + "0100" // len = 1, malformed value
+ + "0100"; // type = 1
+
+ @Test
+ public void testParseInetDiagResponseMalformedNlAttr() throws Exception {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(
+ HexEncoding.decode((INET_DIAG_MSG_HEX_MALFORMED).toCharArray(), false));
+ byteBuffer.order(ByteOrder.nativeOrder());
+ assertNull(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+ }
+
+ // Hexadecimal representation of InetDiagMessage
+ private static final String INET_DIAG_MSG_HEX_TRUNCATED =
+ // struct nlmsghdr
+ "5E000000" // length = 96
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0200" // flags = NLM_F_MULTI
+ + "00000000" // seqno
+ + "f5220000" // pid
+ // struct inet_diag_msg
+ + "0a" // family = AF_INET6
+ + "02" // idiag_state = 2
+ + "10" // idiag_timer = 16
+ + "20" // idiag_retrans = 32
+ // inet_diag_sockid
+ + "a845" // idiag_sport = 43077
+ + "01bb" // idiag_dport = 443
+ + "20010db8000000000000000000000005" // idiag_src = 2001:db8::5
+ + "20010db8000000000000000000000006" // idiag_dst = 2001:db8::6
+ + "08000000" // idiag_if = 8
+ + "6300000000000000" // idiag_cookie = 99
+ + "30000000" // idiag_expires = 48
+ + "40000000" // idiag_rqueue = 64
+ + "50000000" // idiag_wqueue = 80
+ + "39300000" // idiag_uid = 12345
+ + "851a0000" // idiag_inode = 6789
+ + "0800" // len = 8
+ + "0100" // type = 1
+ + "000000"; // data, less than the expected length
+
+ @Test
+ public void testParseInetDiagResponseTruncatedNlAttr() throws Exception {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(
+ HexEncoding.decode((INET_DIAG_MSG_HEX_TRUNCATED).toCharArray(), false));
+ byteBuffer.order(ByteOrder.nativeOrder());
+ assertNull(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
}
private static final byte[] INET_DIAG_MSG_BYTES =
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..0958f11 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
@@ -21,7 +21,7 @@
import static android.system.OsConstants.AF_UNSPEC;
import static android.system.OsConstants.EACCES;
import static android.system.OsConstants.NETLINK_ROUTE;
-
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
@@ -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. */
@@ -228,4 +191,17 @@
return bytes;
}
+
+ @Test
+ public void testGetIpv6MulticastRoutes_doesNotThrow() {
+ var multicastRoutes = NetlinkUtils.getIpv6MulticastRoutes();
+
+ for (var route : multicastRoutes) {
+ assertNotNull(route);
+ assertEquals("Route is not IP6MR: " + route,
+ RTNL_FAMILY_IP6MR, route.getRtmFamily());
+ assertNotNull("Route doesn't contain source: " + route, route.getSource());
+ assertNotNull("Route doesn't contain destination: " + route, route.getDestination());
+ }
+ }
}
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/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java
new file mode 100644
index 0000000..a83fc36
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java
@@ -0,0 +1,102 @@
+package com.android.net.module.util.structs;
+
+import static android.system.OsConstants.AF_INET6;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import android.util.ArraySet;
+import androidx.test.runner.AndroidJUnit4;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMf6cctlTest {
+ private static final byte[] MSG_BYTES = new byte[] {
+ 10, 0, /* AF_INET6 */
+ 0, 0, /* originPort */
+ 0, 0, 0, 0, /* originFlowinfo */
+ 32, 1, 13, -72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* originAddress */
+ 0, 0, 0, 0, /* originScopeId */
+ 10, 0, /* AF_INET6 */
+ 0, 0, /* groupPort */
+ 0, 0, 0, 0, /* groupFlowinfo*/
+ -1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 52, /*groupAddress*/
+ 0, 0, 0, 0, /* groupScopeId*/
+ 1, 0, /* mf6ccParent */
+ 0, 0, /* padding */
+ 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 /* mf6ccIfset */
+ };
+
+ private static final int OIF = 10;
+ private static final byte[] OIFSET_BYTES = new byte[] {
+ 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+ };
+
+ private static final Inet6Address SOURCE =
+ (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
+ private static final Inet6Address DESTINATION =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+
+ @Test
+ public void testConstructor() {
+ final Set<Integer> oifset = new ArraySet<>();
+ oifset.add(OIF);
+
+ StructMf6cctl mf6cctl = new StructMf6cctl(SOURCE, DESTINATION,
+ 1 /* mf6ccParent */, oifset);
+
+ assertTrue(Arrays.equals(SOURCE.getAddress(), mf6cctl.originAddress));
+ assertTrue(Arrays.equals(DESTINATION.getAddress(), mf6cctl.groupAddress));
+ assertEquals(1, mf6cctl.mf6ccParent);
+ assertArrayEquals(OIFSET_BYTES, mf6cctl.mf6ccIfset);
+ }
+
+ @Test
+ public void testConstructor_tooBigOifIndex_throwsIllegalArgumentException()
+ throws UnknownHostException {
+ final Set<Integer> oifset = new ArraySet<>();
+ oifset.add(1000);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> new StructMf6cctl(SOURCE, DESTINATION, 1, oifset));
+ }
+
+ @Test
+ public void testParseMf6cctl() {
+ final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+ buf.order(ByteOrder.nativeOrder());
+ StructMf6cctl mf6cctl = StructMf6cctl.parse(StructMf6cctl.class, buf);
+
+ assertEquals(AF_INET6, mf6cctl.originFamily);
+ assertEquals(AF_INET6, mf6cctl.groupFamily);
+ assertArrayEquals(SOURCE.getAddress(), mf6cctl.originAddress);
+ assertArrayEquals(DESTINATION.getAddress(), mf6cctl.groupAddress);
+ assertEquals(1, mf6cctl.mf6ccParent);
+ assertArrayEquals("mf6ccIfset = " + Arrays.toString(mf6cctl.mf6ccIfset),
+ OIFSET_BYTES, mf6cctl.mf6ccIfset);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ final Set<Integer> oifset = new ArraySet<>();
+ oifset.add(OIF);
+
+ StructMf6cctl mf6cctl = new StructMf6cctl(SOURCE, DESTINATION,
+ 1 /* mf6ccParent */, oifset);
+ byte[] bytes = mf6cctl.writeToBytes();
+
+ assertArrayEquals("bytes = " + Arrays.toString(bytes), MSG_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java
new file mode 100644
index 0000000..75196e4
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMif6ctlTest.java
@@ -0,0 +1,70 @@
+package com.android.net.module.util.structs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.util.ArraySet;
+import androidx.test.runner.AndroidJUnit4;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMif6ctlTest {
+ private static final byte[] MSG_BYTES = new byte[] {
+ 1, 0, /* mif6cMifi */
+ 0, /* mif6cFlags */
+ 1, /* vifcThreshold*/
+ 20, 0, /* mif6cPifi */
+ 0, 0, 0, 0, /* vifcRateLimit */
+ 0, 0 /* padding */
+ };
+
+ @Test
+ public void testConstructor() {
+ StructMif6ctl mif6ctl = new StructMif6ctl(10 /* mif6cMifi */,
+ (short) 11 /* mif6cFlags */,
+ (short) 12 /* vifcThreshold */,
+ 13 /* mif6cPifi */,
+ 14L /* vifcRateLimit */);
+
+ assertEquals(10, mif6ctl.mif6cMifi);
+ assertEquals(11, mif6ctl.mif6cFlags);
+ assertEquals(12, mif6ctl.vifcThreshold);
+ assertEquals(13, mif6ctl.mif6cPifi);
+ assertEquals(14, mif6ctl.vifcRateLimit);
+ }
+
+ @Test
+ public void testParseMif6ctl() {
+ final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+ buf.order(ByteOrder.nativeOrder());
+ StructMif6ctl mif6ctl = StructMif6ctl.parse(StructMif6ctl.class, buf);
+
+ assertEquals(1, mif6ctl.mif6cMifi);
+ assertEquals(0, mif6ctl.mif6cFlags);
+ assertEquals(1, mif6ctl.vifcThreshold);
+ assertEquals(20, mif6ctl.mif6cPifi);
+ assertEquals(0, mif6ctl.vifcRateLimit);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ StructMif6ctl mif6ctl = new StructMif6ctl(1 /* mif6cMifi */,
+ (short) 0 /* mif6cFlags */,
+ (short) 1 /* vifcThreshold */,
+ 20 /* mif6cPifi */,
+ (long) 0 /* vifcRateLimit */);
+
+ byte[] bytes = mif6ctl.writeToBytes();
+
+ assertArrayEquals("bytes = " + Arrays.toString(bytes), MSG_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java
new file mode 100644
index 0000000..f1b75a0
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMrt6MsgTest.java
@@ -0,0 +1,58 @@
+package com.android.net.module.util.structs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.Struct;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StructMrt6MsgTest {
+
+ private static final byte[] MSG_BYTES = new byte[] {
+ 0, /* mbz = 0 */
+ 1, /* message type = MRT6MSG_NOCACHE */
+ 1, 0, /* mif u16 = 1 */
+ 0, 0, 0, 0, /* padding */
+ 32, 1, 13, -72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* source=2001:db8::1 */
+ -1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 52, /* destination=ff05::1234 */
+ };
+
+ private static final Inet6Address SOURCE =
+ (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
+ private static final Inet6Address GROUP =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
+
+ @Test
+ public void testParseMrt6Msg() {
+ final ByteBuffer buf = ByteBuffer.wrap(MSG_BYTES);
+ StructMrt6Msg mrt6Msg = StructMrt6Msg.parse(buf);
+
+ assertEquals(1, mrt6Msg.mif);
+ assertEquals(StructMrt6Msg.MRT6MSG_NOCACHE, mrt6Msg.msgType);
+ assertEquals(SOURCE, mrt6Msg.src);
+ assertEquals(GROUP, mrt6Msg.dst);
+ }
+
+ @Test
+ public void testWriteToBytes() {
+ StructMrt6Msg msg = new StructMrt6Msg((byte) 0 /* mbz must be 0 */,
+ StructMrt6Msg.MRT6MSG_NOCACHE,
+ 1 /* mif */,
+ SOURCE,
+ GROUP);
+ byte[] bytes = msg.writeToBytes();
+
+ assertArrayEquals(MSG_BYTES, bytes);
+ }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index a5e5afb..a5c4fea 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -84,6 +84,13 @@
"host/**/*.kt",
],
libs: ["tradefed"],
- test_suites: ["ats", "device-tests", "general-tests", "cts", "mts-networking"],
+ test_suites: [
+ "ats",
+ "device-tests",
+ "general-tests",
+ "cts",
+ "mts-networking",
+ "mcts-networking",
+ ],
data: [":ConnectivityTestPreparer"],
}
diff --git a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
index 585157f..57602f1 100644
--- a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
+++ b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
@@ -133,7 +133,16 @@
}
@Test
- fun testReadFromRecorder_manyUids() {
+ fun testReadFromRecorder_manyUids_useDataInput() {
+ doTestReadFromRecorder_manyUids(useFastDataInput = false)
+ }
+
+ @Test
+ fun testReadFromRecorder_manyUids_useFastDataInput() {
+ doTestReadFromRecorder_manyUids(useFastDataInput = true)
+ }
+
+ fun doTestReadFromRecorder_manyUids(useFastDataInput: Boolean) {
val mockObserver = mock<NonMonotonicObserver<String>>()
val mockDropBox = mock<DropBoxManager>()
testFilesAssets.forEach {
@@ -146,7 +155,9 @@
PREFIX_UID,
UID_COLLECTION_BUCKET_DURATION_MS,
false /* includeTags */,
- false /* wipeOnError */
+ false /* wipeOnError */,
+ useFastDataInput /* useFastDataInput */,
+ it
)
recorder.orLoadCompleteLocked
}
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/hostside/Android.bp b/tests/cts/hostside/Android.bp
index e55ba63..a0aafc6 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -43,6 +43,8 @@
test_suites: [
"cts",
"general-tests",
+ "mcts-tethering",
+ "mts-tethering",
"sts"
],
data: [
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index 82f4a65..ab956bf 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -203,6 +203,7 @@
// Initial state
setBatterySaverMode(false);
setRestrictBackground(false);
+ setAppIdle(false);
// Get transports of the active network, this has to be done before changing meteredness,
// since wifi will be disconnected when changing from non-metered to metered.
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 9310888..3d53d6c 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -131,6 +131,7 @@
"cts",
"general-tests",
"mts-tethering",
+ "mcts-tethering",
],
}
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index da4fe28..a9e3715 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -49,5 +49,7 @@
"general-tests",
"mts-dnsresolver",
"mts-networking",
+ "mcts-dnsresolver",
+ "mcts-networking",
],
}
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 6b7954a..f6a025a 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -648,7 +648,7 @@
testIpv6Only, requiresValidation, testSessionKey , testIkeTunConnParams)));
}
- @Test
+ @Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileV4() throws Exception {
doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
false /* testSessionKey */, false /* testIkeTunConnParams */);
@@ -660,7 +660,7 @@
false /* testSessionKey */, false /* testIkeTunConnParams */);
}
- @Test
+ @Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileV6() throws Exception {
doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
false /* testSessionKey */, false /* testIkeTunConnParams */);
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index fe2f813..84b6745 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -626,6 +626,7 @@
}
}
agent.unregister()
+ callback.eventuallyExpect<Lost> { it.network == agent.network }
// callback will be unregistered in tearDown()
}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index f374181..1b1f367 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -62,7 +62,7 @@
@Test
fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
- assumeFalse(isInterfaceForTetheringAvailable)
+ assumeFalse(isInterfaceForTetheringAvailable())
var downstreamIface: TestNetworkInterface? = null
var tetheringEventCallback: MyTetheringEventCallback? = null
@@ -104,7 +104,7 @@
@Test
fun testMdnsDiscoveryWorkOnTetheringInterface() {
- assumeFalse(isInterfaceForTetheringAvailable)
+ assumeFalse(isInterfaceForTetheringAvailable())
setIncludeTestInterfaces(true)
var downstreamIface: TestNetworkInterface? = null
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index e1ea2b9..a040201 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,130 @@
}
@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 testMultipleSubTypeAdvertisingAndDiscovery_withUpdate() {
+ val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype1"
+ }
+ val si2 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype2"
+ }
+ val registrationRecord = NsdRegistrationRecord()
+ val subtype3DiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si1)
+ updateService(registrationRecord, si2)
+ nsdManager.discoverServices("_subtype2.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD, testNetwork1.network,
+ { it.run() }, subtype3DiscoveryRecord)
+ subtype3DiscoveryRecord.waitForServiceDiscovered(serviceName,
+ serviceType, testNetwork1.network)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(subtype3DiscoveryRecord)
+ subtype3DiscoveryRecord.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())
@@ -1305,6 +1430,18 @@
return cb.serviceInfo
}
+ /**
+ * Update a service.
+ */
+ private fun updateService(
+ record: NsdRegistrationRecord,
+ si: NsdServiceInfo,
+ executor: Executor = Executor { it.run() }
+ ) {
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, executor, record)
+ // TODO: add the callback check for the update.
+ }
+
private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
val record = NsdResolveRecord()
nsdManager.resolveService(discoveredInfo, Executor { it.run() }, record)
diff --git a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
index 9de7f4d..8919666 100644
--- a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
+++ b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
@@ -213,7 +213,6 @@
assertFalse(isUidNetworkingBlocked(TEST_UID3))
}
- @IgnoreUpTo(VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
fun testGetDataSaverEnabled() {
testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(DATA_SAVER_DISABLED))
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index a6e9e95..81557f8 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -64,6 +64,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
@@ -90,7 +91,8 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
public class NetworkStatsCollectionTest {
-
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
private static final String TEST_FILE = "test.bin";
private static final String TEST_IMSI = "310260000000000";
private static final int TEST_SUBID = 1;
@@ -199,6 +201,33 @@
77017831L, 100995L, 35436758L, 92344L);
}
+ private InputStream getUidInputStreamFromRes(int uidRes) throws Exception {
+ final File testFile =
+ new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE);
+ stageFile(uidRes, testFile);
+
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS);
+ collection.readLegacyUid(testFile, true);
+
+ // now export into a unified format
+ final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ collection.write(bos);
+ return new ByteArrayInputStream(bos.toByteArray());
+ }
+
+ @Test
+ public void testFastDataInputRead() throws Exception {
+ final NetworkStatsCollection legacyCollection =
+ new NetworkStatsCollection(30 * MINUTE_IN_MILLIS, false /* useFastDataInput */);
+ final NetworkStatsCollection fastReadCollection =
+ new NetworkStatsCollection(30 * MINUTE_IN_MILLIS, true /* useFastDataInput */);
+ final InputStream bis = getUidInputStreamFromRes(R.raw.netstats_uid_v4);
+ legacyCollection.read(bis);
+ bis.reset();
+ fastReadCollection.read(bis);
+ assertCollectionEntries(legacyCollection.getEntries(), fastReadCollection);
+ }
+
@Test
public void testStartEndAtomicBuckets() throws Exception {
final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS);
diff --git a/tests/unit/java/android/net/NetworkStatsRecorderTest.java b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
index fad11a3..7d039b6 100644
--- a/tests/unit/java/android/net/NetworkStatsRecorderTest.java
+++ b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
@@ -16,8 +16,17 @@
package com.android.server.net;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.SET_FOREGROUND;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG;
+import static com.android.server.ConnectivityStatsLog.NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.mockito.Mockito.any;
@@ -29,21 +38,31 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.annotation.NonNull;
+import android.net.NetworkIdentity;
+import android.net.NetworkIdentitySet;
import android.net.NetworkStats;
+import android.net.NetworkStatsCollection;
import android.os.DropBoxManager;
import androidx.test.filters.SmallTest;
import com.android.internal.util.FileRotator;
+import com.android.metrics.NetworkStatsMetricsLogger;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
+import libcore.testing.io.TestIoUtils;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
@RunWith(DevSdkIgnoreRunner.class)
@@ -53,6 +72,8 @@
private static final String TAG = NetworkStatsRecorderTest.class.getSimpleName();
private static final String TEST_PREFIX = "test";
+ private static final int TEST_UID1 = 1234;
+ private static final int TEST_UID2 = 1235;
@Mock private DropBoxManager mDropBox;
@Mock private NetworkStats.NonMonotonicObserver mObserver;
@@ -64,7 +85,8 @@
private NetworkStatsRecorder buildRecorder(FileRotator rotator, boolean wipeOnError) {
return new NetworkStatsRecorder(rotator, mObserver, mDropBox, TEST_PREFIX,
- HOUR_IN_MILLIS, false /* includeTags */, wipeOnError);
+ HOUR_IN_MILLIS, false /* includeTags */, wipeOnError,
+ false /* useFastDataInput */, null /* baseDir */);
}
@Test
@@ -85,4 +107,110 @@
// Verify that the rotator won't delete files.
verify(rotator, never()).deleteAll();
}
+
+ @Test
+ public void testFileReadingMetrics_empty() {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+ final NetworkStatsMetricsLogger.Dependencies deps =
+ mock(NetworkStatsMetricsLogger.Dependencies.class);
+ final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+ logger.logRecorderFileReading(PREFIX_XT, 888, null /* statsDir */, collection,
+ false /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT,
+ 1 /* readIndex */,
+ 888 /* readLatencyMillis */,
+ 0 /* fileCount */,
+ 0 /* totalFileSize */,
+ 0 /* keys */,
+ 0 /* uids */,
+ 0 /* totalHistorySize */,
+ false /* useFastDataInput */
+ );
+
+ // Write second time, verify the index increases.
+ logger.logRecorderFileReading(PREFIX_XT, 567, null /* statsDir */, collection,
+ true /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_XT,
+ 2 /* readIndex */,
+ 567 /* readLatencyMillis */,
+ 0 /* fileCount */,
+ 0 /* totalFileSize */,
+ 0 /* keys */,
+ 0 /* uids */,
+ 0 /* totalHistorySize */,
+ true /* useFastDataInput */
+ );
+ }
+
+ @Test
+ public void testFileReadingMetrics() {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+ final NetworkStats.Entry entry = new NetworkStats.Entry();
+ final NetworkIdentitySet identSet = new NetworkIdentitySet();
+ identSet.add(new NetworkIdentity.Builder().build());
+ // Empty entries will be skipped, put some ints to make sure they can be recorded.
+ entry.rxBytes = 1;
+
+ collection.recordData(identSet, TEST_UID1, SET_DEFAULT, TAG_NONE, 0, 60, entry);
+ collection.recordData(identSet, TEST_UID2, SET_DEFAULT, TAG_NONE, 0, 60, entry);
+ collection.recordData(identSet, TEST_UID2, SET_FOREGROUND, TAG_NONE, 30, 60, entry);
+
+ final NetworkStatsMetricsLogger.Dependencies deps =
+ mock(NetworkStatsMetricsLogger.Dependencies.class);
+ final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+ logger.logRecorderFileReading(PREFIX_UID, 123, null /* statsDir */, collection,
+ false /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UID,
+ 1 /* readIndex */,
+ 123 /* readLatencyMillis */,
+ 0 /* fileCount */,
+ 0 /* totalFileSize */,
+ 3 /* keys */,
+ 2 /* uids */,
+ 5 /* totalHistorySize */,
+ false /* useFastDataInput */
+ );
+ }
+
+ @Test
+ public void testFileReadingMetrics_fileAttributes() throws IOException {
+ final NetworkStatsCollection collection = new NetworkStatsCollection(30);
+
+ // Create files for testing. Only the first and the third files should be counted,
+ // with total 26 (each char takes 2 bytes) bytes in the content.
+ final File statsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
+ write(statsDir, "uid_tag.1024-2048", "wanted");
+ write(statsDir, "uid_tag.1024-2048.backup", "");
+ write(statsDir, "uid_tag.2048-", "wanted2");
+ write(statsDir, "uid.2048-4096", "unwanted");
+ write(statsDir, "uid.2048-4096.backup", "unwanted2");
+
+ final NetworkStatsMetricsLogger.Dependencies deps =
+ mock(NetworkStatsMetricsLogger.Dependencies.class);
+ final NetworkStatsMetricsLogger logger = new NetworkStatsMetricsLogger(deps);
+ logger.logRecorderFileReading(PREFIX_UID_TAG, 678, statsDir, collection,
+ false /* useFastDataInput */);
+ verify(deps).writeRecorderFileReadingStats(
+ NETWORK_STATS_RECORDER_FILE_OPERATED__RECORDER_PREFIX__PREFIX_UIDTAG,
+ 1 /* readIndex */,
+ 678 /* readLatencyMillis */,
+ 2 /* fileCount */,
+ 26 /* totalFileSize */,
+ 0 /* keys */,
+ 0 /* uids */,
+ 0 /* totalHistorySize */,
+ false /* useFastDataInput */
+ );
+ }
+
+ private void write(@NonNull File baseDir, @NonNull String name,
+ @NonNull String value) throws IOException {
+ final DataOutputStream out = new DataOutputStream(
+ new FileOutputStream(new File(baseDir, name)));
+ out.writeChars(value);
+ out.close();
+ }
}
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 550a9ee..461ead8 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -38,6 +38,7 @@
import androidx.test.filters.SmallTest;
+import com.android.modules.utils.build.SdkLevel;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.FunctionalUtils.ThrowingConsumer;
@@ -86,73 +87,81 @@
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testResolveServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestResolveService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testResolveServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestResolveService();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testDiscoverServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestDiscoverService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testDiscoverServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestDiscoverService();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testParallelResolveServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestParallelResolveService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testParallelResolveServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestParallelResolveService();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testInvalidCallsS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestInvalidCalls();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testInvalidCallsPreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestInvalidCalls();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testRegisterServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestRegisterService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testRegisterServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestRegisterService();
}
+ private void verifyDaemonStarted(boolean targetSdkPreS) throws Exception {
+ if (targetSdkPreS && !SdkLevel.isAtLeastV()) {
+ verify(mServiceConn).startDaemon();
+ } else {
+ verify(mServiceConn, never()).startDaemon();
+ }
+ }
+
private void doTestResolveService() throws Exception {
NsdManager manager = mManager;
@@ -196,6 +205,22 @@
verify(listener2, timeout(mTimeoutMs).times(1)).onServiceResolved(reply);
}
+ @Test
+ public void testRegisterServiceWithAdvertisingRequest() throws Exception {
+ final NsdManager manager = mManager;
+ final NsdServiceInfo request = new NsdServiceInfo("another_name2", "another_type2");
+ request.setPort(2203);
+ final AdvertisingRequest advertisingRequest = new AdvertisingRequest.Builder(request,
+ PROTOCOL).build();
+ final NsdManager.RegistrationListener listener = mock(
+ NsdManager.RegistrationListener.class);
+
+ manager.registerService(advertisingRequest, Runnable::run, listener);
+ int key4 = getRequestKey(req -> verify(mServiceConn).registerService(req.capture(), any()));
+ mCallback.onRegisterServiceSucceeded(key4, request);
+ verify(listener, timeout(mTimeoutMs).times(1)).onServiceRegistered(request);
+ }
+
private void doTestRegisterService() throws Exception {
NsdManager manager = mManager;
@@ -346,8 +371,19 @@
NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
NsdServiceInfo invalidService = new NsdServiceInfo(null, null);
- NsdServiceInfo validService = new NsdServiceInfo("a_name", "a_type");
+ NsdServiceInfo validService = new NsdServiceInfo("a_name", "_a_type._tcp");
+ NsdServiceInfo otherServiceWithSubtype = new NsdServiceInfo("b_name", "_a_type._tcp,_sub1");
+ NsdServiceInfo validServiceDuplicate = new NsdServiceInfo("a_name", "_a_type._tcp");
+ NsdServiceInfo validServiceSubtypeUpdate = new NsdServiceInfo("a_name",
+ "_a_type._tcp,_sub1,_s2");
+ NsdServiceInfo otherSubtypeUpdate = new NsdServiceInfo("a_name", "_a_type._tcp,_sub1,_s3");
+ NsdServiceInfo dotSyntaxSubtypeUpdate = new NsdServiceInfo("a_name", "_sub1._a_type._tcp");
validService.setPort(2222);
+ otherServiceWithSubtype.setPort(2222);
+ validServiceDuplicate.setPort(2222);
+ validServiceSubtypeUpdate.setPort(2222);
+ otherSubtypeUpdate.setPort(2222);
+ dotSyntaxSubtypeUpdate.setPort(2222);
// Service registration
// - invalid arguments
@@ -358,7 +394,21 @@
mustFail(() -> { manager.registerService(validService, -1, listener1); });
mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
manager.registerService(validService, PROTOCOL, listener1);
- // - listener already registered
+ // - update without subtype is not allowed
+ mustFail(() -> { manager.registerService(validServiceDuplicate, PROTOCOL, listener1); });
+ // - update with subtype is allowed
+ manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+ // - re-updating to the same subtype is allowed
+ manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+ // - updating to other subtypes is allowed
+ manager.registerService(otherSubtypeUpdate, PROTOCOL, listener1);
+ // - update back to the service without subtype is allowed
+ manager.registerService(validService, PROTOCOL, listener1);
+ // - updating to a subtype with _sub._type syntax is not allowed
+ mustFail(() -> { manager.registerService(dotSyntaxSubtypeUpdate, PROTOCOL, listener1); });
+ // - updating to a different service name is not allowed
+ mustFail(() -> { manager.registerService(otherServiceWithSubtype, PROTOCOL, listener1); });
+ // - listener already registered, and not using subtypes
mustFail(() -> { manager.registerService(validService, PROTOCOL, listener1); });
manager.unregisterService(listener1);
// TODO: make listener immediately reusable
diff --git a/tests/unit/java/com/android/metrics/NetworkRequestStateInfoTest.java b/tests/unit/java/com/android/metrics/NetworkRequestStateInfoTest.java
new file mode 100644
index 0000000..5709ed1
--- /dev/null
+++ b/tests/unit/java/com/android/metrics/NetworkRequestStateInfoTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.metrics;
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class NetworkRequestStateInfoTest {
+
+ @Mock
+ private NetworkRequestStateInfo.Dependencies mDependencies;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+ @Test
+ public void testSetNetworkRequestRemoved() {
+ final long nrStartTime = 1L;
+ final long nrEndTime = 101L;
+
+ NetworkRequest notMeteredWifiNetworkRequest = new NetworkRequest(
+ new NetworkCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true),
+ 0, 1, NetworkRequest.Type.REQUEST
+ );
+
+ // This call will be used to calculate NR received time
+ Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrStartTime);
+ NetworkRequestStateInfo mNetworkRequestStateInfo = new NetworkRequestStateInfo(
+ notMeteredWifiNetworkRequest, mDependencies);
+
+ // This call will be used to calculate NR removed time
+ Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrEndTime);
+ mNetworkRequestStateInfo.setNetworkRequestRemoved();
+ assertEquals(
+ nrEndTime - nrStartTime,
+ mNetworkRequestStateInfo.getNetworkRequestDurationMillis());
+ assertEquals(mNetworkRequestStateInfo.getNetworkRequestStateStatsType(),
+ NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED);
+ }
+
+ @Test
+ public void testCheckInitialState() {
+ NetworkRequestStateInfo mNetworkRequestStateInfo = new NetworkRequestStateInfo(
+ new NetworkRequest(new NetworkCapabilities(), 0, 1, NetworkRequest.Type.REQUEST),
+ mDependencies);
+ assertEquals(mNetworkRequestStateInfo.getNetworkRequestStateStatsType(),
+ NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED);
+ }
+}
diff --git a/tests/unit/java/com/android/metrics/NetworkRequestStateStatsMetricsTest.java b/tests/unit/java/com/android/metrics/NetworkRequestStateStatsMetricsTest.java
new file mode 100644
index 0000000..17a0719
--- /dev/null
+++ b/tests/unit/java/com/android/metrics/NetworkRequestStateStatsMetricsTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.metrics;
+
+
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
+import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.HandlerThread;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.testutils.HandlerUtils;
+
+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.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class NetworkRequestStateStatsMetricsTest {
+ @Mock
+ private NetworkRequestStateStatsMetrics.Dependencies mNRStateStatsDeps;
+ @Mock
+ private NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
+ @Captor
+ private ArgumentCaptor<NetworkRequestStateInfo> mNetworkRequestStateInfoCaptor;
+ private NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
+ private HandlerThread mHandlerThread;
+ private static final int TEST_REQUEST_ID = 10;
+ private static final int TEST_PACKAGE_UID = 20;
+ private static final int TIMEOUT_MS = 30_000;
+ private static final NetworkRequest NOT_METERED_WIFI_NETWORK_REQUEST = new NetworkRequest(
+ new NetworkCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true)
+ .setCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET, false)
+ .setRequestorUid(TEST_PACKAGE_UID),
+ 0, TEST_REQUEST_ID, NetworkRequest.Type.REQUEST
+ );
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mHandlerThread = new HandlerThread("NetworkRequestStateStatsMetrics");
+ Mockito.when(mNRStateStatsDeps.makeHandlerThread("NetworkRequestStateStatsMetrics"))
+ .thenReturn(mHandlerThread);
+ mNetworkRequestStateStatsMetrics = new NetworkRequestStateStatsMetrics(
+ mNRStateStatsDeps, mNRStateInfoDeps);
+ }
+
+ @Test
+ public void testNetworkRequestReceivedRemoved() {
+ final long nrStartTime = 1L;
+ final long nrEndTime = 101L;
+ // This call will be used to calculate NR received time
+ Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrStartTime);
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+
+ verify(mNRStateStatsDeps, times(1))
+ .writeStats(mNetworkRequestStateInfoCaptor.capture());
+
+ NetworkRequestStateInfo nrStateInfoSent = mNetworkRequestStateInfoCaptor.getValue();
+ assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED,
+ nrStateInfoSent.getNetworkRequestStateStatsType());
+ assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
+ assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
+ assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
+ assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
+ assertFalse(nrStateInfoSent.getNetCapabilityInternet());
+ assertEquals(0, nrStateInfoSent.getNetworkRequestDurationMillis());
+
+ clearInvocations(mNRStateStatsDeps);
+ // This call will be used to calculate NR removed time
+ Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrEndTime);
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+
+ verify(mNRStateStatsDeps, times(1))
+ .writeStats(mNetworkRequestStateInfoCaptor.capture());
+
+ nrStateInfoSent = mNetworkRequestStateInfoCaptor.getValue();
+ assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED,
+ nrStateInfoSent.getNetworkRequestStateStatsType());
+ assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
+ assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
+ assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
+ assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
+ assertFalse(nrStateInfoSent.getNetCapabilityInternet());
+ assertEquals(nrEndTime - nrStartTime, nrStateInfoSent.getNetworkRequestDurationMillis());
+ }
+
+ @Test
+ public void testUnreceivedNetworkRequestRemoved() {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+ verify(mNRStateStatsDeps, never())
+ .writeStats(any(NetworkRequestStateInfo.class));
+ }
+
+ @Test
+ public void testExistingNetworkRequestReceived() {
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+ verify(mNRStateStatsDeps, times(1))
+ .writeStats(any(NetworkRequestStateInfo.class));
+
+ clearInvocations(mNRStateStatsDeps);
+ mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+ verify(mNRStateStatsDeps, never())
+ .writeStats(any(NetworkRequestStateInfo.class));
+
+ }
+}
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/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 32014c2..87e7967 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
@@ -149,6 +150,9 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NsdServiceTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
private static final long CLEANUP_DELAY_MS = 500;
private static final long TIMEOUT_MS = 500;
@@ -254,6 +258,8 @@
}
}
+ // Native mdns provided by Netd is removed after U.
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
@DisableCompatChanges({
RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER,
@@ -286,6 +292,7 @@
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testNoDaemonStartedWhenClientsConnect() throws Exception {
// Creating an NsdManager will not cause daemon startup.
connectClient(mService);
@@ -321,6 +328,7 @@
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testClientRequestsAreGCedAtDisconnection() throws Exception {
final NsdManager client = connectClient(mService);
final INsdManagerCallback cb1 = getCallback();
@@ -365,6 +373,7 @@
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testCleanupDelayNoRequestActive() throws Exception {
final NsdManager client = connectClient(mService);
@@ -401,6 +410,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testDiscoverOnTetheringDownstream() throws Exception {
final NsdManager client = connectClient(mService);
final int interfaceIdx = 123;
@@ -499,6 +509,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testDiscoverOnBlackholeNetwork() throws Exception {
final NsdManager client = connectClient(mService);
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -531,6 +542,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testServiceRegistrationSuccessfulAndFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -585,6 +597,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testServiceDiscoveryFailed() throws Exception {
final NsdManager client = connectClient(mService);
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -617,6 +630,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testServiceResolutionFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -652,6 +666,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testGettingAddressFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -703,6 +718,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testNoCrashWhenProcessResolutionAfterBinderDied() throws Exception {
final NsdManager client = connectClient(mService);
final INsdManagerCallback cb = getCallback();
@@ -723,6 +739,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testStopServiceResolution() {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -749,6 +766,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testStopResolutionFailed() {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -774,6 +792,7 @@
@Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testStopResolutionDuringGettingAddress() throws RemoteException {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -955,6 +974,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testMdnsDiscoveryManagerFeature() {
// Create NsdService w/o feature enabled.
final NsdManager client = connectClient(mService);
@@ -1117,7 +1137,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);
@@ -1201,6 +1222,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testMdnsAdvertiserFeatureFlagging() {
// Create NsdService w/o feature enabled.
final NsdManager client = connectClient(mService);
@@ -1223,7 +1245,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();
@@ -1239,6 +1261,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testTypeSpecificFeatureFlagging() {
doReturn("_type1._tcp:flag1,_type2._tcp:flag2").when(mDeps).getTypeAllowlistFlags();
doReturn(true).when(mDeps).isFeatureEnabled(any(),
@@ -1283,9 +1306,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 +1333,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 +1381,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 +1411,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();
@@ -1453,14 +1475,22 @@
final String serviceType5 = "_TEST._999._tcp.";
final String serviceType6 = "_998._tcp.,_TEST";
final String serviceType7 = "_997._tcp,_TEST";
+ final String serviceType8 = "_997._tcp,_test1,_test2,_test3";
+ final String serviceType9 = "_test4._997._tcp,_test1,_test2,_test3";
assertNull(parseTypeAndSubtype(serviceType1));
assertNull(parseTypeAndSubtype(serviceType2));
assertNull(parseTypeAndSubtype(serviceType3));
- assertEquals(new Pair<>("_123._udp", null), parseTypeAndSubtype(serviceType4));
- assertEquals(new Pair<>("_999._tcp", "_TEST"), parseTypeAndSubtype(serviceType5));
- assertEquals(new Pair<>("_998._tcp", "_TEST"), parseTypeAndSubtype(serviceType6));
- assertEquals(new Pair<>("_997._tcp", "_TEST"), parseTypeAndSubtype(serviceType7));
+ assertEquals(new Pair<>("_123._udp", Collections.emptyList()),
+ parseTypeAndSubtype(serviceType4));
+ assertEquals(new Pair<>("_999._tcp", List.of("_TEST")), parseTypeAndSubtype(serviceType5));
+ assertEquals(new Pair<>("_998._tcp", List.of("_TEST")), parseTypeAndSubtype(serviceType6));
+ assertEquals(new Pair<>("_997._tcp", List.of("_TEST")), parseTypeAndSubtype(serviceType7));
+
+ assertEquals(new Pair<>("_997._tcp", List.of("_test1", "_test2", "_test3")),
+ parseTypeAndSubtype(serviceType8));
+ assertEquals(new Pair<>("_997._tcp", List.of("_test4")),
+ parseTypeAndSubtype(serviceType9));
}
@Test
@@ -1479,7 +1509,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 +1542,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/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index 10a0982..4fcf8a8 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -142,81 +142,81 @@
// Hexadecimal representation of a SOCK_DIAG response with tcp info.
private static final String SOCK_DIAG_TCP_INET_HEX =
// struct nlmsghdr.
- "14010000" + // length = 276
- "1400" + // type = SOCK_DIAG_BY_FAMILY
- "0301" + // flags = NLM_F_REQUEST | NLM_F_DUMP
- "00000000" + // seqno
- "00000000" + // pid (0 == kernel)
+ "14010000" // length = 276
+ + "1400" // type = SOCK_DIAG_BY_FAMILY
+ + "0301" // flags = NLM_F_REQUEST | NLM_F_DUMP
+ + "00000000" // seqno
+ + "00000000" // pid (0 == kernel)
// struct inet_diag_req_v2
- "02" + // family = AF_INET
- "06" + // state
- "00" + // timer
- "00" + // retrans
+ + "02" // family = AF_INET
+ + "06" // state
+ + "00" // timer
+ + "00" // retrans
// inet_diag_sockid
- "DEA5" + // idiag_sport = 42462
- "71B9" + // idiag_dport = 47473
- "0a006402000000000000000000000000" + // idiag_src = 10.0.100.2
- "08080808000000000000000000000000" + // idiag_dst = 8.8.8.8
- "00000000" + // idiag_if
- "34ED000076270000" + // idiag_cookie = 43387759684916
- "00000000" + // idiag_expires
- "00000000" + // idiag_rqueue
- "00000000" + // idiag_wqueue
- "00000000" + // idiag_uid
- "00000000" + // idiag_inode
+ + "DEA5" // idiag_sport = 42462
+ + "71B9" // idiag_dport = 47473
+ + "0a006402000000000000000000000000" // idiag_src = 10.0.100.2
+ + "08080808000000000000000000000000" // idiag_dst = 8.8.8.8
+ + "00000000" // idiag_if
+ + "34ED000076270000" // idiag_cookie = 43387759684916
+ + "00000000" // idiag_expires
+ + "00000000" // idiag_rqueue
+ + "00000000" // idiag_wqueue
+ + "39300000" // idiag_uid = 12345
+ + "00000000" // idiag_inode
// rtattr
- "0500" + // len = 5
- "0800" + // type = 8
- "00000000" + // data
- "0800" + // len = 8
- "0F00" + // type = 15(INET_DIAG_MARK)
- "850A0C00" + // data, socket mark=789125
- "AC00" + // len = 172
- "0200" + // type = 2(INET_DIAG_INFO)
+ + "0500" // len = 5
+ + "0800" // type = 8
+ + "00000000" // data
+ + "0800" // len = 8
+ + "0F00" // type = 15(INET_DIAG_MARK)
+ + "850A0C00" // data, socket mark=789125
+ + "AC00" // len = 172
+ + "0200" // type = 2(INET_DIAG_INFO)
// tcp_info
- "01" + // state = TCP_ESTABLISHED
- "00" + // ca_state = TCP_CA_OPEN
- "05" + // retransmits = 5
- "00" + // probes = 0
- "00" + // backoff = 0
- "07" + // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
- "88" + // wscale = 8
- "00" + // delivery_rate_app_limited = 0
- "4A911B00" + // rto = 1806666
- "00000000" + // ato = 0
- "2E050000" + // sndMss = 1326
- "18020000" + // rcvMss = 536
- "00000000" + // unsacked = 0
- "00000000" + // acked = 0
- "00000000" + // lost = 0
- "00000000" + // retrans = 0
- "00000000" + // fackets = 0
- "BB000000" + // lastDataSent = 187
- "00000000" + // lastAckSent = 0
- "BB000000" + // lastDataRecv = 187
- "BB000000" + // lastDataAckRecv = 187
- "DC050000" + // pmtu = 1500
- "30560100" + // rcvSsthresh = 87600
- "3E2C0900" + // rttt = 601150
- "1F960400" + // rttvar = 300575
- "78050000" + // sndSsthresh = 1400
- "0A000000" + // sndCwnd = 10
- "A8050000" + // advmss = 1448
- "03000000" + // reordering = 3
- "00000000" + // rcvrtt = 0
- "30560100" + // rcvspace = 87600
- "00000000" + // totalRetrans = 0
- "53AC000000000000" + // pacingRate = 44115
- "FFFFFFFFFFFFFFFF" + // maxPacingRate = 18446744073709551615
- "0100000000000000" + // bytesAcked = 1
- "0000000000000000" + // bytesReceived = 0
- "0A000000" + // SegsOut = 10
- "00000000" + // SegsIn = 0
- "00000000" + // NotSentBytes = 0
- "3E2C0900" + // minRtt = 601150
- "00000000" + // DataSegsIn = 0
- "00000000" + // DataSegsOut = 0
- "0000000000000000"; // deliverRate = 0
+ + "01" // state = TCP_ESTABLISHED
+ + "00" // ca_state = TCP_CA_OPEN
+ + "05" // retransmits = 5
+ + "00" // probes = 0
+ + "00" // backoff = 0
+ + "07" // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
+ + "88" // wscale = 8
+ + "00" // delivery_rate_app_limited = 0
+ + "4A911B00" // rto = 1806666
+ + "00000000" // ato = 0
+ + "2E050000" // sndMss = 1326
+ + "18020000" // rcvMss = 536
+ + "00000000" // unsacked = 0
+ + "00000000" // acked = 0
+ + "00000000" // lost = 0
+ + "00000000" // retrans = 0
+ + "00000000" // fackets = 0
+ + "BB000000" // lastDataSent = 187
+ + "00000000" // lastAckSent = 0
+ + "BB000000" // lastDataRecv = 187
+ + "BB000000" // lastDataAckRecv = 187
+ + "DC050000" // pmtu = 1500
+ + "30560100" // rcvSsthresh = 87600
+ + "3E2C0900" // rttt = 601150
+ + "1F960400" // rttvar = 300575
+ + "78050000" // sndSsthresh = 1400
+ + "0A000000" // sndCwnd = 10
+ + "A8050000" // advmss = 1448
+ + "03000000" // reordering = 3
+ + "00000000" // rcvrtt = 0
+ + "30560100" // rcvspace = 87600
+ + "00000000" // totalRetrans = 0
+ + "53AC000000000000" // pacingRate = 44115
+ + "FFFFFFFFFFFFFFFF" // maxPacingRate = 18446744073709551615
+ + "0100000000000000" // bytesAcked = 1
+ + "0000000000000000" // bytesReceived = 0
+ + "0A000000" // SegsOut = 10
+ + "00000000" // SegsIn = 0
+ + "00000000" // NotSentBytes = 0
+ + "3E2C0900" // minRtt = 601150
+ + "00000000" // DataSegsIn = 0
+ + "00000000" // DataSegsOut = 0
+ + "0000000000000000"; // deliverRate = 0
private static final String SOCK_DIAG_NO_TCP_INET_HEX =
// struct nlmsghdr
"14000000" // length = 20
@@ -427,6 +427,16 @@
}
@Test
+ public void testIsAnyTcpSocketConnected_noTargetUidSocket() throws Exception {
+ setupResponseWithSocketExisting();
+ // Configured uid(12345) is not in the VPN range.
+ assertFalse(visibleOnHandlerThread(mTestHandler,
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(
+ TEST_NETID,
+ new ArraySet<>(Arrays.asList(new Range<>(99999, 99999))))));
+ }
+
+ @Test
public void testIsAnyTcpSocketConnected_withIncorrectNetId() throws Exception {
setupResponseWithSocketExisting();
assertFalse(visibleOnHandlerThread(mTestHandler,
diff --git a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt b/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
index 12758c6..4e15d5f 100644
--- a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
@@ -18,14 +18,17 @@
import android.net.INetd
import android.os.Build
+import android.util.Log
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.test.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
-import kotlin.test.assertFailsWith
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@@ -46,9 +49,15 @@
inOrder.verify(mNetd).tetherAddForward("from2", "to1")
inOrder.verify(mNetd).ipfwdAddInterfaceForward("from2", "to1")
- assertFailsWith<IllegalStateException> {
- // Can't add the same pair again
+ val hasFailed = AtomicBoolean(false)
+ val prevHandler = Log.setWtfHandler { tag, what, system ->
+ hasFailed.set(true)
+ }
+ tryTest {
mService.addInterfaceForward("from2", "to1")
+ assertTrue(hasFailed.get())
+ } cleanup {
+ Log.setWtfHandler(prevHandler)
}
mService.removeInterfaceForward("from1", "to1")
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..4b1f166 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -22,11 +22,18 @@
import android.os.Build
import android.os.HandlerThread
import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_A
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_AAAA
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_ANY
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_PTR
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_SRV
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_TXT
import com.android.server.connectivity.mdns.MdnsRecordRepository.Dependencies
import com.android.server.connectivity.mdns.MdnsRecordRepository.getReverseDnsAddress
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
@@ -35,7 +42,9 @@
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
+import kotlin.test.assertNull
import kotlin.test.assertTrue
+import kotlin.test.fail
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -46,6 +55,14 @@
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"
+// RFC6762 10. Resource Record TTL Values and Cache Coherency
+// The recommended TTL value for Multicast DNS resource records with a host name as the resource
+// record's name (e.g., A, AAAA, HINFO) or a host name contained within the resource record's rdata
+// (e.g., SRV, reverse mapping PTR record) SHOULD be 120 seconds. The recommended TTL value for
+// other Multicast DNS resource records is 75 minutes.
+private const val LONG_TTL = 4_500_000L
+private const val SHORT_TTL = 120_000L
private val TEST_HOSTNAME = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
private val TEST_ADDRESSES = listOf(
LinkAddress(parseNumericAddress("192.0.2.111"), 24),
@@ -95,8 +112,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)
@@ -119,7 +135,7 @@
assertEquals(MdnsServiceRecord(expectedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
0 /* servicePriority */, 0 /* serviceWeight */,
TEST_PORT, TEST_HOSTNAME), packet.authorityRecords[0])
@@ -131,10 +147,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 +160,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 +191,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 +202,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 +245,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 +262,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 +275,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 +294,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 +305,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 +316,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 +369,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 +397,7 @@
0L /* receiptTimeMillis */,
false /* cacheFlush */,
4500000L /* ttlMillis */,
- serviceType)
- ), packet.answers)
+ serviceType))
assertContentEquals(listOf(
MdnsNsecRecord(v4AddrRev,
@@ -376,31 +405,31 @@
true /* cacheFlush */,
120000L /* ttlMillis */,
v4AddrRev,
- intArrayOf(MdnsRecord.TYPE_PTR)),
+ intArrayOf(TYPE_PTR)),
MdnsNsecRecord(TEST_HOSTNAME,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
TEST_HOSTNAME,
- intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
+ intArrayOf(TYPE_A, TYPE_AAAA)),
MdnsNsecRecord(v6Addr1Rev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v6Addr1Rev,
- intArrayOf(MdnsRecord.TYPE_PTR)),
+ intArrayOf(TYPE_PTR)),
MdnsNsecRecord(v6Addr2Rev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v6Addr2Rev,
- intArrayOf(MdnsRecord.TYPE_PTR)),
+ intArrayOf(TYPE_PTR)),
MdnsNsecRecord(serviceName,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName,
- intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV))
+ intArrayOf(TYPE_TXT, TYPE_SRV))
), packet.additionalRecords)
}
@@ -482,103 +511,283 @@
assertEquals(7, replyCaseInsensitive.additionalAnswers.size)
}
- @Test
- fun testGetReply() {
- doGetReplyTest(subtype = null)
- }
-
- @Test
- fun testGetReply_WithSubtype() {
- doGetReplyTest(TEST_SUBTYPE)
- }
-
- private fun doGetReplyTest(subtype: String?) {
- 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")
-
- val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
- val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
+ /**
+ * Creates mDNS query packet with given query names and types.
+ */
+ private fun makeQuery(vararg queries: Pair<Int, Array<String>>): MdnsPacket {
+ val questions = queries.map { (type, name) -> makeQuestionRecord(name, type) }
+ return MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ }
+
+ private fun makeQuestionRecord(name: Array<String>, type: Int): MdnsRecord {
+ when (type) {
+ TYPE_PTR -> return MdnsPointerRecord(name, false /* isUnicast */)
+ TYPE_SRV -> return MdnsServiceRecord(name, false /* isUnicast */)
+ TYPE_TXT -> return MdnsTextRecord(name, false /* isUnicast */)
+ TYPE_A, TYPE_AAAA -> return MdnsInetAddressRecord(name, type, false /* isUnicast */)
+ else -> fail("Unexpected question type: $type")
+ }
+ }
+
+ @Test
+ fun testGetReply_singlePtrQuestion_returnsSrvTxtAddressNsecRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
val reply = repository.getReply(query, src)
assertNotNull(reply)
- // Source address is IPv4
- assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address)
- assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
-
- // TTLs as per RFC6762 10.
- val longTtl = 4_500_000L
- val shortTtl = 120_000L
- val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
-
assertEquals(listOf(
MdnsPointerRecord(
- queriedName,
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- longTtl,
- serviceName),
- ), reply.answers)
-
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
assertEquals(listOf(
- MdnsTextRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- longTtl,
- listOf() /* entries */),
- MdnsServiceRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- 0 /* servicePriority */,
- 0 /* serviceWeight */,
- TEST_PORT,
- TEST_HOSTNAME),
- MdnsInetAddressRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- TEST_ADDRESSES[0].address),
- MdnsInetAddressRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- TEST_ADDRESSES[1].address),
- MdnsInetAddressRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- TEST_ADDRESSES[2].address),
- MdnsNsecRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- longTtl,
- serviceName /* nextDomain */,
- intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
- MdnsNsecRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- shortTtl,
- TEST_HOSTNAME /* nextDomain */,
- intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
- ), reply.additionalAnswers)
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_singleSubtypePtrQuestion_returnsSrvTxtAddressNsecRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"), 0L, false,
+ LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_duplicatePtrQuestions_doesNotReturnDuplicateRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_multiplePtrQuestionsWithSubtype_doesNotReturnDuplicateRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+ TYPE_PTR to arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName),
+ MdnsPointerRecord(
+ arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"),
+ 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_txtQuestion_returnsNoNsecRecord() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(TYPE_TXT to serviceName)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf())),
+ reply.answers)
+ // No NSEC records because the reply doesn't include the SRV record
+ assertTrue(reply.additionalAnswers.isEmpty())
+ }
+
+ @Test
+ fun testGetReply_AAAAQuestionButNoIpv6Address_returnsNsecRecord() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(
+ TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE),
+ listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+
+ val query = makeQuery(TYPE_AAAA to TEST_HOSTNAME)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertTrue(reply.answers.isEmpty())
+ assertEquals(listOf(
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, LONG_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_AAAA))),
+ reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_ptrAndSrvQuestions_doesNotReturnSrvRecordInAdditionalAnswerSection() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+ TYPE_SRV to serviceName)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName),
+ MdnsServiceRecord(
+ serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME)),
+ reply.answers)
+ assertFalse(reply.additionalAnswers.any { it -> it is MdnsServiceRecord })
+ }
+
+ @Test
+ fun testGetReply_srvTxtAddressQuestions_returnsAllRecordsInAnswerSectionExceptNsec() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_SRV to serviceName,
+ TYPE_TXT to serviceName,
+ TYPE_SRV to serviceName,
+ TYPE_A to TEST_HOSTNAME,
+ TYPE_AAAA to TEST_HOSTNAME)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA))),
+ reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_queryWithIpv4Address_replyWithIpv4Address() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+ val srcIpv4 = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val replyIpv4 = repository.getReply(query, srcIpv4)
+
+ assertNotNull(replyIpv4)
+ assertEquals(MdnsConstants.getMdnsIPv4Address(), replyIpv4.destination.address)
+ assertEquals(MdnsConstants.MDNS_PORT, replyIpv4.destination.port)
+ }
+
+ @Test
+ fun testGetReply_queryWithIpv6Address_replyWithIpv6Address() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+ val srcIpv6 = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+ val replyIpv6 = repository.getReply(query, srcIpv6)
+
+ assertNotNull(replyIpv6)
+ assertEquals(MdnsConstants.getMdnsIPv6Address(), replyIpv6.destination.address)
+ assertEquals(MdnsConstants.MDNS_PORT, replyIpv6.destination.port)
}
@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 +814,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 +842,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 +871,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 +927,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)
@@ -746,7 +954,7 @@
expectedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
0 /* servicePriority */,
0 /* serviceWeight */,
TEST_PORT,
@@ -755,33 +963,294 @@
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
TEST_ADDRESSES[0].address),
MdnsInetAddressRecord(
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
TEST_ADDRESSES[1].address),
MdnsInetAddressRecord(
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- 120_000L /* ttlMillis */,
+ SHORT_TTL /* ttlMillis */,
TEST_ADDRESSES[2].address)
), packet.authorityRecords)
assertContentEquals(intArrayOf(TEST_SERVICE_ID_1), repository.clearServices())
}
+
+ private fun doGetReplyWithAnswersTest(
+ questions: List<MdnsRecord>,
+ knownAnswers: List<MdnsRecord>,
+ replyAnswers: List<MdnsRecord>,
+ additionalAnswers: List<MdnsRecord>,
+ expectReply: Boolean
+ ) {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+ MdnsFeatureFlags.newBuilder().setIsKnownAnswerSuppressionEnabled(true).build())
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ val query = MdnsPacket(0 /* flags */, questions, knownAnswers,
+ listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val reply = repository.getReply(query, src)
+
+ if (!expectReply) {
+ assertNull(reply)
+ return
+ }
+
+ assertNotNull(reply)
+ // Source address is IPv4
+ assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address)
+ assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
+ assertEquals(replyAnswers, reply.answers)
+ assertEquals(additionalAnswers, reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+ doGetReplyWithAnswersTest(questions, knownAnswers, listOf() /* replyAnswers */,
+ listOf() /* additionalAnswers */, false /* expectReply */)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers_TtlLessThanHalf() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ (LONG_TTL / 2 - 1000L),
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")))
+ val replyAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ serviceName))
+ val additionalAnswers = listOf(
+ MdnsTextRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ listOf() /* entries */),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers,
+ true /* expectReply */)
+ }
+
+ @Test
+ fun testGetReply_HasAnotherAnswer() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ arrayOf("MyOtherTestService", "_testservice", "_tcp", "local")))
+ val replyAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL,
+ serviceName))
+ val additionalAnswers = listOf(
+ MdnsTextRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ listOf() /* entries */),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers,
+ true /* expectReply */)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers_MultiQuestions() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(
+ MdnsPointerRecord(queriedName, false /* isUnicast */),
+ MdnsServiceRecord(serviceName, false /* isUnicast */))
+ val knownAnswers = listOf(MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL - 1000L,
+ serviceName))
+ val replyAnswers = listOf(MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ SHORT_TTL /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME))
+ val additionalAnswers = listOf(
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ SHORT_TTL,
+ TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ doGetReplyWithAnswersTest(questions, knownAnswers, replyAnswers, additionalAnswers,
+ true /* expectReply */)
+ }
+
+ @Test
+ fun testGetReply_HasAnswers_MultiQuestions_NoReply() {
+ val queriedName = arrayOf("_testservice", "_tcp", "local")
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ val questions = listOf(
+ MdnsPointerRecord(queriedName, false /* isUnicast */),
+ MdnsServiceRecord(serviceName, false /* isUnicast */))
+ val knownAnswers = listOf(
+ MdnsPointerRecord(
+ queriedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ LONG_TTL - 1000L,
+ serviceName),
+ MdnsServiceRecord(
+ serviceName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ SHORT_TTL - 15_000L,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME))
+ doGetReplyWithAnswersTest(questions, knownAnswers, listOf() /* replyAnswers */,
+ listOf() /* additionalAnswers */, false /* expectReply */)
+ }
}
private fun MdnsRecordRepository.initWithService(
serviceId: Int,
serviceInfo: NsdServiceInfo,
- subtype: String? = null,
+ subtypes: Set<String> = setOf(),
+ addresses: List<LinkAddress> = TEST_ADDRESSES
): AnnouncementInfo {
- updateAddresses(TEST_ADDRESSES)
- addService(serviceId, serviceInfo, subtype)
+ updateAddresses(addresses)
+ serviceInfo.setSubtypes(subtypes)
+ addService(serviceId, serviceInfo)
val probingInfo = setServiceProbing(serviceId)
assertNotNull(probingInfo)
return onProbingSucceeded(probingInfo)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt
new file mode 100644
index 0000000..9e2933f
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsReplySenderTest.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.connectivity.mdns
+
+import android.net.InetAddresses
+import android.net.LinkAddress
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Message
+import com.android.net.module.util.SharedLog
+import com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import java.net.InetSocketAddress
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+private const val TEST_PORT = 12345
+private const val DEFAULT_TIMEOUT_MS = 2000L
+private const val LONG_TTL = 4_500_000L
+private const val SHORT_TTL = 120_000L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class MdnsReplySenderTest {
+ private val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ private val serviceType = arrayOf("_testservice", "_tcp", "local")
+ private val hostname = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
+ private val hostAddresses = listOf(
+ LinkAddress(InetAddresses.parseNumericAddress("192.0.2.111"), 24),
+ LinkAddress(InetAddresses.parseNumericAddress("2001:db8::111"), 64),
+ LinkAddress(InetAddresses.parseNumericAddress("2001:db8::222"), 64))
+ private val answers = listOf(
+ MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */, false /* cacheFlush */,
+ LONG_TTL, serviceName))
+ private val additionalAnswers = listOf(
+ MdnsTextRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */, LONG_TTL,
+ listOf() /* entries */),
+ MdnsServiceRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, 0 /* servicePriority */, 0 /* serviceWeight */, TEST_PORT, hostname),
+ MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[0].address),
+ MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[1].address),
+ MdnsInetAddressRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ SHORT_TTL, hostAddresses[2].address),
+ MdnsNsecRecord(serviceName, 0L /* receiptTimeMillis */, true /* cacheFlush */, LONG_TTL,
+ serviceName /* nextDomain */,
+ intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
+ MdnsNsecRecord(hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */, SHORT_TTL,
+ hostname /* nextDomain */, intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
+ private val thread = HandlerThread(MdnsReplySenderTest::class.simpleName)
+ private val socket = mock(MdnsInterfaceSocket::class.java)
+ private val buffer = ByteArray(1500)
+ private val sharedLog = SharedLog(MdnsReplySenderTest::class.simpleName)
+ private val deps = mock(MdnsReplySender.Dependencies::class.java)
+ private val handler by lazy { Handler(thread.looper) }
+ private val replySender by lazy {
+ MdnsReplySender(thread.looper, socket, buffer, sharedLog, false /* enableDebugLog */, deps)
+ }
+
+ @Before
+ fun setUp() {
+ thread.start()
+ doReturn(true).`when`(socket).hasJoinedIpv4()
+ doReturn(true).`when`(socket).hasJoinedIpv6()
+ }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ thread.join()
+ }
+
+ private fun <T> runningOnHandlerAndReturn(functor: (() -> T)): T {
+ val future = CompletableFuture<T>()
+ handler.post {
+ future.complete(functor())
+ }
+ return future.get(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+
+ private fun sendNow(packet: MdnsPacket, destination: InetSocketAddress):
+ Unit = runningOnHandlerAndReturn { replySender.sendNow(packet, destination) }
+
+ private fun queueReply(reply: MdnsReplyInfo):
+ Unit = runningOnHandlerAndReturn { replySender.queueReply(reply) }
+
+ @Test
+ fun testSendNow() {
+ val packet = MdnsPacket(0x8400,
+ listOf() /* questions */,
+ answers,
+ listOf() /* authorityRecords */,
+ additionalAnswers)
+ sendNow(packet, IPV4_SOCKET_ADDR)
+ verify(socket).send(argThat{ it.socketAddress.equals(IPV4_SOCKET_ADDR) })
+ }
+
+ @Test
+ fun testQueueReply() {
+ val reply = MdnsReplyInfo(answers, additionalAnswers, 20L /* sendDelayMs */,
+ IPV4_SOCKET_ADDR)
+ val handlerCaptor = ArgumentCaptor.forClass(Handler::class.java)
+ val messageCaptor = ArgumentCaptor.forClass(Message::class.java)
+ queueReply(reply)
+ verify(deps).sendMessageDelayed(handlerCaptor.capture(), messageCaptor.capture(), eq(20L))
+
+ val realHandler = handlerCaptor.value
+ val delayMessage = messageCaptor.value
+ realHandler.sendMessage(delayMessage)
+ verify(socket, timeout(DEFAULT_TIMEOUT_MS)).send(argThat{
+ it.socketAddress.equals(IPV4_SOCKET_ADDR)
+ })
+ }
+}
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())
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
new file mode 100644
index 0000000..35f8ae5
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
@@ -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 android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.Process
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CSNetworkRequestStateStatsMetricsTests : CSTest() {
+ private val CELL_INTERNET_NOT_METERED_NC = NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+ .build().setRequestorUidAndPackageName(Process.myUid(), context.getPackageName())
+
+ private val CELL_INTERNET_NOT_METERED_NR = NetworkRequest.Builder()
+ .setCapabilities(CELL_INTERNET_NOT_METERED_NC).build()
+
+ @Before
+ fun setup() {
+ waitForIdle()
+ clearInvocations(networkRequestStateStatsMetrics)
+ }
+
+ @Test
+ fun testRequestTypeNRProduceMetrics() {
+ cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+ waitForIdle()
+
+ verify(networkRequestStateStatsMetrics).onNetworkRequestReceived(
+ argThat{req -> req.networkCapabilities.equals(
+ CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+ }
+
+ @Test
+ fun testListenTypeNRProduceNoMetrics() {
+ cm.registerNetworkCallback(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
+ waitForIdle()
+ verify(networkRequestStateStatsMetrics, never()).onNetworkRequestReceived(any())
+ }
+
+ @Test
+ fun testRemoveRequestTypeNRProduceMetrics() {
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, cb)
+
+ waitForIdle()
+ clearInvocations(networkRequestStateStatsMetrics)
+
+ cm.unregisterNetworkCallback(cb)
+ waitForIdle()
+ verify(networkRequestStateStatsMetrics).onNetworkRequestRemoved(
+ argThat{req -> req.networkCapabilities.equals(
+ CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 958c4f2..5c9a762 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -54,6 +54,7 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.android.internal.app.IBatteryStats
import com.android.internal.util.test.BroadcastInterceptingContext
+import com.android.metrics.NetworkRequestStateStatsMetrics
import com.android.modules.utils.build.SdkLevel
import com.android.net.module.util.ArrayTrackRecord
import com.android.networkstack.apishim.common.UnsupportedApiLevelException
@@ -157,6 +158,7 @@
val netd = mock<INetd>()
val bpfNetMaps = mock<BpfNetMaps>()
val clatCoordinator = mock<ClatCoordinator>()
+ val networkRequestStateStatsMetrics = mock<NetworkRequestStateStatsMetrics>()
val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
val alarmManager = makeMockAlarmManager()
val systemConfigManager = makeMockSystemConfigManager()
@@ -197,6 +199,9 @@
MultinetworkPolicyTracker(c, h, r,
MultinetworkPolicyTrackerTestDependencies(connResources.get()))
+ override fun makeNetworkRequestStateStatsMetrics(c: Context) =
+ this@CSTest.networkRequestStateStatsMetrics
+
// All queried features must be mocked, because the test cannot hold the
// READ_DEVICE_CONFIG permission and device config utils use static methods for
// checking permissions.
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 7a4dfed..1ee3f9d 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -67,6 +67,8 @@
import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME;
+import static com.android.server.net.NetworkStatsService.NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME;
@@ -169,7 +171,6 @@
import java.io.File;
import java.io.FileDescriptor;
-import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
@@ -284,9 +285,14 @@
private @Mock PersistentInt mImportLegacyAttemptsCounter;
private @Mock PersistentInt mImportLegacySuccessesCounter;
private @Mock PersistentInt mImportLegacyFallbacksCounter;
+ private int mFastDataInputTargetAttempts = 0;
+ private @Mock PersistentInt mFastDataInputSuccessesCounter;
+ private @Mock PersistentInt mFastDataInputFallbacksCounter;
+ private String mCompareStatsResult = null;
private @Mock Resources mResources;
private Boolean mIsDebuggable;
private HandlerThread mObserverHandlerThread;
+ final TestDependencies mDeps = new TestDependencies();
private class MockContext extends BroadcastInterceptingContext {
private final Context mBaseContext;
@@ -369,7 +375,6 @@
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
mHandlerThread = new HandlerThread("NetworkStatsServiceTest-HandlerThread");
- final NetworkStatsService.Dependencies deps = makeDependencies();
// Create a separate thread for observers to run on. This thread cannot be the same
// as the handler thread, because the observer callback is fired on this thread, and
// it should not be blocked by client code. Additionally, creating the observers
@@ -384,7 +389,7 @@
}
};
mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
- mClock, mSettings, mStatsFactory, statsObservers, deps);
+ mClock, mSettings, mStatsFactory, statsObservers, mDeps);
mElapsedRealtime = 0L;
@@ -423,132 +428,150 @@
mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
}
- @NonNull
- private NetworkStatsService.Dependencies makeDependencies() {
- return new NetworkStatsService.Dependencies() {
- @Override
- public File getLegacyStatsDir() {
- return mLegacyStatsDir;
- }
+ class TestDependencies extends NetworkStatsService.Dependencies {
+ private int mCompareStatsInvocation = 0;
- @Override
- public File getOrCreateStatsDir() {
- return mStatsDir;
- }
+ @Override
+ public File getLegacyStatsDir() {
+ return mLegacyStatsDir;
+ }
- @Override
- public boolean getStoreFilesInApexData() {
- return mStoreFilesInApexData;
- }
+ @Override
+ public File getOrCreateStatsDir() {
+ return mStatsDir;
+ }
- @Override
- public int getImportLegacyTargetAttempts() {
- return mImportLegacyTargetAttempts;
- }
+ @Override
+ public boolean getStoreFilesInApexData() {
+ return mStoreFilesInApexData;
+ }
- @Override
- public PersistentInt createPersistentCounter(@androidx.annotation.NonNull Path dir,
- @androidx.annotation.NonNull String name) throws IOException {
- switch (name) {
- case NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME:
- return mImportLegacyAttemptsCounter;
- case NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME:
- return mImportLegacySuccessesCounter;
- case NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME:
- return mImportLegacyFallbacksCounter;
- default:
- throw new IllegalArgumentException("Unknown counter name: " + name);
- }
- }
+ @Override
+ public int getImportLegacyTargetAttempts() {
+ return mImportLegacyTargetAttempts;
+ }
- @Override
- public NetworkStatsCollection readPlatformCollection(
- @NonNull String prefix, long bucketDuration) {
- return mPlatformNetworkStatsCollection.get(prefix);
- }
+ @Override
+ public int getUseFastDataInputTargetAttempts() {
+ return mFastDataInputTargetAttempts;
+ }
- @Override
- public HandlerThread makeHandlerThread() {
- return mHandlerThread;
- }
+ @Override
+ public String compareStats(NetworkStatsCollection a, NetworkStatsCollection b,
+ boolean allowKeyChange) {
+ mCompareStatsInvocation++;
+ return mCompareStatsResult;
+ }
- @Override
- public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(
- @NonNull Context context, @NonNull Executor executor,
- @NonNull NetworkStatsService service) {
+ int getCompareStatsInvocation() {
+ return mCompareStatsInvocation;
+ }
- return mNetworkStatsSubscriptionsMonitor;
+ @Override
+ public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name) {
+ switch (name) {
+ case NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME:
+ return mImportLegacyAttemptsCounter;
+ case NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME:
+ return mImportLegacySuccessesCounter;
+ case NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME:
+ return mImportLegacyFallbacksCounter;
+ case NETSTATS_FASTDATAINPUT_SUCCESSES_COUNTER_NAME:
+ return mFastDataInputSuccessesCounter;
+ case NETSTATS_FASTDATAINPUT_FALLBACKS_COUNTER_NAME:
+ return mFastDataInputFallbacksCounter;
+ default:
+ throw new IllegalArgumentException("Unknown counter name: " + name);
}
+ }
- @Override
- public ContentObserver makeContentObserver(Handler handler,
- NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) {
- mHandler = handler;
- return mContentObserver = super.makeContentObserver(handler, settings, monitor);
- }
+ @Override
+ public NetworkStatsCollection readPlatformCollection(
+ @NonNull String prefix, long bucketDuration) {
+ return mPlatformNetworkStatsCollection.get(prefix);
+ }
- @Override
- public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
- return mLocationPermissionChecker;
- }
+ @Override
+ public HandlerThread makeHandlerThread() {
+ return mHandlerThread;
+ }
- @Override
- public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
- @NonNull Context ctx, @NonNull Handler handler) {
- return mBpfInterfaceMapUpdater;
- }
+ @Override
+ public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(
+ @NonNull Context context, @NonNull Executor executor,
+ @NonNull NetworkStatsService service) {
- @Override
- public IBpfMap<S32, U8> getUidCounterSetMap() {
- return mUidCounterSetMap;
- }
+ return mNetworkStatsSubscriptionsMonitor;
+ }
- @Override
- public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
- return mCookieTagMap;
- }
+ @Override
+ public ContentObserver makeContentObserver(Handler handler,
+ NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) {
+ mHandler = handler;
+ return mContentObserver = super.makeContentObserver(handler, settings, monitor);
+ }
- @Override
- public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
- return mStatsMapA;
- }
+ @Override
+ public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
+ return mLocationPermissionChecker;
+ }
- @Override
- public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
- return mStatsMapB;
- }
+ @Override
+ public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
+ @NonNull Context ctx, @NonNull Handler handler) {
+ return mBpfInterfaceMapUpdater;
+ }
- @Override
- public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
- return mAppUidStatsMap;
- }
+ @Override
+ public IBpfMap<S32, U8> getUidCounterSetMap() {
+ return mUidCounterSetMap;
+ }
- @Override
- public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
- return mIfaceStatsMap;
- }
+ @Override
+ public IBpfMap<CookieTagMapKey, CookieTagMapValue> getCookieTagMap() {
+ return mCookieTagMap;
+ }
- @Override
- public boolean isDebuggable() {
- return mIsDebuggable == Boolean.TRUE;
- }
+ @Override
+ public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapA() {
+ return mStatsMapA;
+ }
- @Override
- public BpfNetMaps makeBpfNetMaps(Context ctx) {
- return mBpfNetMaps;
- }
+ @Override
+ public IBpfMap<StatsMapKey, StatsMapValue> getStatsMapB() {
+ return mStatsMapB;
+ }
- @Override
- public SkDestroyListener makeSkDestroyListener(
- IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
- return mSkDestroyListener;
- }
+ @Override
+ public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
+ return mAppUidStatsMap;
+ }
- @Override
- public boolean supportEventLogger(@NonNull Context cts) {
- return true;
- }
- };
+ @Override
+ public IBpfMap<S32, StatsMapValue> getIfaceStatsMap() {
+ return mIfaceStatsMap;
+ }
+
+ @Override
+ public boolean isDebuggable() {
+ return mIsDebuggable == Boolean.TRUE;
+ }
+
+ @Override
+ public BpfNetMaps makeBpfNetMaps(Context ctx) {
+ return mBpfNetMaps;
+ }
+
+ @Override
+ public SkDestroyListener makeSkDestroyListener(
+ IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
+ return mSkDestroyListener;
+ }
+
+ @Override
+ public boolean supportEventLogger(@NonNull Context cts) {
+ return true;
+ }
}
@After
@@ -2166,6 +2189,71 @@
}
@Test
+ public void testAdoptFastDataInput_featureDisabled() throws Exception {
+ // Boot through serviceReady() with flag disabled, verify the persistent
+ // counters are not increased.
+ mFastDataInputTargetAttempts = 0;
+ doReturn(0).when(mFastDataInputSuccessesCounter).get();
+ doReturn(0).when(mFastDataInputFallbacksCounter).get();
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ assertEquals(0, mDeps.getCompareStatsInvocation());
+ }
+
+ @Test
+ public void testAdoptFastDataInput_noRetryAfterFail() throws Exception {
+ // Boot through serviceReady(), verify the service won't retry unexpectedly
+ // since the target attempt remains the same.
+ mFastDataInputTargetAttempts = 1;
+ doReturn(0).when(mFastDataInputSuccessesCounter).get();
+ doReturn(1).when(mFastDataInputFallbacksCounter).get();
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ }
+
+ @Test
+ public void testAdoptFastDataInput_noRetryAfterSuccess() throws Exception {
+ // Boot through serviceReady(), verify the service won't retry unexpectedly
+ // since the target attempt remains the same.
+ mFastDataInputTargetAttempts = 1;
+ doReturn(1).when(mFastDataInputSuccessesCounter).get();
+ doReturn(0).when(mFastDataInputFallbacksCounter).get();
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ }
+
+ @Test
+ public void testAdoptFastDataInput_hasDiff() throws Exception {
+ // Boot through serviceReady() with flag enabled and assumes the stats are
+ // failed to compare, verify the fallbacks counter is increased.
+ mockDefaultSettings();
+ doReturn(0).when(mFastDataInputSuccessesCounter).get();
+ doReturn(0).when(mFastDataInputFallbacksCounter).get();
+ mFastDataInputTargetAttempts = 1;
+ mCompareStatsResult = "Has differences";
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter, never()).set(anyInt());
+ verify(mFastDataInputFallbacksCounter).set(1);
+ }
+
+ @Test
+ public void testAdoptFastDataInput_noDiff() throws Exception {
+ // Boot through serviceReady() with target attempts increased,
+ // assumes there was a previous failure,
+ // and assumes the stats are successfully compared,
+ // verify the successes counter is increased.
+ mFastDataInputTargetAttempts = 2;
+ doReturn(1).when(mFastDataInputFallbacksCounter).get();
+ mCompareStatsResult = null;
+ mService.systemReady();
+ verify(mFastDataInputSuccessesCounter).set(1);
+ verify(mFastDataInputFallbacksCounter, never()).set(anyInt());
+ }
+
+ @Test
public void testStatsFactoryRemoveUids() throws Exception {
// pretend that network comes online
mockDefaultSettings();
@@ -2230,7 +2318,8 @@
final DropBoxManager dropBox = mock(DropBoxManager.class);
return new NetworkStatsRecorder(new FileRotator(
directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
- observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError);
+ observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError,
+ false /* useFastDataInput */, directory);
}
private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index 30aeca5..6a5ea4b 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -2,11 +2,14 @@
"presubmit": [
{
"name": "CtsThreadNetworkTestCases"
+ },
+ {
+ "name": "ThreadNetworkUnitTests"
}
],
"postsubmit": [
{
- "name": "ThreadNetworkUnitTests"
+ "name": "ThreadNetworkIntegrationTests"
}
]
}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index 89dcd39..a9da8d6 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -38,6 +38,8 @@
void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
void leave(in IOperationReceiver receiver);
+ void setTestNetworkAsUpstream(in String testNetworkInterfaceName, in IOperationReceiver receiver);
+
int getThreadVersion();
void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 34b0b06..b5699a9 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -31,6 +31,7 @@
import android.os.RemoteException;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -499,6 +500,31 @@
}
}
+ /**
+ * Sets to use a specified test network as the upstream.
+ *
+ * @param testNetworkInterfaceName The name of the test network interface. When it's null,
+ * forbids using test network as an upstream.
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ * @hide
+ */
+ @VisibleForTesting
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void setTestNetworkAsUpstream(
+ @Nullable String testNetworkInterfaceName,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.setTestNetworkAsUpstream(
+ testNetworkInterfaceName, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
private static <T> void propagateError(
Executor executor,
OutcomeReceiver<T, ThreadNetworkException> receiver,
diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
index d7c49a0..be54cbc 100644
--- a/thread/service/java/com/android/server/thread/InfraInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -36,8 +36,7 @@
* @return an ICMPv6 socket file descriptor on the Infrastructure network interface.
* @throws IOException when fails to create the socket.
*/
- public static ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName)
- throws IOException {
+ public ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName) throws IOException {
return ParcelFileDescriptor.adoptFd(nativeCreateIcmp6Socket(infraInterfaceName));
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 60c97bf..2cd1be3 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -53,14 +53,15 @@
import android.Manifest.permission;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.LocalNetworkConfig;
-import android.net.MulticastRoutingConfig;
import android.net.LocalNetworkInfo;
+import android.net.MulticastRoutingConfig;
import android.net.Network;
import android.net.NetworkAgent;
import android.net.NetworkAgentConfig;
@@ -69,6 +70,7 @@
import android.net.NetworkRequest;
import android.net.NetworkScore;
import android.net.RouteInfo;
+import android.net.TestNetworkSpecifier;
import android.net.thread.ActiveOperationalDataset;
import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
import android.net.thread.IActiveOperationalDatasetReceiver;
@@ -91,12 +93,12 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.ServiceManagerWrapper;
+import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
import com.android.server.thread.openthread.IOtDaemon;
import com.android.server.thread.openthread.IOtDaemonCallback;
import com.android.server.thread.openthread.IOtStatusReceiver;
import com.android.server.thread.openthread.Ipv6AddressInfo;
import com.android.server.thread.openthread.OtDaemonState;
-import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
import java.io.IOException;
import java.net.Inet6Address;
@@ -136,6 +138,7 @@
private final Supplier<IOtDaemon> mOtDaemonSupplier;
private final ConnectivityManager mConnectivityManager;
private final TunInterfaceController mTunIfController;
+ private final InfraInterfaceController mInfraIfController;
private final LinkProperties mLinkProperties = new LinkProperties();
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
@@ -147,9 +150,10 @@
private MulticastRoutingConfig mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
private MulticastRoutingConfig mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
private Network mUpstreamNetwork;
- private final NetworkRequest mUpstreamNetworkRequest;
+ private NetworkRequest mUpstreamNetworkRequest;
+ private UpstreamNetworkCallback mUpstreamNetworkCallback;
+ private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
private final HashMap<Network, String> mNetworkToInterface;
- private final LocalNetworkConfig mLocalNetworkConfig;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@@ -160,7 +164,8 @@
NetworkProvider networkProvider,
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
- TunInterfaceController tunIfController) {
+ TunInterfaceController tunIfController,
+ InfraInterfaceController infraIfController) {
mContext = context;
mHandlerThread = handlerThread;
mHandler = new Handler(handlerThread.getLooper());
@@ -168,16 +173,8 @@
mOtDaemonSupplier = otDaemonSupplier;
mConnectivityManager = connectivityManager;
mTunIfController = tunIfController;
- mUpstreamNetworkRequest =
- new NetworkRequest.Builder()
- .clearCapabilities()
- .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .build();
- mLocalNetworkConfig =
- new LocalNetworkConfig.Builder()
- .setUpstreamSelector(mUpstreamNetworkRequest)
- .build();
+ mInfraIfController = infraIfController;
+ mUpstreamNetworkRequest = newUpstreamNetworkRequest();
mNetworkToInterface = new HashMap<Network, String>();
mBorderRouterConfig = new BorderRouterConfigurationParcel();
}
@@ -194,7 +191,8 @@
networkProvider,
() -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
context.getSystemService(ConnectivityManager.class),
- new TunInterfaceController(TUN_IF_NAME));
+ new TunInterfaceController(TUN_IF_NAME),
+ new InfraInterfaceController());
}
private static NetworkCapabilities newNetworkCapabilities() {
@@ -237,6 +235,60 @@
LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
}
+ private NetworkRequest newUpstreamNetworkRequest() {
+ NetworkRequest.Builder builder = new NetworkRequest.Builder().clearCapabilities();
+
+ if (mUpstreamTestNetworkSpecifier != null) {
+ return builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(mUpstreamTestNetworkSpecifier)
+ .build();
+ }
+ return builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ }
+
+ private LocalNetworkConfig newLocalNetworkConfig() {
+ return new LocalNetworkConfig.Builder()
+ .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
+ .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
+ .setUpstreamSelector(mUpstreamNetworkRequest)
+ .build();
+ }
+
+ @Override
+ public void setTestNetworkAsUpstream(
+ @Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ Log.i(TAG, "setTestNetworkAsUpstream: " + testNetworkInterfaceName);
+ mHandler.post(() -> setTestNetworkAsUpstreamInternal(testNetworkInterfaceName, receiver));
+ }
+
+ private void setTestNetworkAsUpstreamInternal(
+ @Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
+ checkOnHandlerThread();
+
+ TestNetworkSpecifier testNetworkSpecifier = null;
+ if (testNetworkInterfaceName != null) {
+ testNetworkSpecifier = new TestNetworkSpecifier(testNetworkInterfaceName);
+ }
+
+ if (!Objects.equals(mUpstreamTestNetworkSpecifier, testNetworkSpecifier)) {
+ cancelRequestUpstreamNetwork();
+ mUpstreamTestNetworkSpecifier = testNetworkSpecifier;
+ mUpstreamNetworkRequest = newUpstreamNetworkRequest();
+ requestUpstreamNetwork();
+ sendLocalNetworkConfig();
+ }
+ try {
+ receiver.onSuccess();
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+
private void initializeOtDaemon() {
try {
getOtDaemon();
@@ -289,45 +341,63 @@
}
private void requestUpstreamNetwork() {
+ if (mUpstreamNetworkCallback != null) {
+ throw new AssertionError("The upstream network request is already there.");
+ }
+ mUpstreamNetworkCallback = new UpstreamNetworkCallback();
mConnectivityManager.registerNetworkCallback(
- mUpstreamNetworkRequest,
- new ConnectivityManager.NetworkCallback() {
- @Override
- public void onAvailable(@NonNull Network network) {
- Log.i(TAG, "onAvailable: " + network);
- }
+ mUpstreamNetworkRequest, mUpstreamNetworkCallback, mHandler);
+ }
- @Override
- public void onLost(@NonNull Network network) {
- Log.i(TAG, "onLost: " + network);
- }
+ private void cancelRequestUpstreamNetwork() {
+ if (mUpstreamNetworkCallback == null) {
+ throw new AssertionError("The upstream network request null.");
+ }
+ mNetworkToInterface.clear();
+ mConnectivityManager.unregisterNetworkCallback(mUpstreamNetworkCallback);
+ mUpstreamNetworkCallback = null;
+ }
- @Override
- public void onLinkPropertiesChanged(
- @NonNull Network network, @NonNull LinkProperties linkProperties) {
- Log.i(
- TAG,
- String.format(
- "onLinkPropertiesChanged: {network: %s, interface: %s}",
- network, linkProperties.getInterfaceName()));
- mNetworkToInterface.put(network, linkProperties.getInterfaceName());
- if (network.equals(mUpstreamNetwork)) {
- enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
- }
- }
- },
- mHandler);
+ private final class UpstreamNetworkCallback extends ConnectivityManager.NetworkCallback {
+ @Override
+ public void onAvailable(@NonNull Network network) {
+ checkOnHandlerThread();
+ Log.i(TAG, "onAvailable: " + network);
+ }
+
+ @Override
+ public void onLost(@NonNull Network network) {
+ checkOnHandlerThread();
+ Log.i(TAG, "onLost: " + network);
+ }
+
+ @Override
+ public void onLinkPropertiesChanged(
+ @NonNull Network network, @NonNull LinkProperties linkProperties) {
+ checkOnHandlerThread();
+ Log.i(
+ TAG,
+ String.format(
+ "onLinkPropertiesChanged: {network: %s, interface: %s}",
+ network, linkProperties.getInterfaceName()));
+ mNetworkToInterface.put(network, linkProperties.getInterfaceName());
+ if (network.equals(mUpstreamNetwork)) {
+ enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+ }
+ }
}
private final class ThreadNetworkCallback extends ConnectivityManager.NetworkCallback {
@Override
public void onAvailable(@NonNull Network network) {
+ checkOnHandlerThread();
Log.i(TAG, "onAvailable: Thread network Available");
}
@Override
public void onLocalNetworkInfoChanged(
@NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
+ checkOnHandlerThread();
Log.i(TAG, "onLocalNetworkInfoChanged: " + localNetworkInfo);
if (localNetworkInfo.getUpstreamNetwork() == null) {
mUpstreamNetwork = null;
@@ -370,7 +440,7 @@
TAG,
netCaps,
mLinkProperties,
- mLocalNetworkConfig,
+ newLocalNetworkConfig(),
score,
new NetworkAgentConfig.Builder().build(),
mNetworkProvider) {};
@@ -524,29 +594,29 @@
return -1;
}
- private void enforceAllCallingPermissionsGranted(String... permissions) {
+ private void enforceAllPermissionsGranted(String... permissions) {
for (String permission : permissions) {
- mContext.enforceCallingPermission(
+ mContext.enforceCallingOrSelfPermission(
permission, "Permission " + permission + " is missing");
}
}
@Override
public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
- enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+ enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
}
@Override
public void unregisterStateCallback(IStateCallback stateCallback) throws RemoteException {
- enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+ enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
mHandler.post(() -> mOtDaemonCallbackProxy.unregisterStateCallback(stateCallback));
}
@Override
public void registerOperationalDatasetCallback(IOperationalDatasetCallback callback)
throws RemoteException {
- enforceAllCallingPermissionsGranted(
+ enforceAllPermissionsGranted(
permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> mOtDaemonCallbackProxy.registerDatasetCallback(callback));
}
@@ -554,7 +624,7 @@
@Override
public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback)
throws RemoteException {
- enforceAllCallingPermissionsGranted(
+ enforceAllPermissionsGranted(
permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback));
}
@@ -609,7 +679,7 @@
@Override
public void join(
@NonNull ActiveOperationalDataset activeDataset, @NonNull IOperationReceiver receiver) {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
mHandler.post(() -> joinInternal(activeDataset, receiverWrapper));
@@ -633,7 +703,7 @@
public void scheduleMigration(
@NonNull PendingOperationalDataset pendingDataset,
@NonNull IOperationReceiver receiver) {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
mHandler.post(() -> scheduleMigrationInternal(pendingDataset, receiverWrapper));
@@ -656,7 +726,7 @@
@Override
public void leave(@NonNull IOperationReceiver receiver) throws RemoteException {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver)));
}
@@ -681,7 +751,7 @@
try {
mBorderRouterConfig.infraInterfaceName = infraIfName;
mBorderRouterConfig.infraInterfaceIcmp6Socket =
- InfraInterfaceController.createIcmp6Socket(infraIfName);
+ mInfraIfController.createIcmp6Socket(infraIfName);
mBorderRouterConfig.isBorderRoutingEnabled = true;
mOtDaemon.configureBorderRouter(
@@ -754,20 +824,9 @@
if (mNetworkAgent == null) {
return;
}
- final LocalNetworkConfig.Builder configBuilder = new LocalNetworkConfig.Builder();
- LocalNetworkConfig localNetworkConfig =
- configBuilder
- .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
- .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
- .setUpstreamSelector(mUpstreamNetworkRequest)
- .build();
+ final LocalNetworkConfig localNetworkConfig = newLocalNetworkConfig();
mNetworkAgent.sendLocalNetworkConfig(localNetworkConfig);
- Log.d(
- TAG,
- "Sent localNetworkConfig with upstreamConfig "
- + mUpstreamMulticastRoutingConfig
- + " downstreamConfig"
- + mDownstreamMulticastRoutingConfig);
+ Log.d(TAG, "Sent localNetworkConfig: " + localNetworkConfig);
}
private void handleMulticastForwardingStateChanged(boolean isEnabled) {
@@ -800,8 +859,8 @@
MulticastRoutingConfig newDownstreamConfig;
MulticastRoutingConfig.Builder builder;
- if (mDownstreamMulticastRoutingConfig.getForwardingMode() !=
- MulticastRoutingConfig.FORWARD_SELECTED) {
+ if (mDownstreamMulticastRoutingConfig.getForwardingMode()
+ != MulticastRoutingConfig.FORWARD_SELECTED) {
Log.e(
TAG,
"Ignore multicast listening address updates when downstream multicast "
@@ -809,8 +868,8 @@
// Don't update the address set if downstream multicast forwarding is disabled.
return;
}
- if (isAdded ==
- mDownstreamMulticastRoutingConfig.getListeningAddresses().contains(address)) {
+ if (isAdded
+ == mDownstreamMulticastRoutingConfig.getListeningAddresses().contains(address)) {
return;
}
diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp
new file mode 100644
index 0000000..405fb76
--- /dev/null
+++ b/thread/tests/integration/Android.bp
@@ -0,0 +1,55 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+ name: "ThreadNetworkIntegrationTestsDefaults",
+ min_sdk_version: "30",
+ static_libs: [
+ "androidx.test.rules",
+ "guava",
+ "mockito-target-minus-junit4",
+ "net-tests-utils",
+ "net-utils-device-common",
+ "net-utils-device-common-bpf",
+ "testables",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ ],
+}
+
+android_test {
+ name: "ThreadNetworkIntegrationTests",
+ platform_apis: true,
+ manifest: "AndroidManifest.xml",
+ defaults: [
+ "framework-connectivity-test-defaults",
+ "ThreadNetworkIntegrationTestsDefaults"
+ ],
+ test_suites: [
+ "general-tests",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+ compile_multilib: "both",
+}
diff --git a/thread/tests/integration/AndroidManifest.xml b/thread/tests/integration/AndroidManifest.xml
new file mode 100644
index 0000000..a347654
--- /dev/null
+++ b/thread/tests/integration/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.thread.tests.integration">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <!-- The test need CHANGE_NETWORK_STATE permission to use requestNetwork API to setup test
+ network. Since R shell application don't have such permission, grant permission to the test
+ here. TODO: Remove CHANGE_NETWORK_STATE permission here and use adopt shell permission to
+ obtain CHANGE_NETWORK_STATE for testing once R device is no longer supported. -->
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.thread.tests.integration"
+ android:label="Thread integration tests">
+ </instrumentation>
+</manifest>
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
new file mode 100644
index 0000000..5d3818a
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.thread;
+
+import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
+import static android.net.thread.IntegrationTestUtils.isExpectedIcmpv6Packet;
+import static android.net.thread.IntegrationTestUtils.newPacketReader;
+import static android.net.thread.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.IntegrationTestUtils.waitFor;
+import static android.net.thread.IntegrationTestUtils.waitForStateAnyOf;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
+import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.TapPacketReader;
+import com.android.testutils.TestNetworkTracker;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/** Integration test cases for Thread Border Routing feature. */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class BorderRoutingTest {
+ private static final String TAG = BorderRoutingTest.class.getSimpleName();
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final ThreadNetworkManager mThreadNetworkManager =
+ mContext.getSystemService(ThreadNetworkManager.class);
+ private ThreadNetworkController mThreadNetworkController;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private TestNetworkTracker mInfraNetworkTracker;
+
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
+ private static final byte[] DEFAULT_DATASET_TLVS =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+ private static final ActiveOperationalDataset DEFAULT_DATASET =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+ @Before
+ public void setUp() throws Exception {
+ mHandlerThread = new HandlerThread(getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ var threadControllers = mThreadNetworkManager.getAllThreadNetworkControllers();
+ assertEquals(threadControllers.size(), 1);
+ mThreadNetworkController = threadControllers.get(0);
+ mInfraNetworkTracker =
+ runAsShell(
+ MANAGE_TEST_NETWORKS,
+ () ->
+ initTestNetwork(
+ mContext, new LinkProperties(), 5000 /* timeoutMs */));
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ CountDownLatch latch = new CountDownLatch(1);
+ mThreadNetworkController.setTestNetworkAsUpstream(
+ mInfraNetworkTracker.getTestIface().getInterfaceName(),
+ MoreExecutors.directExecutor(),
+ v -> {
+ latch.countDown();
+ });
+ latch.await();
+ });
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ CountDownLatch latch = new CountDownLatch(2);
+ mThreadNetworkController.setTestNetworkAsUpstream(
+ null, MoreExecutors.directExecutor(), v -> latch.countDown());
+ mThreadNetworkController.leave(
+ MoreExecutors.directExecutor(), v -> latch.countDown());
+ latch.await(10, TimeUnit.SECONDS);
+ });
+ runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
+
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+ }
+
+ @Test
+ public void infraDevicePingTheadDeviceOmr_Succeeds() throws Exception {
+ /*
+ * <pre>
+ * Topology:
+ * infra network Thread
+ * infra device -------------------- Border Router -------------- Full Thread device
+ * (Cuttlefish)
+ * </pre>
+ */
+
+ // BR forms a network.
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> {
+ mThreadNetworkController.join(
+ DEFAULT_DATASET, MoreExecutors.directExecutor(), result -> {});
+ });
+ waitForStateAnyOf(
+ mThreadNetworkController, List.of(DEVICE_ROLE_LEADER), 30 /* timeoutSeconds */);
+
+ // Creates a Full Thread Device (FTD) and lets it join the network.
+ FullThreadDevice ftd = new FullThreadDevice(5 /* node ID */);
+ ftd.factoryReset();
+ ftd.joinNetwork(DEFAULT_DATASET);
+ ftd.waitForStateAnyOf(List.of("router", "child"), 10 /* timeoutSeconds */);
+ waitFor(() -> ftd.getOmrAddress() != null, 60 /* timeoutSeconds */);
+ Inet6Address ftdOmr = ftd.getOmrAddress();
+ assertNotNull(ftdOmr);
+
+ // Creates a infra network device.
+ TapPacketReader infraNetworkReader =
+ newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
+ InfraNetworkDevice infraDevice =
+ new InfraNetworkDevice(MacAddress.fromString("1:2:3:4:5:6"), infraNetworkReader);
+ infraDevice.runSlaac(60 /* timeoutSeconds */);
+ assertNotNull(infraDevice.ipv6Addr);
+
+ // Infra device sends an echo request to FTD's OMR.
+ infraDevice.sendEchoRequest(ftdOmr);
+
+ // Infra device receives an echo reply sent by FTD.
+ assertNotNull(
+ readPacketFrom(
+ infraNetworkReader,
+ p -> isExpectedIcmpv6Packet(p, ICMPV6_ECHO_REPLY_TYPE)));
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/FullThreadDevice.java
new file mode 100644
index 0000000..01638f3
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/FullThreadDevice.java
@@ -0,0 +1,180 @@
+/*
+ * 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.thread;
+
+import static android.net.thread.IntegrationTestUtils.waitFor;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import static org.junit.Assert.fail;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.Inet6Address;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A class that launches and controls a simulation Full Thread Device (FTD).
+ *
+ * <p>This class launches an `ot-cli-ftd` process and communicates with it via command line input
+ * and output. See <a
+ * href="https://github.com/openthread/openthread/blob/main/src/cli/README.md">this page</a> for
+ * available commands.
+ */
+public final class FullThreadDevice {
+ private final Process mProcess;
+ private final BufferedReader mReader;
+ private final BufferedWriter mWriter;
+
+ private ActiveOperationalDataset mActiveOperationalDataset;
+
+ /**
+ * Constructs a {@link FullThreadDevice} for the given node ID.
+ *
+ * <p>It launches an `ot-cli-ftd` process using the given node ID. The node ID is an integer in
+ * range [1, OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE]. `OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE`
+ * is defined in `external/openthread/examples/platforms/simulation/platform-config.h`.
+ *
+ * @param nodeId the node ID for the simulation Full Thread Device.
+ * @throws IllegalStateException the node ID is already occupied by another simulation Thread
+ * device.
+ */
+ public FullThreadDevice(int nodeId) {
+ try {
+ mProcess = Runtime.getRuntime().exec("/system/bin/ot-cli-ftd " + nodeId);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to start ot-cli-ftd (id=" + nodeId + ")", e);
+ }
+ mReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream()));
+ mWriter = new BufferedWriter(new OutputStreamWriter(mProcess.getOutputStream()));
+ mActiveOperationalDataset = null;
+ }
+
+ /**
+ * Returns an OMR (Off-Mesh-Routable) address on this device if any.
+ *
+ * <p>This methods goes through all unicast addresses on the device and returns the first
+ * address which is neither link-local nor mesh-local.
+ */
+ public Inet6Address getOmrAddress() {
+ List<String> addresses = executeCommand("ipaddr");
+ IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
+ for (String address : addresses) {
+ if (address.startsWith("fe80:")) {
+ continue;
+ }
+ Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
+ if (!meshLocalPrefix.contains(addr)) {
+ return addr;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Joins the Thread network using the given {@link ActiveOperationalDataset}.
+ *
+ * @param dataset the Active Operational Dataset
+ */
+ public void joinNetwork(ActiveOperationalDataset dataset) {
+ mActiveOperationalDataset = dataset;
+ executeCommand("dataset set active " + base16().lowerCase().encode(dataset.toThreadTlvs()));
+ executeCommand("ifconfig up");
+ executeCommand("thread start");
+ }
+
+ /** Stops the Thread network radio. */
+ public void stopThreadRadio() {
+ executeCommand("thread stop");
+ executeCommand("ifconfig down");
+ }
+
+ /**
+ * Waits for the Thread device to enter the any state of the given {@link List<String>}.
+ *
+ * @param states the list of states to wait for. Valid states are "disabled", "detached",
+ * "child", "router" and "leader".
+ * @param timeoutSeconds the number of seconds to wait for.
+ */
+ public void waitForStateAnyOf(List<String> states, int timeoutSeconds) throws TimeoutException {
+ waitFor(() -> states.contains(getState()), timeoutSeconds);
+ }
+
+ /**
+ * Gets the state of the Thread device.
+ *
+ * @return a string representing the state.
+ */
+ public String getState() {
+ return executeCommand("state").get(0);
+ }
+
+ /** Runs the "factoryreset" command on the device. */
+ public void factoryReset() {
+ try {
+ mWriter.write("factoryreset\n");
+ mWriter.flush();
+ // fill the input buffer to avoid truncating next command
+ for (int i = 0; i < 1000; ++i) {
+ mWriter.write("\n");
+ }
+ mWriter.flush();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to run factoryreset on ot-cli-ftd", e);
+ }
+ }
+
+ private List<String> executeCommand(String command) {
+ try {
+ mWriter.write(command + "\n");
+ mWriter.flush();
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Failed to write the command " + command + " to ot-cli-ftd", e);
+ }
+ try {
+ return readUntilDone();
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Failed to read the ot-cli-ftd output of command: " + command, e);
+ }
+ }
+
+ private List<String> readUntilDone() throws IOException {
+ ArrayList<String> result = new ArrayList<>();
+ String line;
+ while ((line = mReader.readLine()) != null) {
+ if (line.equals("Done")) {
+ break;
+ }
+ if (line.startsWith("Error:")) {
+ fail("ot-cli-ftd reported an error: " + line);
+ }
+ if (!line.startsWith("> ")) {
+ result.add(line);
+ }
+ }
+ return result;
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java b/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java
new file mode 100644
index 0000000..43a800d
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/InfraNetworkDevice.java
@@ -0,0 +1,128 @@
+/*
+ * 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.thread;
+
+import static android.net.thread.IntegrationTestUtils.getRaPios;
+import static android.net.thread.IntegrationTestUtils.readPacketFrom;
+import static android.net.thread.IntegrationTestUtils.waitFor;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+
+import android.net.InetAddresses;
+import android.net.MacAddress;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.testutils.TapPacketReader;
+
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A class that simulates a device on the infrastructure network.
+ *
+ * <p>This class directly interacts with the TUN interface of the test network to pretend there's a
+ * device on the infrastructure network.
+ */
+public final class InfraNetworkDevice {
+ // The MAC address of this device.
+ public final MacAddress macAddr;
+ // The packet reader of the TUN interface of the test network.
+ public final TapPacketReader packetReader;
+ // The IPv6 address generated by SLAAC for the device.
+ public Inet6Address ipv6Addr;
+
+ /**
+ * Constructs an InfraNetworkDevice with the given {@link MAC address} and {@link
+ * TapPacketReader}.
+ *
+ * @param macAddr the MAC address of the device
+ * @param packetReader the packet reader of the TUN interface of the test network.
+ */
+ public InfraNetworkDevice(MacAddress macAddr, TapPacketReader packetReader) {
+ this.macAddr = macAddr;
+ this.packetReader = packetReader;
+ }
+
+ /**
+ * Sends an ICMPv6 echo request message to the given {@link Inet6Address}.
+ *
+ * @param dstAddr the destination address of the packet.
+ * @throws IOException when it fails to send the packet.
+ */
+ public void sendEchoRequest(Inet6Address dstAddr) throws IOException {
+ ByteBuffer icmp6Packet = Ipv6Utils.buildEchoRequestPacket(ipv6Addr, dstAddr);
+ packetReader.sendResponse(icmp6Packet);
+ }
+
+ /**
+ * Sends an ICMPv6 Router Solicitation (RS) message to all routers on the network.
+ *
+ * @throws IOException when it fails to send the packet.
+ */
+ public void sendRsPacket() throws IOException {
+ ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, macAddr);
+ ByteBuffer rs =
+ Ipv6Utils.buildRsPacket(
+ (Inet6Address) InetAddresses.parseNumericAddress("fe80::1"),
+ IPV6_ADDR_ALL_ROUTERS_MULTICAST,
+ slla);
+ packetReader.sendResponse(rs);
+ }
+
+ /**
+ * Runs SLAAC to generate an IPv6 address for the device.
+ *
+ * <p>The devices sends an RS message, processes the received RA messages and generates an IPv6
+ * address if there's any available Prefix Information Option (PIO). For now it only generates
+ * one address in total and doesn't track the expiration.
+ *
+ * @param timeoutSeconds the number of seconds to wait for.
+ * @throws TimeoutException when the device fails to generate a SLAAC address in given timeout.
+ */
+ public void runSlaac(int timeoutSeconds) throws TimeoutException {
+ waitFor(() -> (ipv6Addr = runSlaac()) != null, timeoutSeconds, 5 /* intervalSeconds */);
+ }
+
+ private Inet6Address runSlaac() {
+ try {
+ sendRsPacket();
+
+ final byte[] raPacket = readPacketFrom(packetReader, p -> !getRaPios(p).isEmpty());
+
+ final List<PrefixInformationOption> options = getRaPios(raPacket);
+
+ for (PrefixInformationOption pio : options) {
+ if (pio.validLifetime > 0 && pio.preferredLifetime > 0) {
+ final byte[] addressBytes = pio.prefix;
+ addressBytes[addressBytes.length - 1] = (byte) (new Random()).nextInt();
+ addressBytes[addressBytes.length - 2] = (byte) (new Random()).nextInt();
+ return (Inet6Address) InetAddress.getByAddress(addressBytes);
+ }
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to generate an address by SLAAC", e);
+ }
+ return null;
+ }
+}
diff --git a/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java
new file mode 100644
index 0000000..9d9a4ff
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/IntegrationTestUtils.java
@@ -0,0 +1,221 @@
+/*
+ * 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.thread;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import android.net.TestNetworkInterface;
+import android.os.Handler;
+import android.os.SystemClock;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.testutils.HandlerUtils;
+import com.android.testutils.TapPacketReader;
+
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.io.FileDescriptor;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/** Static utility methods relating to Thread integration tests. */
+public final class IntegrationTestUtils {
+ private IntegrationTestUtils() {}
+
+ /**
+ * Waits for the given {@link Supplier} to be true until given timeout.
+ *
+ * <p>It checks the condition once every second.
+ *
+ * @param condition the condition to check.
+ * @param timeoutSeconds the number of seconds to wait for.
+ * @throws TimeoutException if the condition is not met after the timeout.
+ */
+ public static void waitFor(Supplier<Boolean> condition, int timeoutSeconds)
+ throws TimeoutException {
+ waitFor(condition, timeoutSeconds, 1);
+ }
+
+ /**
+ * Waits for the given {@link Supplier} to be true until given timeout.
+ *
+ * <p>It checks the condition once every {@code intervalSeconds}.
+ *
+ * @param condition the condition to check.
+ * @param timeoutSeconds the number of seconds to wait for.
+ * @param intervalSeconds the period to check the {@code condition}.
+ * @throws TimeoutException if the condition is still not met when the timeout expires.
+ */
+ public static void waitFor(Supplier<Boolean> condition, int timeoutSeconds, int intervalSeconds)
+ throws TimeoutException {
+ for (int i = 0; i < timeoutSeconds; i += intervalSeconds) {
+ if (condition.get()) {
+ return;
+ }
+ SystemClock.sleep(intervalSeconds * 1000L);
+ }
+ if (condition.get()) {
+ return;
+ }
+ throw new TimeoutException(
+ String.format(
+ "The condition failed to become true in %d seconds.", timeoutSeconds));
+ }
+
+ /**
+ * Creates a {@link TapPacketReader} given the {@link TestNetworkInterface} and {@link Handler}.
+ *
+ * @param testNetworkInterface the TUN interface of the test network.
+ * @param handler the handler to process the packets.
+ * @return the {@link TapPacketReader}.
+ */
+ public static TapPacketReader newPacketReader(
+ TestNetworkInterface testNetworkInterface, Handler handler) {
+ FileDescriptor fd = testNetworkInterface.getFileDescriptor().getFileDescriptor();
+ final TapPacketReader reader =
+ new TapPacketReader(handler, fd, testNetworkInterface.getMtu());
+ handler.post(() -> reader.start());
+ HandlerUtils.waitForIdle(handler, 5000 /* timeout in milliseconds */);
+ return reader;
+ }
+
+ /**
+ * Waits for the Thread module to enter any state of the given {@code deviceRoles}.
+ *
+ * @param controller the {@link ThreadNetworkController}.
+ * @param deviceRoles the desired device roles. See also {@link
+ * ThreadNetworkController.DeviceRole}.
+ * @param timeoutSeconds the number of seconds ot wait for.
+ * @return the {@link ThreadNetworkController.DeviceRole} after waiting.
+ * @throws TimeoutException if the device hasn't become any of expected roles until the timeout
+ * expires.
+ */
+ public static int waitForStateAnyOf(
+ ThreadNetworkController controller, List<Integer> deviceRoles, int timeoutSeconds)
+ throws TimeoutException {
+ SettableFuture<Integer> future = SettableFuture.create();
+ ThreadNetworkController.StateCallback callback =
+ newRole -> {
+ if (deviceRoles.contains(newRole)) {
+ future.set(newRole);
+ }
+ };
+ controller.registerStateCallback(directExecutor(), callback);
+ try {
+ int role = future.get(timeoutSeconds, TimeUnit.SECONDS);
+ controller.unregisterStateCallback(callback);
+ return role;
+ } catch (InterruptedException | ExecutionException e) {
+ throw new TimeoutException(
+ String.format(
+ "The device didn't become an expected role in %d seconds.",
+ timeoutSeconds));
+ }
+ }
+
+ /**
+ * Reads a packet from a given {@link TapPacketReader} that satisfies the {@code filter}.
+ *
+ * @param packetReader a TUN packet reader.
+ * @param filter the filter to be applied on the packet.
+ * @return the first IPv6 packet that satisfies the {@code filter}. If it has waited for more
+ * than 3000ms to read the next packet, the method will return null.
+ */
+ public static byte[] readPacketFrom(TapPacketReader packetReader, Predicate<byte[]> filter) {
+ byte[] packet;
+ while ((packet = packetReader.poll(3000 /* timeoutMs */)) != null) {
+ if (filter.test(packet)) return packet;
+ }
+ return null;
+ }
+
+ /** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
+ public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
+ if (packet == null) {
+ return false;
+ }
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ try {
+ if (Struct.parse(Ipv6Header.class, buf).nextHeader != (byte) IPPROTO_ICMPV6) {
+ return false;
+ }
+ return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
+ } catch (IllegalArgumentException ignored) {
+ // It's fine that the passed in packet is malformed because it's could be sent
+ // by anybody.
+ }
+ return false;
+ }
+
+ /** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
+ public static List<PrefixInformationOption> getRaPios(byte[] raMsg) {
+ final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
+
+ if (raMsg == null) {
+ return pioList;
+ }
+
+ final ByteBuffer buf = ByteBuffer.wrap(raMsg);
+ final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class, buf);
+ if (ipv6Header.nextHeader != (byte) IPPROTO_ICMPV6) {
+ return pioList;
+ }
+
+ final Icmpv6Header icmpv6Header = Struct.parse(Icmpv6Header.class, buf);
+ if (icmpv6Header.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) {
+ return pioList;
+ }
+
+ Struct.parse(RaHeader.class, buf);
+ while (buf.position() < raMsg.length) {
+ final int currentPos = buf.position();
+ final int type = Byte.toUnsignedInt(buf.get());
+ final int length = Byte.toUnsignedInt(buf.get());
+ if (type == ICMPV6_ND_OPTION_PIO) {
+ final ByteBuffer pioBuf =
+ ByteBuffer.wrap(
+ buf.array(),
+ currentPos,
+ Struct.getSize(PrefixInformationOption.class));
+ final PrefixInformationOption pio =
+ Struct.parse(PrefixInformationOption.class, pioBuf);
+ pioList.add(pio);
+
+ // Move ByteBuffer position to the next option.
+ buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
+ } else {
+ // The length is in units of 8 octets.
+ buf.position(currentPos + (length * 8));
+ }
+ }
+ return pioList;
+ }
+}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
index 2f120b2..75eb043 100644
--- a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -28,11 +28,6 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
-import android.net.thread.IActiveOperationalDatasetReceiver;
-import android.net.thread.IOperationReceiver;
-import android.net.thread.IOperationalDatasetCallback;
-import android.net.thread.IStateCallback;
-import android.net.thread.IThreadNetworkController;
import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
import android.net.thread.ThreadNetworkController.StateCallback;
import android.os.Binder;
@@ -111,6 +106,11 @@
return (IOperationReceiver) invocation.getArguments()[1];
}
+ private static IOperationReceiver getSetTestNetworkAsUpstreamReceiver(
+ InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[1];
+ }
+
private static IActiveOperationalDatasetReceiver getCreateDatasetReceiver(
InvocationOnMock invocation) {
return (IActiveOperationalDatasetReceiver) invocation.getArguments()[1];
@@ -359,4 +359,27 @@
assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
}
+
+ @Test
+ public void setTestNetworkAsUpstream_callbackIsInvokedWithCallingAppIdentity()
+ throws Exception {
+ setBinderUid(SYSTEM_UID);
+
+ AtomicInteger callbackUid = new AtomicInteger(0);
+
+ doAnswer(
+ invoke -> {
+ getSetTestNetworkAsUpstreamReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .setTestNetworkAsUpstream(anyString(), any(IOperationReceiver.class));
+ mController.setTestNetworkAsUpstream(
+ null, Runnable::run, v -> callbackUid.set(Binder.getCallingUid()));
+ mController.setTestNetworkAsUpstream(
+ new String("test0"), Runnable::run, v -> callbackUid.set(Binder.getCallingUid()));
+
+ assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(callbackUid.get()).isEqualTo(Process.myUid());
+ }
}