diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-01-10 19:01:31 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-01-10 19:01:31 +0000 |
commit | e9c8b2f20b66ddb691594a18bbf04caf425ddbcf (patch) | |
tree | 6d5d0ada3893fc87fe85b376058985989eb197e7 | |
parent | 9387031b2239b3e8a2c590a6453606e08b1be5fe (diff) | |
parent | 5bd0fdc28c0f738a1821276725b488f44a9585a7 (diff) | |
download | Connectivity-aml_tz5_341510010.tar.gz |
Snap for 11296156 from 5bd0fdc28c0f738a1821276725b488f44a9585a7 to mainline-tzdata5-releaseaml_tz5_341510070aml_tz5_341510050aml_tz5_341510010aml_tz5_341510010
Change-Id: I3d06b340dfd1c7b76380e1cc7daf8c87e7258bae
139 files changed, 8547 insertions, 1505 deletions
diff --git a/.gitignore b/.gitignore index c9b6393da3..b5176749fc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ # VS Code project **/.vscode **/*.code-workspace + +# Vim temporary files +**/*.swp diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp index a0b2434a38..7b52694b1a 100644 --- a/Cronet/tests/cts/Android.bp +++ b/Cronet/tests/cts/Android.bp @@ -62,6 +62,7 @@ android_test { test_suites: [ "cts", "general-tests", - "mts-tethering" + "mts-tethering", + "mcts-tethering", ], } diff --git a/DnsResolver/include/DnsHelperPublic.h b/DnsResolver/include/DnsHelperPublic.h index 7c9fc9e58e..44b00128d9 100644 --- a/DnsResolver/include/DnsHelperPublic.h +++ b/DnsResolver/include/DnsHelperPublic.h @@ -25,7 +25,8 @@ __BEGIN_DECLS * Perform any required initialization - including opening any required BPF maps. This function * needs to be called before using other functions of this library. * - * Returns 0 on success, a negative POSIX error code (see errno.h) on other failures. + * Returns 0 on success, -EOPNOTSUPP when the function is called on the Android version before + * T. Returns a negative POSIX error code (see errno.h) on other failures. */ int ADnsHelper_init(); @@ -36,7 +37,9 @@ int ADnsHelper_init(); * |uid| is a Linux/Android UID to be queried. It is a combination of UserID and AppID. * |metered| indicates whether the uid is currently using a billing network. * - * Returns 0(false)/1(true) on success, a negative POSIX error code (see errno.h) on other failures. + * Returns 0(false)/1(true) on success, -EUNATCH when the ADnsHelper_init is not called before + * calling this function. Returns a negative POSIX error code (see errno.h) on other failures + * that return from bpf syscall. */ int ADnsHelper_isUidNetworkingBlocked(uid_t uid, bool metered); diff --git a/Tethering/Android.bp b/Tethering/Android.bp index 414e50a103..73c11baae1 100644 --- a/Tethering/Android.bp +++ b/Tethering/Android.bp @@ -94,14 +94,17 @@ android_library { "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 @@ android_library { 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 @@ java_defaults { 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 @@ android_app { lint: { strict_updatability_linting: true, error_checks: ["NewApi"], + baseline_filename: "lint-baseline.xml", }, } @@ -239,19 +254,24 @@ sdk { 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 6e8d0c9ca2..bcea425c93 100644 --- a/Tethering/common/TetheringLib/Android.bp +++ b/Tethering/common/TetheringLib/Android.bp @@ -43,6 +43,7 @@ java_sdk_library { "//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/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java index 5e9bbcb5d1..50d6c4b18d 100644 --- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java +++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java @@ -18,7 +18,6 @@ package android.net.ip; 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.LinkAddress; 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 @@ public class RouterAdvertisementDaemon { 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 @@ public class RouterAdvertisementDaemon { @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 @@ public class RouterAdvertisementDaemon { } } - 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 @@ public class RouterAdvertisementDaemon { /** 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 @@ public class RouterAdvertisementDaemon { 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 @@ public class RouterAdvertisementDaemon { } } - 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; + } - return mRaMessageHandler.sendMessage( - Message.obtain(mRaMessageHandler, cmd, obj)); + maybeSendRA(mSolicitor); + } + } } // TODO: Consider moving this to run on a provided Looper as a Handler, diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java index 5022b40525..552b105947 100644 --- a/Tethering/src/com/android/networkstack/tethering/Tethering.java +++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java @@ -136,6 +136,7 @@ import com.android.internal.util.StateMachine; import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.BaseNetdUnsolicitedEventListener; import com.android.net.module.util.CollectionUtils; +import com.android.net.module.util.HandlerUtils; import com.android.net.module.util.NetdUtils; import com.android.net.module.util.SdkUtil.LateSdk; import com.android.net.module.util.SharedLog; @@ -161,11 +162,8 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; /** * @@ -2694,31 +2692,10 @@ public class Tethering { return; } - final CountDownLatch latch = new CountDownLatch(1); - - // Don't crash the system if something in doDump throws an exception, but try to propagate - // the exception to the caller. - AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>(); - mHandler.post(() -> { - try { - doDump(fd, writer, args); - } catch (RuntimeException e) { - exceptionRef.set(e); - } - latch.countDown(); - }); - - try { - if (!latch.await(DUMP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { - writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms"); - return; - } - } catch (InterruptedException e) { - exceptionRef.compareAndSet(null, new IllegalStateException("Dump interrupted", e)); + if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> doDump(fd, writer, args), + DUMP_TIMEOUT_MS)) { + writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms"); } - - final RuntimeException e = exceptionRef.get(); - if (e != null) throw e; } private void maybeDhcpLeasesChanged() { diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java index 377da91ca4..c2326974a4 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.isAddressIpv4; 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.Assert.fail; 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.TetheringEventCallback; 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 @@ public abstract class EthernetTetheringTestBase { 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 @@ public abstract class EthernetTetheringTestBase { private MyTetheringEventCallback mTetheringEventCallback; public Context getContext() { - return mContext; + return sContext; } @BeforeClass @@ -170,19 +170,24 @@ public abstract class EthernetTetheringTestBase { // 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 @@ public abstract class EthernetTetheringTestBase { 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 @@ public abstract class EthernetTetheringTestBase { } } - 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 @@ public abstract class EthernetTetheringTestBase { } } - 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 @@ public abstract class EthernetTetheringTestBase { protected void stopEthernetTethering(final MyTetheringEventCallback callback) { runAsShell(TETHER_PRIVILEGED, () -> { - mTm.stopTethering(TETHERING_ETHERNET); + sTm.stopTethering(TETHERING_ETHERNET); maybeUnregisterTetheringEventCallback(callback); }); } @@ -277,18 +282,18 @@ public abstract class EthernetTetheringTestBase { } } - 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 @@ public abstract class EthernetTetheringTestBase { } } - 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 @@ public abstract class EthernetTetheringTestBase { 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 @@ public abstract class EthernetTetheringTestBase { 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 @@ public abstract class EthernetTetheringTestBase { // 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 @@ public abstract class EthernetTetheringTestBase { 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 @@ public abstract class EthernetTetheringTestBase { } } - 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 @@ public abstract class EthernetTetheringTestBase { }; 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 @@ public abstract class EthernetTetheringTestBase { 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 @@ public abstract class EthernetTetheringTestBase { } 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 @@ public abstract class EthernetTetheringTestBase { 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 @@ public abstract class EthernetTetheringTestBase { } } - 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 @@ public abstract class EthernetTetheringTestBase { 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 @@ public abstract class EthernetTetheringTestBase { 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 @@ public abstract class EthernetTetheringTestBase { // 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 @@ public abstract class EthernetTetheringTestBase { } }; 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 @@ public abstract class EthernetTetheringTestBase { 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/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java index 82b8845099..750bfce951 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java @@ -2810,12 +2810,10 @@ public class TetheringTest { final FileDescriptor mockFd = mock(FileDescriptor.class); final PrintWriter mockPw = mock(PrintWriter.class); runUsbTethering(null); - mLooper.startAutoDispatch(); mTethering.dump(mockFd, mockPw, new String[0]); verify(mConfig).dump(any()); verify(mEntitleMgr).dump(any()); verify(mOffloadCtrl).dump(any()); - mLooper.stopAutoDispatch(); } @Test diff --git a/common/Android.bp b/common/Android.bp index 6d04b6ca6d..f2f3929e11 100644 --- a/common/Android.bp +++ b/common/Android.bp @@ -19,10 +19,6 @@ package { default_applicable_licenses: ["Android-Apache-2.0"], } -// 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 59874c216d..0000000000 --- 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 6c3cdb1c42..7d2c563c56 100644 --- a/framework-t/Android.bp +++ b/framework-t/Android.bp @@ -57,6 +57,10 @@ java_defaults { "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 @@ java_sdk_library { "//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 fb46ee7826..60a88c045b 100644 --- a/framework-t/api/current.txt +++ b/framework-t/api/current.txt @@ -275,6 +275,7 @@ package android.net.nsd { 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); @@ -283,6 +284,7 @@ package android.net.nsd { 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 d89964d391..d7cff2cfa2 100644 --- a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java +++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java @@ -27,6 +27,8 @@ import android.net.nsd.NsdManager; 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 @@ public final class ConnectivityFrameworkInitializerTiramisu { } ); - 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/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java index b6f6dbb1cb..934b4c6a0e 100644 --- a/framework-t/src/android/net/NetworkStatsCollection.java +++ b/framework-t/src/android/net/NetworkStatsCollection.java @@ -60,6 +60,7 @@ import android.util.proto.ProtoOutputStream; 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 @@ public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.W 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 @@ public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.W /** @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 @@ public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.W * @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 @@ public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.W 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 0000000000..b1ef98f73f --- /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 e671db15e8..b03eb297a2 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 @@ import android.os.Messenger; * {@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 bf01a9d633..b4f2be9de5 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.text.TextUtils; 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.lang.annotation.RetentionPolicy; 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 final class NsdManager { 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 @@ public final class NsdManager { 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 @@ public final class NsdManager { 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 final class NsdManager { */ 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 caeecdda86..ac4ea2318e 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.Parcel; 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.ArrayList; 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 @@ public final class NsdServiceInfo implements Parcelable { private String mServiceType; - private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>(); + private final Set<String> mSubtypes; + + private final ArrayMap<String, byte[]> mTxtRecord; - private final List<InetAddress> mHostAddresses = new ArrayList<>(); + private final List<InetAddress> mHostAddresses; private int mPort; @@ -60,14 +67,34 @@ public final class NsdServiceInfo implements Parcelable { 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 @@ public final class NsdServiceInfo implements Parcelable { 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 final class NsdServiceInfo implements Parcelable { 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 @@ public final class NsdServiceInfo implements Parcelable { 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 1e6262d655..f3d8689bed 100644 --- a/framework/Android.bp +++ b/framework/Android.bp @@ -105,7 +105,9 @@ java_defaults { apex_available: [ "com.android.tethering", ], - lint: { strict_updatability_linting: true }, + lint: { + strict_updatability_linting: true, + }, } java_library { @@ -134,7 +136,10 @@ java_library { "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 @@ java_sdk_library { "//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 @@ java_library { apex_available: [ "com.android.tethering", ], + lint: { + baseline_filename: "lint-baseline.xml", + }, } java_genrule { @@ -293,9 +305,9 @@ droidstubs { ], 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 @@ droidstubs { 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 @@ java_library { 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 0000000000..2848074634 --- /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 4ab6d3e0b6..ee422abd9c 100644 --- a/framework/src/android/net/BpfNetMapsReader.java +++ b/framework/src/android/net/BpfNetMapsReader.java @@ -36,7 +36,6 @@ import android.os.Build; 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 class BpfNetMapsReader { 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/nearby/README.md b/nearby/README.md index 0d265638ba..8dac61caa1 100644 --- a/nearby/README.md +++ b/nearby/README.md @@ -47,8 +47,17 @@ Then, add the jar in IDE as below. ## 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 diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp index 46309026b6..17b80b0d84 100644 --- a/nearby/service/Android.bp +++ b/nearby/service/Android.bp @@ -30,7 +30,7 @@ java_library { srcs: [":nearby-service-srcs"], defaults: [ - "framework-system-server-module-defaults" + "framework-system-server-module-defaults", ], libs: [ "androidx.annotation_annotation", @@ -66,13 +66,16 @@ java_library { 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/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp index 66a1ffe236..4309d7ed25 100644 --- a/nearby/tests/cts/fastpair/Android.bp +++ b/nearby/tests/cts/fastpair/Android.bp @@ -39,6 +39,7 @@ android_test { "cts", "general-tests", "mts-tethering", + "mcts-tethering", ], certificate: "platform", sdk_version: "module_current", diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp index 112c75109f..bbf42c7c9c 100644 --- a/nearby/tests/unit/Android.bp +++ b/nearby/tests/unit/Android.bp @@ -43,7 +43,6 @@ android_test { "platform-test-annotations", "service-nearby-pre-jarjar", "truth", - // "Robolectric_all-target", ], // these are needed for Extended Mockito jni_libs: [ diff --git a/netd/Android.bp b/netd/Android.bp index 4325d89aeb..3cdbc9780c 100644 --- a/netd/Android.bp +++ b/netd/Android.bp @@ -69,10 +69,10 @@ cc_test { "BpfBaseTest.cpp" ], static_libs: [ + "libbase", "libnetd_updatable", ], shared_libs: [ - "libbase", "libcutils", "liblog", "libnetdutils", diff --git a/netd/NetdUpdatable.cpp b/netd/NetdUpdatable.cpp index 8b9e5a759c..3b15916398 100644 --- a/netd/NetdUpdatable.cpp +++ b/netd/NetdUpdatable.cpp @@ -31,8 +31,8 @@ int libnetd_updatable_init(const char* cg2_path) { 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 7e588cd412..bd2f916c32 100644 --- a/service-t/Android.bp +++ b/service-t/Android.bp @@ -31,6 +31,7 @@ filegroup { ], 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 @@ java_library { "//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 @@ java_library { 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 @@ droidstubs { 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/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp index bdbb655e5c..81912ae2cd 100644 --- a/service-t/jni/com_android_server_net_NetworkStatsService.cpp +++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp @@ -34,77 +34,64 @@ using android::bpf::bpfGetUidStats; using android::bpf::bpfGetIfaceStats; -using android::bpf::bpfGetIfIndexStats; using android::bpf::NetworkTraceHandler; namespace android { -// NOTE: keep these in sync with TrafficStats.java -static const uint64_t UNKNOWN = -1; - -enum StatsType { - RX_BYTES = 0, - RX_PACKETS = 1, - TX_BYTES = 2, - TX_PACKETS = 3, -}; +static jobject statsValueToEntry(JNIEnv* env, StatsValue* stats) { + // Find the Java class that represents the structure + jclass gEntryClass = env->FindClass("android/net/NetworkStats$Entry"); + if (gEntryClass == nullptr) { + return nullptr; + } -static uint64_t getStatsType(StatsValue* stats, StatsType type) { - switch (type) { - case RX_BYTES: - return stats->rxBytes; - case RX_PACKETS: - return stats->rxPackets; - case TX_BYTES: - return stats->txBytes; - case TX_PACKETS: - return stats->txPackets; - default: - return UNKNOWN; + // Create a new instance of the Java class + jobject result = env->AllocObject(gEntryClass); + if (result == nullptr) { + return nullptr; } + + // Set the values of the structure fields in the Java object + env->SetLongField(result, env->GetFieldID(gEntryClass, "rxBytes", "J"), stats->rxBytes); + env->SetLongField(result, env->GetFieldID(gEntryClass, "txBytes", "J"), stats->txBytes); + env->SetLongField(result, env->GetFieldID(gEntryClass, "rxPackets", "J"), stats->rxPackets); + env->SetLongField(result, env->GetFieldID(gEntryClass, "txPackets", "J"), stats->txPackets); + + return result; } -static jlong nativeGetTotalStat(JNIEnv* env, jclass clazz, jint type) { +static jobject nativeGetTotalStat(JNIEnv* env, jclass clazz) { StatsValue stats = {}; if (bpfGetIfaceStats(NULL, &stats) == 0) { - return getStatsType(&stats, (StatsType) type); + return statsValueToEntry(env, &stats); } else { - return UNKNOWN; + return nullptr; } } -static jlong nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface, jint type) { +static jobject nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface) { ScopedUtfChars iface8(env, iface); if (iface8.c_str() == NULL) { - return UNKNOWN; + return nullptr; } StatsValue stats = {}; if (bpfGetIfaceStats(iface8.c_str(), &stats) == 0) { - return getStatsType(&stats, (StatsType) type); - } else { - return UNKNOWN; - } -} - -static jlong nativeGetIfIndexStat(JNIEnv* env, jclass clazz, jint ifindex, jint type) { - StatsValue stats = {}; - if (bpfGetIfIndexStats(ifindex, &stats) == 0) { - return getStatsType(&stats, (StatsType) type); + return statsValueToEntry(env, &stats); } else { - return UNKNOWN; + return nullptr; } } -static jlong nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) { +static jobject nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid) { StatsValue stats = {}; if (bpfGetUidStats(uid, &stats) == 0) { - return getStatsType(&stats, (StatsType) type); + return statsValueToEntry(env, &stats); } else { - return UNKNOWN; + return nullptr; } } @@ -113,11 +100,26 @@ static void nativeInitNetworkTracing(JNIEnv* env, jclass clazz) { } static const JNINativeMethod gMethods[] = { - {"nativeGetTotalStat", "(I)J", (void*)nativeGetTotalStat}, - {"nativeGetIfaceStat", "(Ljava/lang/String;I)J", (void*)nativeGetIfaceStat}, - {"nativeGetIfIndexStat", "(II)J", (void*)nativeGetIfIndexStat}, - {"nativeGetUidStat", "(II)J", (void*)nativeGetUidStat}, - {"nativeInitNetworkTracing", "()V", (void*)nativeInitNetworkTracing}, + { + "nativeGetTotalStat", + "()Landroid/net/NetworkStats$Entry;", + (void*)nativeGetTotalStat + }, + { + "nativeGetIfaceStat", + "(Ljava/lang/String;)Landroid/net/NetworkStats$Entry;", + (void*)nativeGetIfaceStat + }, + { + "nativeGetUidStat", + "(I)Landroid/net/NetworkStats$Entry;", + (void*)nativeGetUidStat + }, + { + "nativeInitNetworkTracing", + "()V", + (void*)nativeInitNetworkTracing + }, }; int register_android_server_net_NetworkStatsService(JNIEnv* env) { diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp index 0dfd0afa92..b9f3adb91c 100644 --- a/service-t/native/libs/libnetworkstats/Android.bp +++ b/service-t/native/libs/libnetworkstats/Android.bp @@ -73,6 +73,7 @@ cc_test { "-Wthread-safety", ], static_libs: [ + "libbase", "libgmock", "libnetworkstats", "libperfetto_client_experimental", @@ -80,7 +81,6 @@ cc_test { "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 0000000000..3ed21a2b61 --- /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/NsdService.java b/service-t/src/com/android/server/NsdService.java index 2640332b33..b7fd9a8892 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.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; 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.GetAddressInfo; 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.SocketException; 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 @@ public class NsdService extends INsdManager.Stub { "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 @@ public class NsdService extends INsdManager.Stub { 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; @@ -536,6 +546,11 @@ public class NsdService extends INsdManager.Stub { if (DBG) Log.d(TAG, "Daemon is already started."); return; } + + if (mMDnsManager == null) { + Log.wtf(TAG, "maybeStartDaemon: mMDnsManager is null"); + return; + } mMDnsManager.registerEventListener(mMDnsEventCallback); mMDnsManager.startDaemon(); mIsDaemonStarted = true; @@ -548,6 +563,11 @@ public class NsdService extends INsdManager.Stub { if (DBG) Log.d(TAG, "Daemon has not been started."); return; } + + if (mMDnsManager == null) { + Log.wtf(TAG, "maybeStopDaemon: mMDnsManager is null"); + return; + } mMDnsManager.unregisterEventListener(mMDnsEventCallback); mMDnsManager.stopDaemon(); mIsDaemonStarted = false; @@ -688,17 +708,45 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { .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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { servInfo, network == null ? INetd.LOCAL_NET_ID : network.netId, serviceInfo.getInterfaceIndex()); + servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes())); return servInfo; } @@ -1581,34 +1666,36 @@ public class NsdService extends INsdManager.Stub { * 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { } } + 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 @@ public class NsdService extends INsdManager.Stub { @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 @@ public class NsdService extends INsdManager.Stub { @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 class NsdService extends INsdManager.Stub { 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 @@ public class NsdService extends INsdManager.Stub { 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)); - // 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 (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); } - 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { } 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 @@ public class NsdService extends INsdManager.Stub { } 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 fc0e11b025..135d957948 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.Collections; 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 @@ public class MdnsAdvertiser { 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 @@ public class MdnsAdvertiser { 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 @@ public class MdnsAdvertiser { 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 @@ public class MdnsAdvertiser { 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 @@ public class MdnsAdvertiser { } /** - * 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 @@ public class MdnsAdvertiser { // 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 class MdnsAdvertiser { public NsdServiceInfo getServiceInfo() { return mServiceInfo; } - - @Nullable - public String getSubtype() { - return mSubtype; - } } /** @@ -665,14 +649,14 @@ public class MdnsAdvertiser { * * @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 @@ public class MdnsAdvertiser { 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 @@ public class MdnsAdvertiser { 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 @@ public class MdnsAdvertiser { 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 0a6d8c130b..1ad47a30b7 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 class MdnsFeatureFlags { */ 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 @@ public class MdnsFeatureFlags { // 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 @@ public class MdnsFeatureFlags { private boolean mIncludeInetAddressRecordsInProbing; private boolean mIsExpiredServicesRemovalEnabled; private boolean mIsLabelCountLimitEnabled; + private boolean mIsKnownAnswerSuppressionEnabled; /** * The constructor for {@link Builder}. @@ -88,6 +99,7 @@ public class MdnsFeatureFlags { mIncludeInetAddressRecordsInProbing = false; mIsExpiredServicesRemovalEnabled = false; mIsLabelCountLimitEnabled = true; // Default enabled. + mIsKnownAnswerSuppressionEnabled = false; } /** @@ -132,13 +144,24 @@ public class MdnsFeatureFlags { } /** + * 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 463df63914..aa40c9227d 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 com.android.server.connectivity.mdns.util.MdnsUtils; 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 @@ public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHand * 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 @@ public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHand * * @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 48ece68311..6b6632cd6e 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.Arrays; 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 @@ public class MdnsRecordRepository { // 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 @@ public class MdnsRecordRepository { 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 @@ public class MdnsRecordRepository { * 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 class MdnsRecordRepository { 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 @@ public class MdnsRecordRepository { 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 @@ public class MdnsRecordRepository { 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 @@ public class MdnsRecordRepository { * @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,16 @@ public class MdnsRecordRepository { * Update a service that already registered in the repository. * * @param serviceId An existing service ID. - * @param subtype A new subtype - * @return + * @param subtypes New subtypes */ - 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 +343,7 @@ public class MdnsRecordRepository { * 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 +356,7 @@ public class MdnsRecordRepository { } 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 +500,17 @@ public class MdnsRecordRepository { 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 +518,33 @@ public class MdnsRecordRepository { 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 +591,15 @@ public class MdnsRecordRepository { 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 +608,15 @@ public class MdnsRecordRepository { @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 +639,20 @@ public class MdnsRecordRepository { } 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 +664,6 @@ public class MdnsRecordRepository { continue; } - // TODO: Don't reply if in known answers of the querier (7.1) if TTL is > half - answerInfo.add(info); } @@ -636,7 +672,7 @@ public class MdnsRecordRepository { // 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 +680,14 @@ public class MdnsRecordRepository { // 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 +706,6 @@ public class MdnsRecordRepository { } } } - - for (RecordInfo<?> info : additionalAnswerInfo) { - additionalAnswerRecords.add(info.record); - } - - // RFC6762 6.1: negative responses - addNsecRecordsForUniqueNames(additionalAnswerRecords, - answerInfo.listIterator(answersStartIndex), - additionalAnswerInfo.listIterator()); return true; } @@ -694,7 +722,7 @@ public class MdnsRecordRepository { * 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 +738,12 @@ public class MdnsRecordRepository { 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 +761,27 @@ public class MdnsRecordRepository { } } + /** 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 +796,10 @@ public class MdnsRecordRepository { 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 +984,7 @@ public class MdnsRecordRepository { 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 ea3af5e83b..651b643a3c 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.Handler; 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 @@ public class MdnsReplySender { @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 class MdnsReplySender { 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 class MdnsReplySender { */ 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 32f604e8d5..df0a04042e 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 @@ public class MdnsServiceTypeClient { } 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 1482ebb7a0..8fc81148a4 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 @@ public class MdnsUtils { && 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 48e86d8e7e..458d64f36f 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 android.util.Log; 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 @@ public class EthernetTracker { 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 3da15856b7..8ee8591a4f 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.net.TrafficStats.MB_IN_BYTES; 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.NetworkTemplate; 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 @@ public class NetworkStatsRecorder { 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 @@ public class NetworkStatsRecorder { 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 @@ public class NetworkStatsRecorder { 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 class NetworkStatsRecorder { */ 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 @@ public class NetworkStatsRecorder { 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 @@ public class NetworkStatsRecorder { 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 @@ public class NetworkStatsRecorder { } 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 2c9f30c8f7..eb754616a6 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.STATS_PER_UID; 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 @@ public class NetworkStatsService extends INetworkStatsService.Stub { 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 @@ public class NetworkStatsService extends INetworkStatsService.Stub { 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 @@ public class NetworkStatsService extends INetworkStatsService.Stub { } /** + * 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 @@ public class NetworkStatsService extends INetworkStatsService.Stub { 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 @@ public class NetworkStatsService extends INetworkStatsService.Stub { 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 @@ public class NetworkStatsService extends INetworkStatsService.Stub { 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 @@ public class NetworkStatsService extends INetworkStatsService.Stub { 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 @@ public class NetworkStatsService extends INetworkStatsService.Stub { if (runComparison) { final boolean success = - compareImportedToLegacyStats(migration, legacyRecorders[i]); + compareImportedToLegacyStats(migration, legacyRecorders[i], + true /* allowKeyChange */); if (!success && !dryRunImportOnly) { tryIncrementLegacyFallbacksCounter(); } @@ -1243,7 +1355,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { * 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 @@ public class NetworkStatsService extends INetworkStatsService.Stub { // 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); @@ -1868,36 +1981,56 @@ public class NetworkStatsService extends INetworkStatsService.Stub { if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) { return UNSUPPORTED; } - return nativeGetUidStat(uid, type); + return getEntryValueForType(nativeGetUidStat(uid), type); } @Override public long getIfaceStats(@NonNull String iface, int type) { Objects.requireNonNull(iface); - long nativeIfaceStats = nativeGetIfaceStat(iface, type); - if (nativeIfaceStats == -1) { - return nativeIfaceStats; + final NetworkStats.Entry entry = nativeGetIfaceStat(iface); + final long value = getEntryValueForType(entry, type); + if (value == UNSUPPORTED) { + return UNSUPPORTED; } else { // When tethering offload is in use, nativeIfaceStats does not contain usage from // offload, add it back here. Note that the included statistics might be stale // since polling newest stats from hardware might impact system health and not // suitable for TrafficStats API use cases. - return nativeIfaceStats + getProviderIfaceStats(iface, type); + entry.add(getProviderIfaceStats(iface)); + return getEntryValueForType(entry, type); + } + } + + private long getEntryValueForType(@Nullable NetworkStats.Entry entry, int type) { + if (entry == null) return UNSUPPORTED; + switch (type) { + case TrafficStats.TYPE_RX_BYTES: + return entry.rxBytes; + case TrafficStats.TYPE_TX_BYTES: + return entry.txBytes; + case TrafficStats.TYPE_RX_PACKETS: + return entry.rxPackets; + case TrafficStats.TYPE_TX_PACKETS: + return entry.txPackets; + default: + return UNSUPPORTED; } } @Override public long getTotalStats(int type) { - long nativeTotalStats = nativeGetTotalStat(type); - if (nativeTotalStats == -1) { - return nativeTotalStats; + final NetworkStats.Entry entry = nativeGetTotalStat(); + final long value = getEntryValueForType(entry, type); + if (value == UNSUPPORTED) { + return UNSUPPORTED; } else { // Refer to comment in getIfaceStats - return nativeTotalStats + getProviderIfaceStats(IFACE_ALL, type); + entry.add(getProviderIfaceStats(IFACE_ALL)); + return getEntryValueForType(entry, type); } } - private long getProviderIfaceStats(@Nullable String iface, int type) { + private NetworkStats.Entry getProviderIfaceStats(@Nullable String iface) { final NetworkStats providerSnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE); final HashSet<String> limitIfaces; if (iface == IFACE_ALL) { @@ -1906,19 +2039,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { limitIfaces = new HashSet<>(); limitIfaces.add(iface); } - final NetworkStats.Entry entry = providerSnapshot.getTotal(null, limitIfaces); - switch (type) { - case TrafficStats.TYPE_RX_BYTES: - return entry.rxBytes; - case TrafficStats.TYPE_RX_PACKETS: - return entry.rxPackets; - case TrafficStats.TYPE_TX_BYTES: - return entry.txBytes; - case TrafficStats.TYPE_TX_PACKETS: - return entry.txPackets; - default: - return 0; - } + return providerSnapshot.getTotal(null, limitIfaces); } /** @@ -2639,6 +2760,17 @@ public class NetworkStatsService extends INetworkStatsService.Stub { } } 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(); @@ -3274,10 +3406,13 @@ public class NetworkStatsService extends INetworkStatsService.Stub { } } - private static native long nativeGetTotalStat(int type); - private static native long nativeGetIfaceStat(String iface, int type); - private static native long nativeGetIfIndexStat(int ifindex, int type); - private static native long nativeGetUidStat(int uid, int type); + // TODO: Read stats by using BpfNetMapsReader. + @Nullable + private static native NetworkStats.Entry nativeGetTotalStat(); + @Nullable + private static native NetworkStats.Entry nativeGetIfaceStat(String iface); + @Nullable + private static native NetworkStats.Entry nativeGetUidStat(int uid); /** Initializes and registers the Perfetto Network Trace data source */ public static native void nativeInitNetworkTracing(); diff --git a/service/Android.bp b/service/Android.bp index ae5c222b69..a81386c84e 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -70,6 +70,9 @@ java_library { 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 @@ java_library { 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 @@ java_library { ], lint: { strict_updatability_linting: true, + baseline_filename: "lint-baseline.xml", }, } @@ -283,12 +290,18 @@ java_defaults { 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 @@ java_library_static { ], 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 f30abc6b0b..6f9d46f36c 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/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml new file mode 100644 index 0000000000..14b54275c1 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values/config_thread.xml @@ -0,0 +1,29 @@ +<?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. +--> + +<!-- These resources are around just to allow their values to be customized + for different hardware and product builds for Thread Network. All + configuration names should use the "config_thread" prefix. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Whether to use location APIs in the algorithm to determine country code or not. + If disabled, will use other sources (telephony, wifi, etc) to determine device location for + Thread Network regulatory purposes. + --> + <bool name="config_thread_location_use_for_country_code_enabled">true</bool> + +</resources> diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml index 4c85e8c30f..1c0759999c 100644 --- a/service/ServiceConnectivityResources/res/values/overlayable.xml +++ b/service/ServiceConnectivityResources/res/values/overlayable.xml @@ -43,6 +43,9 @@ <item type="string" name="config_ethernet_iface_regex"/> <item type="integer" name="config_validationFailureAfterRoamIgnoreTimeMillis" /> <item type="integer" name="config_netstats_validate_import" /> + + <!-- Configuration values for ThreadNetworkService --> + <item type="bool" name="config_thread_location_use_for_country_code_enabled" /> </policy> </overlayable> </resources> diff --git a/service/src/com/android/metrics/NetworkRequestStateInfo.java b/service/src/com/android/metrics/NetworkRequestStateInfo.java new file mode 100644 index 0000000000..e3e172abd2 --- /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 0000000000..361ad22a2b --- /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 0ec0f13154..3b31ed2f9b 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.NetworkCountPerTransports; 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; @@ -290,6 +291,7 @@ import com.android.net.module.util.BitUtils; import com.android.net.module.util.BpfUtils; import com.android.net.module.util.CollectionUtils; import com.android.net.module.util.DeviceConfigUtils; +import com.android.net.module.util.HandlerUtils; import com.android.net.module.util.InterfaceParams; import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult; import com.android.net.module.util.LinkPropertiesUtils.CompareResult; @@ -314,7 +316,6 @@ import com.android.server.connectivity.DnsManager; import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate; import com.android.server.connectivity.DscpPolicyTracker; import com.android.server.connectivity.FullScore; -import com.android.server.connectivity.HandlerUtils; import com.android.server.connectivity.InvalidTagException; import com.android.server.connectivity.KeepaliveResourceUtil; import com.android.server.connectivity.KeepaliveTracker; @@ -941,6 +942,8 @@ public class ConnectivityService extends IConnectivityManager.Stub private final IpConnectivityLog mMetricsLog; + private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics; + @GuardedBy("mBandwidthRequests") private final SparseArray<Integer> mBandwidthRequests = new SparseArray<>(10); @@ -1277,7 +1280,7 @@ public class ConnectivityService extends IConnectivityManager.Stub LocalPriorityDump() {} private void dumpHigh(FileDescriptor fd, PrintWriter pw) { - if (!HandlerUtils.runWithScissors(mHandler, () -> { + if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> { doDump(fd, pw, new String[]{DIAG_ARG}); doDump(fd, pw, new String[]{SHORT_ARG}); }, DUMPSYS_DEFAULT_TIMEOUT_MS)) { @@ -1286,7 +1289,7 @@ public class ConnectivityService extends IConnectivityManager.Stub } private void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args) { - if (!HandlerUtils.runWithScissors(mHandler, () -> doDump(fd, pw, args), + if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> doDump(fd, pw, args), DUMPSYS_DEFAULT_TIMEOUT_MS)) { pw.println("dumpNormal timeout"); } @@ -1422,6 +1425,19 @@ public class ConnectivityService extends IConnectivityManager.Stub } /** + * @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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub updateSignalStrengthThresholds(network, "REGISTER", req); } } + } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) { + mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(req); } } @@ -5541,6 +5560,8 @@ public class ConnectivityService extends IConnectivityManager.Stub } 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 8036ae91b5..94ba9de498 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.AF_INET6; 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 @@ import java.util.Set; */ 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 @@ public class AutomaticOnOffKeepaliveTracker { try { while (NetlinkUtils.enoughBytesRemainForValidNlMsg(bytes)) { - final int startPos = bytes.position(); - - 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); + // 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 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 @@ public class AutomaticOnOffKeepaliveTracker { 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/HandlerUtils.java b/service/src/com/android/server/connectivity/HandlerUtils.java deleted file mode 100644 index 997ecbfb40..0000000000 --- a/service/src/com/android/server/connectivity/HandlerUtils.java +++ /dev/null @@ -1,139 +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. - */ - -package com.android.server.connectivity; - -import android.annotation.NonNull; -import android.os.Handler; -import android.os.Looper; -import android.os.SystemClock; - -/** - * Helper class for Handler related utilities. - * - * @hide - */ -public class HandlerUtils { - // Note: @hide methods copied from android.os.Handler - /** - * Runs the specified task synchronously. - * <p> - * If the current thread is the same as the handler thread, then the runnable - * runs immediately without being enqueued. Otherwise, posts the runnable - * to the handler and waits for it to complete before returning. - * </p><p> - * This method is dangerous! Improper use can result in deadlocks. - * Never call this method while any locks are held or use it in a - * possibly re-entrant manner. - * </p><p> - * This method is occasionally useful in situations where a background thread - * must synchronously await completion of a task that must run on the - * handler's thread. However, this problem is often a symptom of bad design. - * Consider improving the design (if possible) before resorting to this method. - * </p><p> - * One example of where you might want to use this method is when you just - * set up a Handler thread and need to perform some initialization steps on - * it before continuing execution. - * </p><p> - * If timeout occurs then this method returns <code>false</code> but the runnable - * will remain posted on the handler and may already be in progress or - * complete at a later time. - * </p><p> - * When using this method, be sure to use {@link Looper#quitSafely} when - * quitting the looper. Otherwise {@link #runWithScissors} may hang indefinitely. - * (TODO: We should fix this by making MessageQueue aware of blocking runnables.) - * </p> - * - * @param h The target handler. - * @param r The Runnable that will be executed synchronously. - * @param timeout The timeout in milliseconds, or 0 to wait indefinitely. - * - * @return Returns true if the Runnable was successfully executed. - * Returns false on failure, usually because the - * looper processing the message queue is exiting. - * - * @hide This method is prone to abuse and should probably not be in the API. - * If we ever do make it part of the API, we might want to rename it to something - * less funny like runUnsafe(). - */ - public static boolean runWithScissors(@NonNull Handler h, @NonNull Runnable r, long timeout) { - if (r == null) { - throw new IllegalArgumentException("runnable must not be null"); - } - if (timeout < 0) { - throw new IllegalArgumentException("timeout must be non-negative"); - } - - if (Looper.myLooper() == h.getLooper()) { - r.run(); - return true; - } - - BlockingRunnable br = new BlockingRunnable(r); - return br.postAndWait(h, timeout); - } - - private static final class BlockingRunnable implements Runnable { - private final Runnable mTask; - private boolean mDone; - - BlockingRunnable(Runnable task) { - mTask = task; - } - - @Override - public void run() { - try { - mTask.run(); - } finally { - synchronized (this) { - mDone = true; - notifyAll(); - } - } - } - - public boolean postAndWait(Handler handler, long timeout) { - if (!handler.post(this)) { - return false; - } - - synchronized (this) { - if (timeout > 0) { - final long expirationTime = SystemClock.uptimeMillis() + timeout; - while (!mDone) { - long delay = expirationTime - SystemClock.uptimeMillis(); - if (delay <= 0) { - return false; // timeout - } - try { - wait(delay); - } catch (InterruptedException ex) { - } - } - } else { - while (!mDone) { - try { - wait(); - } catch (InterruptedException ex) { - } - } - } - } - return true; - } - } -} diff --git a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java index 3350d2d8b2..742a2ccd60 100644 --- a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java +++ b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java @@ -171,7 +171,8 @@ public class RoutingCoordinatorService extends IRoutingCoordinator.Stub { } 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/Android.bp b/staticlibs/Android.bp index 9f1debc1ac..8f018c004f 100644 --- a/staticlibs/Android.bp +++ b/staticlibs/Android.bp @@ -43,6 +43,7 @@ java_library { "device/com/android/net/module/util/SharedLog.java", "device/com/android/net/module/util/SocketUtils.java", "device/com/android/net/module/util/FeatureVersions.java", + "device/com/android/net/module/util/HandlerUtils.java", // This library is used by system modules, for which the system health impact of Kotlin // has not yet been evaluated. Annotations may need jarjar'ing. // "src_devicecommon/**/*.kt", @@ -295,6 +296,10 @@ java_library { "-Xep:NullablePrimitive:ERROR", ], }, + apex_available: [ + "//apex_available:platform", + "com.android.tethering", + ], } java_library { diff --git a/staticlibs/device/com/android/net/module/util/HandlerUtils.java b/staticlibs/device/com/android/net/module/util/HandlerUtils.java new file mode 100644 index 0000000000..c620368fc2 --- /dev/null +++ b/staticlibs/device/com/android/net/module/util/HandlerUtils.java @@ -0,0 +1,105 @@ +/* + * 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; + +import android.annotation.NonNull; +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Helper class for Handler related utilities. + * + * @hide + */ +public class HandlerUtils { + /** + * Runs the specified task synchronously for dump method. + * <p> + * If the current thread is the same as the handler thread, then the runnable + * runs immediately without being enqueued. Otherwise, posts the runnable + * to the handler and waits for it to complete before returning. + * </p><p> + * This method is dangerous! Improper use can result in deadlocks. + * Never call this method while any locks are held or use it in a + * possibly re-entrant manner. + * </p><p> + * This method is made to let dump method access members on the handler thread to + * avoid concurrent access problems or races. + * </p><p> + * If timeout occurs then this method returns <code>false</code> but the runnable + * will remain posted on the handler and may already be in progress or + * complete at a later time. + * </p><p> + * When using this method, be sure to use {@link Looper#quitSafely} when + * quitting the looper. Otherwise {@link #runWithScissorsForDump} may hang indefinitely. + * (TODO: We should fix this by making MessageQueue aware of blocking runnables.) + * </p> + * + * @param h The target handler. + * @param r The Runnable that will be executed synchronously. + * @param timeout The timeout in milliseconds, or 0 to not wait at all. + * + * @return Returns true if the Runnable was successfully executed. + * Returns false on failure, usually because the + * looper processing the message queue is exiting. + * + * @hide + */ + public static boolean runWithScissorsForDump(@NonNull Handler h, @NonNull Runnable r, + long timeout) { + if (r == null) { + throw new IllegalArgumentException("runnable must not be null"); + } + if (timeout < 0) { + throw new IllegalArgumentException("timeout must be non-negative"); + } + if (Looper.myLooper() == h.getLooper()) { + r.run(); + return true; + } + + final CountDownLatch latch = new CountDownLatch(1); + + // Don't crash in the handler if something in the runnable throws an exception, + // but try to propagate the exception to the caller. + AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>(); + h.post(() -> { + try { + r.run(); + } catch (RuntimeException e) { + exceptionRef.set(e); + } + latch.countDown(); + }); + + try { + if (!latch.await(timeout, TimeUnit.MILLISECONDS)) { + return false; + } + } catch (InterruptedException e) { + exceptionRef.compareAndSet(null, new IllegalStateException("Thread interrupted", e)); + } + + final RuntimeException e = exceptionRef.get(); + if (e != null) throw e; + return true; + } +} diff --git a/staticlibs/device/com/android/net/module/util/Ipv6Utils.java b/staticlibs/device/com/android/net/module/util/Ipv6Utils.java index d5382215c3..497b8cbda8 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 @@ public class Ipv6Utils { } /** + * 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 @@ public class Ipv6Utils { } /** - * 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 4f76577caa..dbd83d0d0a 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 android.system.OsConstants.NETLINK_INET_DIAG; 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.SocketException; 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 class InetDiagMessage extends NetlinkMessage { } public StructInetDiagMsg inetDiagMsg; - + // The netlink attributes. + public List<StructNlAttr> nlAttrs = new ArrayList<>(); @VisibleForTesting public InetDiagMessage(@NonNull StructNlMsgHdr header) { super(header); @@ -172,6 +177,16 @@ public class InetDiagMessage extends NetlinkMessage { 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 @@ public class InetDiagMessage extends NetlinkMessage { 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 @@ public class InetDiagMessage extends NetlinkMessage { 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 @@ public class InetDiagMessage extends NetlinkMessage { 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 44c51d8a8c..ad7a4d7f61 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 class NetlinkConstants { 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/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java index f1f30d37f3..7c2be2cfbe 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.SOL_SOCKET; 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.Inet6Address; 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 @@ public class NetlinkUtils { 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 @@ public class NetlinkUtils { } 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 9acac69cc2..b2b1e9302a 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 @@ package com.android.net.module.util.netlink; 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.Inet4Address; 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 @@ import java.nio.ByteBuffer; */ 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 @@ public class RtNetlinkRouteMessage extends NetlinkMessage { 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 @@ public class RtNetlinkRouteMessage extends NetlinkMessage { } /** + * 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 @@ public class RtNetlinkRouteMessage extends NetlinkMessage { 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 @@ public class RtNetlinkRouteMessage extends NetlinkMessage { @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 @@ public class RtNetlinkRouteMessage extends NetlinkMessage { 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 @@ public class RtNetlinkRouteMessage extends NetlinkMessage { 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 @@ public class RtNetlinkRouteMessage extends NetlinkMessage { // 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 @@ public class RtNetlinkRouteMessage extends NetlinkMessage { 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 @@ public class RtNetlinkRouteMessage extends NetlinkMessage { 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 a9b6495082..43e83121d3 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 android.net.MacAddress; 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 @@ public class StructNlAttr { 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 @@ public class StructNlAttr { 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 @@ public class StructNlAttr { /** * 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 @@ public class StructNlAttr { } /** + * 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/structs/StructMf6cctl.java b/staticlibs/device/com/android/net/module/util/structs/StructMf6cctl.java new file mode 100644 index 0000000000..24e0a970d5 --- /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 0000000000..626a170126 --- /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 0000000000..569e3613d8 --- /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 637a938589..2b7e620705 100644 --- a/staticlibs/netd/Android.bp +++ b/staticlibs/netd/Android.bp @@ -241,5 +241,17 @@ aidl_interface { 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 0000000000..785d42da98 --- /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 0000000000..d31a327893 --- /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 0000000000..2049274454 --- /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 0000000000..d84742bcd2 --- /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 0000000000..187a3d26dd --- /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 0000000000..185111b440 --- /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 0000000000..4aa7d79114 --- /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 ecbe966dea..d84742bcd2 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 4625cac7ab..187a3d26dd 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 255d70ffea..3bf1da8201 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 @@ interface IMDns { * 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 @@ interface IMDns { * 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 @@ interface IMDns { * @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 @@ interface IMDns { * @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 @@ interface IMDns { * @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 @@ interface IMDns { * @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 @@ interface IMDns { * @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 @@ interface IMDns { * @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 @@ interface IMDns { * @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 a202a261a3..f7f028bcb6 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 @@ import android.net.mdns.aidl.ResolutionInfo; 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 @@ oneway interface IMDnsEventListener { /** * 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/tests/unit/java/com/android/server/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt index 62bb6517aa..f2c902f7ce 100644 --- a/tests/unit/java/com/android/server/HandlerUtilsTest.kt +++ b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.android.server +package com.android.net.module.util import android.os.HandlerThread -import com.android.server.connectivity.HandlerUtils import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.DevSdkIgnoreRunner.MonitorThreadLeak import kotlin.test.assertEquals import kotlin.test.assertTrue import org.junit.After @@ -27,6 +27,8 @@ import org.junit.runner.RunWith const val THREAD_BLOCK_TIMEOUT_MS = 1000L const val TEST_REPEAT_COUNT = 100 + +@MonitorThreadLeak @RunWith(DevSdkIgnoreRunner::class) class HandlerUtilsTest { val handlerThread = HandlerThread("HandlerUtilsTestHandlerThread").also { @@ -39,7 +41,7 @@ class HandlerUtilsTest { // Repeat the test a fair amount of times to ensure that it does not pass by chance. repeat(TEST_REPEAT_COUNT) { var result = false - HandlerUtils.runWithScissors(handler, { + HandlerUtils.runWithScissorsForDump(handler, { assertEquals(Thread.currentThread(), handlerThread) result = true }, THREAD_BLOCK_TIMEOUT_MS) 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 65e99f8296..b44e428547 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 com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST; 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 @@ public class InetDiagSocketTest { // 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 @@ public class InetDiagSocketTest { 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 @@ public class InetDiagSocketTest { 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 5a231fc523..0958f11d2a 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_INET6; 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.ByteBuffer; 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 @@ public class NetlinkUtilsTest { @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 @@ public class NetlinkUtilsTest { 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 @@ public class NetlinkUtilsTest { } // 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)); - - 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++; - } + NetlinkUtils.<RtNetlinkNeighborMessage>getAndProcessNetlinkDumpMessages(req, + NETLINK_ROUTE, RtNetlinkNeighborMessage.class, handleNlDumpMsg); + + 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)); - - int addrMessageCount = 0; + List<RtNetlinkAddressMessage> msgs = new ArrayList<>(); + Consumer<RtNetlinkAddressMessage> handleNlDumpMsg = (msg) -> { + msgs.add(msg); + }; + NetlinkUtils.<RtNetlinkAddressMessage>getAndProcessNetlinkDumpMessages(req, NETLINK_ROUTE, + RtNetlinkAddressMessage.class, handleNlDumpMsg); - 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()); + boolean ipv4LoopbackAddressFound = false; + boolean ipv6LoopbackAddressFound = false; + final InetAddress loopbackIpv4 = InetAddress.getByName("127.0.0.1"); + final InetAddress loopbackIpv6 = InetAddress.getByName("::1"); - 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); - assertTrue(addrMessageCount > 0); + if (ipAddress.equals(loopbackIpv4)) { + ipv4LoopbackAddressFound = true; + assertTrue(ipAddress.isLoopbackAddress()); + } + if (ipAddress.equals(loopbackIpv6)) { + ipv6LoopbackAddressFound = true; + assertTrue(ipAddress.isLoopbackAddress()); + } + } - 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 @@ public class NetlinkUtilsTest { 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 9881653a5e..50b8278150 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 org.junit.runner.RunWith; import java.net.Inet6Address; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.Arrays; @RunWith(AndroidJUnit4.class) @SmallTest @@ -127,6 +130,72 @@ public class RtNetlinkRouteMessageTest { 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 @@ public class RtNetlinkRouteMessageTest { + "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 af3fac2e7d..4c3fde60ed 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 @@ public class StructNlAttrTest { 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/structs/StructMf6cctlTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/structs/StructMf6cctlTest.java new file mode 100644 index 0000000000..a83fc36165 --- /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 0000000000..75196e4f78 --- /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 0000000000..f1b75a0a9d --- /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 a5e5afb886..a5c4fea6e3 100644 --- a/staticlibs/testutils/Android.bp +++ b/staticlibs/testutils/Android.bp @@ -84,6 +84,13 @@ java_test_host { "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 585157fa8f..57602f1017 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 @@ class NetworkStatsTest { } @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 @@ class NetworkStatsTest { 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 ffe0e9156d..79c4980a5c 100644 --- a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java +++ b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java @@ -18,6 +18,7 @@ package android.net.nsd; 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.net.InetAddress; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; @RunWith(DevSdkIgnoreRunner.class) @SmallTest @@ -114,6 +116,7 @@ public class NsdServiceInfoTest { 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 @@ public class NsdServiceInfoTest { 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 class NsdServiceInfoTest { } } - 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 2d0722481c..923f8e2805 100644 --- a/tests/cts/hostside/Android.bp +++ b/tests/cts/hostside/Android.bp @@ -43,6 +43,8 @@ java_test_host { 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 82f4a65184..ab956bfc7a 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 @@ public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCa // 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 9310888a20..3d53d6ca2c 100644 --- a/tests/cts/net/Android.bp +++ b/tests/cts/net/Android.bp @@ -131,6 +131,7 @@ java_defaults { "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 da4fe28e1f..a9e3715368 100644 --- a/tests/cts/net/native/dns/Android.bp +++ b/tests/cts/net/native/dns/Android.bp @@ -49,5 +49,7 @@ cc_test { "general-tests", "mts-dnsresolver", "mts-networking", + "mcts-dnsresolver", + "mcts-networking", ], } diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java index e0fe929777..ceb48d45ef 100644 --- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java +++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java @@ -298,17 +298,6 @@ public class ConnectivityDiagnosticsManagerTest { }, android.Manifest.permission.MODIFY_PHONE_STATE); - // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the - // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell - // permissions are updated. - runWithShellPermissionIdentity( - () -> mConnectivityManager.requestNetwork( - CELLULAR_NETWORK_REQUEST, testNetworkCallback), - android.Manifest.permission.CONNECTIVITY_INTERNAL); - - final Network network = testNetworkCallback.waitForAvailable(); - assertNotNull(network); - assertTrue("Didn't receive broadcast for ACTION_CARRIER_CONFIG_CHANGED for subId=" + subId, carrierConfigReceiver.waitForCarrierConfigChanged()); @@ -324,6 +313,17 @@ public class ConnectivityDiagnosticsManagerTest { Thread.sleep(5_000); + // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the + // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell + // permissions are updated. + runWithShellPermissionIdentity( + () -> mConnectivityManager.requestNetwork( + CELLULAR_NETWORK_REQUEST, testNetworkCallback), + android.Manifest.permission.CONNECTIVITY_INTERNAL); + + final Network network = testNetworkCallback.waitForAvailable(); + assertNotNull(network); + // TODO(b/217559768): Receiving carrier config change and immediately checking carrier // privileges is racy, as the CP status is updated after receiving the same signal. Move // the CP check after sleep to temporarily reduce the flakiness. This will soon be fixed diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java index 6b7954af30..f6a025a8d5 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 @@ public class Ikev2VpnTest { 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 @@ public class Ikev2VpnTest { 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 fe2f813882..84b67455ae 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 @@ class NetworkAgentTest { } } 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 f374181d81..1b1f3672ee 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 @@ class NsdManagerDownstreamTetheringTest : EthernetTetheringTestBase() { @Test fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() { - assumeFalse(isInterfaceForTetheringAvailable) + assumeFalse(isInterfaceForTetheringAvailable()) var downstreamIface: TestNetworkInterface? = null var tetheringEventCallback: MyTetheringEventCallback? = null @@ -104,7 +104,7 @@ class NsdManagerDownstreamTetheringTest : EthernetTetheringTestBase() { @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 e1ea2b9a63..a04020149b 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.DiscoveryStarted 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 @@ class NsdManagerTest { } @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 @@ class NsdManagerTest { 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 9de7f4dc7c..8919666551 100644 --- a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt +++ b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt @@ -213,7 +213,6 @@ class BpfNetMapsReaderTest { 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 a6e9e9579a..81557f85c5 100644 --- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java +++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java @@ -64,6 +64,7 @@ import libcore.io.Streams; 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 @@ import java.util.Set; @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 @@ public class NetworkStatsCollectionTest { 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 fad11a3ff7..7d039b6e02 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.reset; 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 @@ public final class NetworkStatsRecorderTest { 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 @@ public final class NetworkStatsRecorderTest { 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 @@ public final class NetworkStatsRecorderTest { // 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 550a9ee26b..461ead8ad6 100644 --- a/tests/unit/java/android/net/nsd/NsdManagerTest.java +++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java @@ -38,6 +38,7 @@ import android.os.Build; 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 @@ public class NsdManagerTest { @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 @@ public class NsdManagerTest { 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 @@ public class NsdManagerTest { 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 @@ public class NsdManagerTest { 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 0000000000..5709ed1918 --- /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 0000000000..17a0719238 --- /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/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java index 32014c2456..87e7967bcd 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.LinkedList; 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 @@ import java.util.Queue; @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 @@ public class NsdServiceTest { } } + // 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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { 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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { 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 @@ public class NsdServiceTest { @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 @@ public class NsdServiceTest { // 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 @@ public class NsdServiceTest { 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 @@ public class NsdServiceTest { 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 @@ public class NsdServiceTest { 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 @@ public class NsdServiceTest { 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 @@ public class NsdServiceTest { 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 @@ public class NsdServiceTest { 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 10a09820c5..4fcf8a8c63 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 @@ public class AutomaticOnOffKeepaliveTrackerTest { // 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 @@ public class AutomaticOnOffKeepaliveTrackerTest { } @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 12758c6081..4e15d5fa87 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 @@ package com.android.server.connectivity 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 @@ class RoutingCoordinatorServiceTest { 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/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt index f0cb6df875..121f844df9 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_1 = SocketKey(1001 /* interfaceIndex */) 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 @@ private val SERVICE_1 = NsdServiceInfo("TestServiceName", "_advertisertest._tcp" 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 @@ private val ALL_NETWORKS_SERVICE = NsdServiceInfo("TestServiceName", "_advertise 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 @@ class MdnsAdvertiserTest { 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 @@ class MdnsAdvertiserTest { } @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 @@ class MdnsAdvertiserTest { 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 @@ class MdnsAdvertiserTest { 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 @@ class MdnsAdvertiserTest { 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 @@ class MdnsAdvertiserTest { // 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 @@ class MdnsAdvertiserTest { 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 @@ class MdnsAdvertiserTest { 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 @@ class MdnsAdvertiserTest { 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 @@ class MdnsAdvertiserTest { 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 f85d71db3c..0c04bff0d5 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 @@ private val TEST_SERVICE_1 = NsdServiceInfo().apply { 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 @@ class MdnsInterfaceAdvertiserTest { 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 @@ class MdnsInterfaceAdvertiserTest { @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 @@ class MdnsInterfaceAdvertiserTest { @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 @@ class MdnsInterfaceAdvertiserTest { 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 c74e330fa8..4b1f16690f 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.net.nsd.NsdServiceInfo 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.assertEquals 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_2 = 43 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 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { @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 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { } @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 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { 0L /* receiptTimeMillis */, false /* cacheFlush */, 4500000L /* ttlMillis */, - serviceType) - ), packet.answers) + serviceType)) assertContentEquals(listOf( MdnsNsecRecord(v4AddrRev, @@ -376,31 +405,31 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { assertEquals(7, replyCaseInsensitive.additionalAnswers.size) } + /** + * 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() { - doGetReplyTest(subtype = null) + 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) + 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_WithSubtype() { - doGetReplyTest(TEST_SUBTYPE) + 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) } - private fun doGetReplyTest(subtype: String?) { + @Test + fun testGetReply_duplicatePtrQuestions_doesNotReturnDuplicateRecords() { val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags) - repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, subtype) - val queriedName = if (subtype == null) arrayOf("_testservice", "_tcp", "local") - else arrayOf(subtype, "_sub", "_testservice", "_tcp", "local") + repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE)) + val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353) + val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local") - val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */)) - val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */, - listOf() /* authorityRecords */, listOf() /* additionalRecords */) + 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) - // Source address is IPv4 - assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address) - assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port) + 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) + } - // TTLs as per RFC6762 10. - val longTtl = 4_500_000L - val shortTtl = 120_000L + @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( - queriedName, - 0L /* receiptTimeMillis */, - false /* cacheFlush */, - longTtl, - serviceName), - ), reply.answers) + 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( - 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) + 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 @@ class MdnsRecordRepositoryTest { @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 @@ class MdnsRecordRepositoryTest { @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 @@ class MdnsRecordRepositoryTest { @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 @@ class MdnsRecordRepositoryTest { 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 @@ class MdnsRecordRepositoryTest { expectedName, 0L /* receiptTimeMillis */, false /* cacheFlush */, - 120_000L /* ttlMillis */, + SHORT_TTL /* ttlMillis */, 0 /* servicePriority */, 0 /* serviceWeight */, TEST_PORT, @@ -755,33 +963,294 @@ class MdnsRecordRepositoryTest { 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 0000000000..9e2933f5b4 --- /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/CSNetworkRequestStateStatsMetricsTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt new file mode 100644 index 0000000000..35f8ae515c --- /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 958c4f2961..5c9a762dbd 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 android.util.ArraySet 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 @@ open class CSTest { 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 @@ open class CSTest { 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 7a4dfed424..1ee3f9dede 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 android.text.format.DateUtils.WEEK_IN_MILLIS; 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 org.mockito.MockitoAnnotations; 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 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { 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 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { 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 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { } }; mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock, - mClock, mSettings, mStatsFactory, statsObservers, deps); + mClock, mSettings, mStatsFactory, statsObservers, mDeps); mElapsedRealtime = 0L; @@ -423,132 +428,150 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { 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 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { } @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 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest { 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 30aeca506c..6a5ea4b9c1 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/flags/thread_base.aconfig b/thread/flags/thread_base.aconfig index bf1f2881a5..f73ea6beec 100644 --- a/thread/flags/thread_base.aconfig +++ b/thread/flags/thread_base.aconfig @@ -6,3 +6,10 @@ flag { description: "Controls whether the Android Thread feature is enabled" bug: "301473012" } + +flag { + name: "thread_user_restriction_enabled" + namespace: "thread_network" + description: "Controls whether user restriction on thread networks is enabled" + bug: "307679182" +} diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl index 89dcd394d6..a9da8d6549 100644 --- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl +++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl @@ -38,6 +38,8 @@ interface IThreadNetworkController { 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 34b0b06976..b5699a90e4 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.OutcomeReceiver; 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 @@ public final class ThreadNetworkController { } } + /** + * 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/Android.bp b/thread/service/Android.bp index 35ae3c277d..69295ccfbf 100644 --- a/thread/service/Android.bp +++ b/thread/service/Android.bp @@ -35,9 +35,13 @@ java_library { libs: [ "framework-connectivity-pre-jarjar", "framework-connectivity-t-pre-jarjar", + "framework-location.stubs.module_lib", + "framework-wifi", "service-connectivity-pre-jarjar", + "ServiceConnectivityResources", ], static_libs: [ + "modules-utils-shell-command-handler", "net-utils-device-common", "net-utils-device-common-netlink", "ot-daemon-aidl-java", diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java index d7c49a0d91..be54cbc28f 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 @@ public class InfraInterfaceController { * @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 60c97bfe35..cd59e4e94a 100644 --- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java +++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java @@ -36,7 +36,6 @@ import static android.net.thread.ThreadNetworkException.ERROR_RESOURCE_EXHAUSTED import static android.net.thread.ThreadNetworkException.ERROR_RESPONSE_BAD_FORMAT; import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT; import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL; -import static android.net.thread.ThreadNetworkException.ErrorCode; import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED; import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT; @@ -53,14 +52,17 @@ import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME; import android.Manifest.permission; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.TargetApi; 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 +71,7 @@ import android.net.NetworkProvider; 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; @@ -80,6 +83,8 @@ import android.net.thread.OperationalDatasetTimestamp; import android.net.thread.PendingOperationalDataset; import android.net.thread.ThreadNetworkController; import android.net.thread.ThreadNetworkController.DeviceRole; +import android.net.thread.ThreadNetworkException.ErrorCode; +import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -91,12 +96,12 @@ import android.util.SparseArray; 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; @@ -115,10 +120,11 @@ import java.util.function.Supplier; * * <p>Threading model: This class is not Thread-safe and should only be accessed from the * ThreadNetworkService class. Additional attention should be paid to handle the threading code - * correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from - * `mHandlerThread` 2. In the @Override methods, the actual work MUST be dispatched to the + * correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from the + * thread of `mHandler` 2. In the @Override methods, the actual work MUST be dispatched to the * HandlerThread except for arguments or permissions checking */ +@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) final class ThreadNetworkControllerService extends IThreadNetworkController.Stub { private static final String TAG = "ThreadNetworkService"; @@ -127,57 +133,52 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub private final Context mContext; private final Handler mHandler; - // Below member fields can only be accessed from the handler thread (`mHandlerThread`). In + // Below member fields can only be accessed from the handler thread (`mHandler`). In // particular, the constructor does not run on the handler thread, so it must not touch any of // the non-final fields, nor must it mutate any of the non-final fields inside these objects. - private final HandlerThread mHandlerThread; private final NetworkProvider mNetworkProvider; 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(); // TODO(b/308310823): read supported channel from Thread dameon private final int mSupportedChannelMask = 0x07FFF800; // from channel 11 to 26 - private IOtDaemon mOtDaemon; - private NetworkAgent mNetworkAgent; + @Nullable private IOtDaemon mOtDaemon; + @Nullable private NetworkAgent mNetworkAgent; + @Nullable private NetworkAgent mTestNetworkAgent; + 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; @VisibleForTesting ThreadNetworkControllerService( Context context, - HandlerThread handlerThread, + Handler handler, NetworkProvider networkProvider, Supplier<IOtDaemon> otDaemonSupplier, ConnectivityManager connectivityManager, - TunInterfaceController tunIfController) { + TunInterfaceController tunIfController, + InfraInterfaceController infraIfController) { mContext = context; - mHandlerThread = handlerThread; - mHandler = new Handler(handlerThread.getLooper()); + mHandler = handler; mNetworkProvider = networkProvider; 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(); } @@ -190,19 +191,12 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub return new ThreadNetworkControllerService( context, - handlerThread, + new Handler(handlerThread.getLooper()), networkProvider, () -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")), context.getSystemService(ConnectivityManager.class), - new TunInterfaceController(TUN_IF_NAME)); - } - - private static NetworkCapabilities newNetworkCapabilities() { - return new NetworkCapabilities.Builder() - .addTransportType(NetworkCapabilities.TRANSPORT_THREAD) - .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) - .build(); + new TunInterfaceController(TUN_IF_NAME), + new InfraInterfaceController()); } private static Inet6Address bytesToInet6Address(byte[] addressBytes) { @@ -237,6 +231,60 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub 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(); @@ -246,6 +294,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub } private IOtDaemon getOtDaemon() throws RemoteException { + checkOnHandlerThread(); + if (mOtDaemon != null) { return mOtDaemon; } @@ -254,9 +304,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub if (otDaemon == null) { throw new RemoteException("Internal error: failed to start OT daemon"); } - otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0); otDaemon.initialize(mTunIfController.getTunFd()); otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1); + otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0); mOtDaemon = otDaemon; return mOtDaemon; } @@ -283,51 +333,70 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub mLinkProperties.setMtu(TunInterfaceController.MTU); mConnectivityManager.registerNetworkProvider(mNetworkProvider); requestUpstreamNetwork(); + requestThreadNetwork(); initializeOtDaemon(); }); } 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; @@ -345,35 +414,54 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub private void requestThreadNetwork() { mConnectivityManager.registerNetworkCallback( new NetworkRequest.Builder() + // clearCapabilities() is needed to remove forbidden capabilities and UID + // requirement. .clearCapabilities() .addTransportType(NetworkCapabilities.TRANSPORT_THREAD) - .removeForbiddenCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK) .build(), new ThreadNetworkCallback(), mHandler); } - private void registerThreadNetwork() { - if (mNetworkAgent != null) { - return; + /** Injects a {@link NetworkAgent} for testing. */ + @VisibleForTesting + void setTestNetworkAgent(@Nullable NetworkAgent testNetworkAgent) { + mTestNetworkAgent = testNetworkAgent; + } + + private NetworkAgent newNetworkAgent() { + if (mTestNetworkAgent != null) { + return mTestNetworkAgent; } - NetworkCapabilities netCaps = newNetworkCapabilities(); - NetworkScore score = + + final NetworkCapabilities netCaps = + new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_THREAD) + .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) + .build(); + final NetworkScore score = new NetworkScore.Builder() .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK) .build(); - requestThreadNetwork(); - mNetworkAgent = - new NetworkAgent( - mContext, - mHandlerThread.getLooper(), - TAG, - netCaps, - mLinkProperties, - mLocalNetworkConfig, - score, - new NetworkAgentConfig.Builder().build(), - mNetworkProvider) {}; + return new NetworkAgent( + mContext, + mHandler.getLooper(), + TAG, + netCaps, + mLinkProperties, + newLocalNetworkConfig(), + score, + new NetworkAgentConfig.Builder().build(), + mNetworkProvider) {}; + } + + private void registerThreadNetwork() { + if (mNetworkAgent != null) { + return; + } + + mNetworkAgent = newNetworkAgent(); mNetworkAgent.register(); mNetworkAgent.markConnected(); Log.i(TAG, "Registered Thread network"); @@ -524,29 +612,29 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub 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,13 +642,13 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub @Override public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback) throws RemoteException { - enforceAllCallingPermissionsGranted( + enforceAllPermissionsGranted( permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback)); } private void checkOnHandlerThread() { - if (Looper.myLooper() != mHandlerThread.getLooper()) { + if (Looper.myLooper() != mHandler.getLooper()) { Log.wtf(TAG, "Must be on the handler thread!"); } } @@ -609,7 +697,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub @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 +721,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub 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 +744,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub @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))); } @@ -672,6 +760,32 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub } } + /** + * Sets the country code. + * + * @param countryCode 2 characters string country code (as defined in ISO 3166) to set. + * @param receiver the receiver to receive result of this operation + */ + @RequiresPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED) + public void setCountryCode(@NonNull String countryCode, @NonNull IOperationReceiver receiver) { + enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED); + + OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver); + mHandler.post(() -> setCountryCodeInternal(countryCode, receiverWrapper)); + } + + private void setCountryCodeInternal( + String countryCode, @NonNull OperationReceiverWrapper receiver) { + checkOnHandlerThread(); + + try { + getOtDaemon().setCountryCode(countryCode, newOtStatusReceiver(receiver)); + } catch (RemoteException e) { + Log.e(TAG, "otDaemon.setCountryCode failed", e); + receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error"); + } + } + private void enableBorderRouting(String infraIfName) { if (mBorderRouterConfig.isBorderRoutingEnabled && infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) { @@ -681,7 +795,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub try { mBorderRouterConfig.infraInterfaceName = infraIfName; mBorderRouterConfig.infraInterfaceIcmp6Socket = - InfraInterfaceController.createIcmp6Socket(infraIfName); + mInfraIfController.createIcmp6Socket(infraIfName); mBorderRouterConfig.isBorderRoutingEnabled = true; mOtDaemon.configureBorderRouter( @@ -754,20 +868,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub 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 +903,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub 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 +912,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub // 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; } @@ -861,8 +964,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub } /** - * Handles and forwards Thread daemon callbacks. This class must be accessed from the {@code - * mHandlerThread}. + * Handles and forwards Thread daemon callbacks. This class must be accessed from the thread of + * {@code mHandler}. */ private final class OtDaemonCallbackProxy extends IOtDaemonCallback.Stub { private final Map<IStateCallback, CallbackMetadata> mStateCallbacks = new HashMap<>(); diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java new file mode 100644 index 0000000000..b7b6233321 --- /dev/null +++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java @@ -0,0 +1,543 @@ +/* + * 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.thread; + +import android.annotation.Nullable; +import android.annotation.StringDef; +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.location.Address; +import android.location.Geocoder; +import android.location.Location; +import android.location.LocationManager; +import android.net.thread.IOperationReceiver; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback; +import android.os.Build; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.connectivity.resources.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.connectivity.ConnectivityResources; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.time.Instant; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +/** + * Provide functions for making changes to Thread Network country code. This Country Code is from + * location, WiFi or telephony configuration. This class sends Country Code to Thread Network native + * layer. + * + * <p>This class is thread-safe. + */ +@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +public class ThreadNetworkCountryCode { + private static final String TAG = "ThreadNetworkCountryCode"; + // To be used when there is no country code available. + @VisibleForTesting public static final String DEFAULT_COUNTRY_CODE = "WW"; + + // Wait 1 hour between updates. + private static final long TIME_BETWEEN_LOCATION_UPDATES_MS = 1000L * 60 * 60 * 1; + // Minimum distance before an update is triggered, in meters. We don't need this to be too + // exact because all we care about is what country the user is in. + private static final float DISTANCE_BETWEEN_LOCALTION_UPDATES_METERS = 5_000.0f; + + /** List of country code sources. */ + @Retention(RetentionPolicy.SOURCE) + @StringDef( + prefix = "COUNTRY_CODE_SOURCE_", + value = { + COUNTRY_CODE_SOURCE_DEFAULT, + COUNTRY_CODE_SOURCE_LOCATION, + COUNTRY_CODE_SOURCE_OVERRIDE, + COUNTRY_CODE_SOURCE_TELEPHONY, + COUNTRY_CODE_SOURCE_TELEPHONY_LAST, + COUNTRY_CODE_SOURCE_WIFI, + }) + private @interface CountryCodeSource {} + + private static final String COUNTRY_CODE_SOURCE_DEFAULT = "Default"; + private static final String COUNTRY_CODE_SOURCE_LOCATION = "Location"; + private static final String COUNTRY_CODE_SOURCE_OVERRIDE = "Override"; + private static final String COUNTRY_CODE_SOURCE_TELEPHONY = "Telephony"; + private static final String COUNTRY_CODE_SOURCE_TELEPHONY_LAST = "TelephonyLast"; + private static final String COUNTRY_CODE_SOURCE_WIFI = "Wifi"; + + private static final CountryCodeInfo DEFAULT_COUNTRY_CODE_INFO = + new CountryCodeInfo(DEFAULT_COUNTRY_CODE, COUNTRY_CODE_SOURCE_DEFAULT); + + private final ConnectivityResources mResources; + private final Context mContext; + private final LocationManager mLocationManager; + @Nullable private final Geocoder mGeocoder; + private final ThreadNetworkControllerService mThreadNetworkControllerService; + private final WifiManager mWifiManager; + private final TelephonyManager mTelephonyManager; + private final SubscriptionManager mSubscriptionManager; + private final Map<Integer, TelephonyCountryCodeSlotInfo> mTelephonyCountryCodeSlotInfoMap = + new ArrayMap(); + + @Nullable private CountryCodeInfo mCurrentCountryCodeInfo; + @Nullable private CountryCodeInfo mLocationCountryCodeInfo; + @Nullable private CountryCodeInfo mOverrideCountryCodeInfo; + @Nullable private CountryCodeInfo mWifiCountryCodeInfo; + @Nullable private CountryCodeInfo mTelephonyCountryCodeInfo; + @Nullable private CountryCodeInfo mTelephonyLastCountryCodeInfo; + + /** Container class to store Thread country code information. */ + private static final class CountryCodeInfo { + private String mCountryCode; + @CountryCodeSource private String mSource; + private final Instant mUpdatedTimestamp; + + public CountryCodeInfo( + String countryCode, @CountryCodeSource String countryCodeSource, Instant instant) { + mCountryCode = countryCode; + mSource = countryCodeSource; + mUpdatedTimestamp = instant; + } + + public CountryCodeInfo(String countryCode, @CountryCodeSource String countryCodeSource) { + this(countryCode, countryCodeSource, Instant.now()); + } + + public String getCountryCode() { + return mCountryCode; + } + + public boolean isCountryCodeMatch(CountryCodeInfo countryCodeInfo) { + if (countryCodeInfo == null) { + return false; + } + + return Objects.equals(countryCodeInfo.mCountryCode, mCountryCode); + } + + @Override + public String toString() { + return "CountryCodeInfo{ mCountryCode: " + + mCountryCode + + ", mSource: " + + mSource + + ", mUpdatedTimestamp: " + + mUpdatedTimestamp + + "}"; + } + } + + /** Container class to store country code per SIM slot. */ + private static final class TelephonyCountryCodeSlotInfo { + public int slotIndex; + public String countryCode; + public String lastKnownCountryCode; + public Instant timestamp; + + @Override + public String toString() { + return "TelephonyCountryCodeSlotInfo{ slotIndex: " + + slotIndex + + ", countryCode: " + + countryCode + + ", lastKnownCountryCode: " + + lastKnownCountryCode + + ", timestamp: " + + timestamp + + "}"; + } + } + + private boolean isLocationUseForCountryCodeEnabled() { + return mResources + .get() + .getBoolean(R.bool.config_thread_location_use_for_country_code_enabled); + } + + public ThreadNetworkCountryCode( + LocationManager locationManager, + ThreadNetworkControllerService threadNetworkControllerService, + @Nullable Geocoder geocoder, + ConnectivityResources resources, + WifiManager wifiManager, + Context context, + TelephonyManager telephonyManager, + SubscriptionManager subscriptionManager) { + mLocationManager = locationManager; + mThreadNetworkControllerService = threadNetworkControllerService; + mGeocoder = geocoder; + mResources = resources; + mWifiManager = wifiManager; + mContext = context; + mTelephonyManager = telephonyManager; + mSubscriptionManager = subscriptionManager; + } + + public static ThreadNetworkCountryCode newInstance( + Context context, ThreadNetworkControllerService controllerService) { + return new ThreadNetworkCountryCode( + context.getSystemService(LocationManager.class), + controllerService, + Geocoder.isPresent() ? new Geocoder(context) : null, + new ConnectivityResources(context), + context.getSystemService(WifiManager.class), + context, + context.getSystemService(TelephonyManager.class), + context.getSystemService(SubscriptionManager.class)); + } + + /** Sets up this country code module to listen to location country code changes. */ + public synchronized void initialize() { + registerGeocoderCountryCodeCallback(); + registerWifiCountryCodeCallback(); + registerTelephonyCountryCodeCallback(); + updateTelephonyCountryCodeFromSimCard(); + updateCountryCode(false /* forceUpdate */); + } + + private synchronized void registerGeocoderCountryCodeCallback() { + if ((mGeocoder != null) && isLocationUseForCountryCodeEnabled()) { + mLocationManager.requestLocationUpdates( + LocationManager.PASSIVE_PROVIDER, + TIME_BETWEEN_LOCATION_UPDATES_MS, + DISTANCE_BETWEEN_LOCALTION_UPDATES_METERS, + location -> setCountryCodeFromGeocodingLocation(location)); + } + } + + private synchronized void geocodeListener(List<Address> addresses) { + if (addresses != null && !addresses.isEmpty()) { + String countryCode = addresses.get(0).getCountryCode(); + + if (isValidCountryCode(countryCode)) { + Log.d(TAG, "Set location country code to: " + countryCode); + mLocationCountryCodeInfo = + new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_LOCATION); + } else { + Log.d(TAG, "Received invalid location country code"); + mLocationCountryCodeInfo = null; + } + + updateCountryCode(false /* forceUpdate */); + } + } + + private synchronized void setCountryCodeFromGeocodingLocation(@Nullable Location location) { + if ((location == null) || (mGeocoder == null)) return; + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) { + Log.wtf( + TAG, + "Unexpected call to set country code from the Geocoding location, " + + "Thread code never runs under T or lower."); + return; + } + + mGeocoder.getFromLocation( + location.getLatitude(), + location.getLongitude(), + 1 /* maxResults */, + this::geocodeListener); + } + + private synchronized void registerWifiCountryCodeCallback() { + if (mWifiManager != null) { + mWifiManager.registerActiveCountryCodeChangedCallback( + r -> r.run(), new WifiCountryCodeCallback()); + } + } + + private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback { + @Override + public void onActiveCountryCodeChanged(String countryCode) { + Log.d(TAG, "Wifi country code is changed to " + countryCode); + synchronized ("ThreadNetworkCountryCode.this") { + mWifiCountryCodeInfo = new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_WIFI); + updateCountryCode(false /* forceUpdate */); + } + } + + @Override + public void onCountryCodeInactive() { + Log.d(TAG, "Wifi country code is inactived"); + synchronized ("ThreadNetworkCountryCode.this") { + mWifiCountryCodeInfo = null; + updateCountryCode(false /* forceUpdate */); + } + } + } + + private synchronized void registerTelephonyCountryCodeCallback() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + Log.wtf( + TAG, + "Unexpected call to register the telephony country code changed callback, " + + "Thread code never runs under T or lower."); + return; + } + + BroadcastReceiver broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int slotIndex = + intent.getIntExtra( + SubscriptionManager.EXTRA_SLOT_INDEX, + SubscriptionManager.INVALID_SIM_SLOT_INDEX); + String lastKnownCountryCode = null; + String countryCode = + intent.getStringExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + lastKnownCountryCode = + intent.getStringExtra( + TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY); + } + + setTelephonyCountryCodeAndLastKnownCountryCode( + slotIndex, countryCode, lastKnownCountryCode); + } + }; + + mContext.registerReceiver( + broadcastReceiver, + new IntentFilter(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED), + Context.RECEIVER_EXPORTED); + } + + private synchronized void updateTelephonyCountryCodeFromSimCard() { + List<SubscriptionInfo> subscriptionInfoList = + mSubscriptionManager.getActiveSubscriptionInfoList(); + + if (subscriptionInfoList == null) { + Log.d(TAG, "No SIM card is found"); + return; + } + + for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) { + String countryCode; + int slotIndex; + + slotIndex = subscriptionInfo.getSimSlotIndex(); + try { + countryCode = mTelephonyManager.getNetworkCountryIso(slotIndex); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Failed to get country code for slot index:" + slotIndex, e); + continue; + } + + Log.d(TAG, "Telephony slot " + slotIndex + " country code is " + countryCode); + setTelephonyCountryCodeAndLastKnownCountryCode( + slotIndex, countryCode, null /* lastKnownCountryCode */); + } + } + + private synchronized void setTelephonyCountryCodeAndLastKnownCountryCode( + int slotIndex, String countryCode, String lastKnownCountryCode) { + Log.d( + TAG, + "Set telephony country code to: " + + countryCode + + ", last country code to: " + + lastKnownCountryCode + + " for slotIndex: " + + slotIndex); + + TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot = + mTelephonyCountryCodeSlotInfoMap.computeIfAbsent( + slotIndex, k -> new TelephonyCountryCodeSlotInfo()); + telephonyCountryCodeInfoSlot.slotIndex = slotIndex; + telephonyCountryCodeInfoSlot.timestamp = Instant.now(); + telephonyCountryCodeInfoSlot.countryCode = countryCode; + telephonyCountryCodeInfoSlot.lastKnownCountryCode = lastKnownCountryCode; + + mTelephonyCountryCodeInfo = null; + mTelephonyLastCountryCodeInfo = null; + + for (TelephonyCountryCodeSlotInfo slotInfo : mTelephonyCountryCodeSlotInfoMap.values()) { + if ((mTelephonyCountryCodeInfo == null) && isValidCountryCode(slotInfo.countryCode)) { + mTelephonyCountryCodeInfo = + new CountryCodeInfo( + slotInfo.countryCode, + COUNTRY_CODE_SOURCE_TELEPHONY, + slotInfo.timestamp); + } + + if ((mTelephonyLastCountryCodeInfo == null) + && isValidCountryCode(slotInfo.lastKnownCountryCode)) { + mTelephonyLastCountryCodeInfo = + new CountryCodeInfo( + slotInfo.lastKnownCountryCode, + COUNTRY_CODE_SOURCE_TELEPHONY_LAST, + slotInfo.timestamp); + } + } + + updateCountryCode(false /* forceUpdate */); + } + + /** + * Priority order of country code sources (we stop at the first known country code source): + * + * <ul> + * <li>1. Override country code - Country code forced via shell command (local/automated + * testing) + * <li>2. Telephony country code - Current country code retrieved via cellular. If there are + * multiple SIM's, the country code chosen is non-deterministic if they return different + * codes. The first valid country code with the lowest slot number will be used. + * <li>3. Wifi country code - Current country code retrieved via wifi (via 80211.ad). + * <li>4. Last known telephony country code - Last known country code retrieved via cellular. + * If there are multiple SIM's, the country code chosen is non-deterministic if they + * return different codes. The first valid last known country code with the lowest slot + * number will be used. + * <li>5. Location country code - Country code retrieved from LocationManager passive location + * provider. + * </ul> + * + * @return the selected country code information. + */ + private CountryCodeInfo pickCountryCode() { + if (mOverrideCountryCodeInfo != null) { + return mOverrideCountryCodeInfo; + } + + if (mTelephonyCountryCodeInfo != null) { + return mTelephonyCountryCodeInfo; + } + + if (mWifiCountryCodeInfo != null) { + return mWifiCountryCodeInfo; + } + + if (mTelephonyLastCountryCodeInfo != null) { + return mTelephonyLastCountryCodeInfo; + } + + if (mLocationCountryCodeInfo != null) { + return mLocationCountryCodeInfo; + } + + return DEFAULT_COUNTRY_CODE_INFO; + } + + private IOperationReceiver newOperationReceiver(CountryCodeInfo countryCodeInfo) { + return new IOperationReceiver.Stub() { + @Override + public void onSuccess() { + synchronized ("ThreadNetworkCountryCode.this") { + mCurrentCountryCodeInfo = countryCodeInfo; + } + } + + @Override + public void onError(int otError, String message) { + Log.e( + TAG, + "Error " + + otError + + ": " + + message + + ". Failed to set country code " + + countryCodeInfo); + } + }; + } + + /** + * Updates country code to the Thread native layer. + * + * @param forceUpdate Force update the country code even if it was the same as previously cached + * value. + */ + @VisibleForTesting + public synchronized void updateCountryCode(boolean forceUpdate) { + CountryCodeInfo countryCodeInfo = pickCountryCode(); + + if (!forceUpdate && countryCodeInfo.isCountryCodeMatch(mCurrentCountryCodeInfo)) { + Log.i(TAG, "Ignoring already set country code " + countryCodeInfo.getCountryCode()); + return; + } + + Log.i(TAG, "Set country code: " + countryCodeInfo); + mThreadNetworkControllerService.setCountryCode( + countryCodeInfo.getCountryCode().toUpperCase(Locale.ROOT), + newOperationReceiver(countryCodeInfo)); + } + + /** Returns the current country code or {@code null} if no country code is set. */ + @Nullable + public synchronized String getCountryCode() { + return (mCurrentCountryCodeInfo != null) ? mCurrentCountryCodeInfo.getCountryCode() : null; + } + + /** + * Returns {@code true} if {@code countryCode} is a valid country code. + * + * <p>A country code is valid if it consists of 2 alphabets. + */ + public static boolean isValidCountryCode(String countryCode) { + return countryCode != null + && countryCode.length() == 2 + && countryCode.chars().allMatch(Character::isLetter); + } + + /** + * Overrides any existing country code. + * + * @param countryCode A 2-Character alphabetical country code (as defined in ISO 3166). + * @throws IllegalArgumentException if {@code countryCode} is an invalid country code. + */ + public synchronized void setOverrideCountryCode(String countryCode) { + if (!isValidCountryCode(countryCode)) { + throw new IllegalArgumentException("The override country code is invalid"); + } + + mOverrideCountryCodeInfo = new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_OVERRIDE); + updateCountryCode(true /* forceUpdate */); + } + + /** Clears the country code previously set through {@link #setOverrideCountryCode} method. */ + public synchronized void clearOverrideCountryCode() { + mOverrideCountryCodeInfo = null; + updateCountryCode(true /* forceUpdate */); + } + + /** Dumps the current state of this ThreadNetworkCountryCode object. */ + public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("---- Dump of ThreadNetworkCountryCode begin ----"); + pw.println("mOverrideCountryCodeInfo: " + mOverrideCountryCodeInfo); + pw.println("mTelephonyCountryCodeSlotInfoMap: " + mTelephonyCountryCodeSlotInfoMap); + pw.println("mTelephonyCountryCodeInfo: " + mTelephonyCountryCodeInfo); + pw.println("mWifiCountryCodeInfo: " + mWifiCountryCodeInfo); + pw.println("mTelephonyLastCountryCodeInfo: " + mTelephonyLastCountryCodeInfo); + pw.println("mLocationCountryCodeInfo: " + mLocationCountryCodeInfo); + pw.println("mCurrentCountryCodeInfo: " + mCurrentCountryCodeInfo); + pw.println("---- Dump of ThreadNetworkCountryCode end ------"); + } +} diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java index cc694a1440..53f2d4f4b3 100644 --- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java +++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java @@ -16,13 +16,20 @@ package com.android.server.thread; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.net.thread.IThreadNetworkController; import android.net.thread.IThreadNetworkManager; +import android.os.Binder; +import android.os.ParcelFileDescriptor; import com.android.server.SystemService; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.Collections; import java.util.List; @@ -31,7 +38,9 @@ import java.util.List; */ public class ThreadNetworkService extends IThreadNetworkManager.Stub { private final Context mContext; + @Nullable private ThreadNetworkCountryCode mCountryCode; @Nullable private ThreadNetworkControllerService mControllerService; + @Nullable private ThreadNetworkShellCommand mShellCommand; /** Creates a new {@link ThreadNetworkService} object. */ public ThreadNetworkService(Context context) { @@ -39,14 +48,21 @@ public class ThreadNetworkService extends IThreadNetworkManager.Stub { } /** - * Called by the service initializer. + * Called by {@link com.android.server.ConnectivityServiceInitializer}. * * @see com.android.server.SystemService#onBootPhase */ public void onBootPhase(int phase) { - if (phase == SystemService.PHASE_BOOT_COMPLETED) { + if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { mControllerService = ThreadNetworkControllerService.newInstance(mContext); mControllerService.initialize(); + } else if (phase == SystemService.PHASE_BOOT_COMPLETED) { + // Country code initialization is delayed to the BOOT_COMPLETED phase because it will + // call into Wi-Fi and Telephony service whose country code module is ready after + // PHASE_ACTIVITY_MANAGER_READY and PHASE_THIRD_PARTY_APPS_CAN_START + mCountryCode = ThreadNetworkCountryCode.newInstance(mContext, mControllerService); + mCountryCode.initialize(); + mShellCommand = new ThreadNetworkShellCommand(mCountryCode); } } @@ -57,4 +73,40 @@ public class ThreadNetworkService extends IThreadNetworkManager.Stub { } return Collections.singletonList(mControllerService); } + + @Override + public int handleShellCommand( + @NonNull ParcelFileDescriptor in, + @NonNull ParcelFileDescriptor out, + @NonNull ParcelFileDescriptor err, + @NonNull String[] args) { + if (mShellCommand == null) { + return -1; + } + return mShellCommand.exec( + this, + in.getFileDescriptor(), + out.getFileDescriptor(), + err.getFileDescriptor(), + args); + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PERMISSION_GRANTED) { + pw.println( + "Permission Denial: can't dump ThreadNetworkService from from pid=" + + Binder.getCallingPid() + + ", uid=" + + Binder.getCallingUid()); + return; + } + + if (mCountryCode != null) { + mCountryCode.dump(fd, pw, args); + } + + pw.println(); + } } diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java new file mode 100644 index 0000000000..c17c5a7534 --- /dev/null +++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java @@ -0,0 +1,183 @@ +/* + * 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.thread; + +import android.annotation.Nullable; +import android.os.Binder; +import android.os.Process; +import android.text.TextUtils; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.modules.utils.BasicShellCommandHandler; + +import java.io.PrintWriter; +import java.util.List; + +/** + * Interprets and executes 'adb shell cmd thread_network [args]'. + * + * <p>To add new commands: - onCommand: Add a case "<command>" execute. Return a 0 if command + * executed successfully. - onHelp: add a description string. + * + * <p>Permissions: currently root permission is required for some commands. Others will enforce the + * corresponding API permissions. + */ +public class ThreadNetworkShellCommand extends BasicShellCommandHandler { + private static final String TAG = "ThreadNetworkShellCommand"; + + // These don't require root access. + private static final List<String> NON_PRIVILEGED_COMMANDS = List.of("help", "get-country-code"); + + @Nullable private final ThreadNetworkCountryCode mCountryCode; + @Nullable private PrintWriter mOutputWriter; + @Nullable private PrintWriter mErrorWriter; + + ThreadNetworkShellCommand(@Nullable ThreadNetworkCountryCode countryCode) { + mCountryCode = countryCode; + } + + @VisibleForTesting + public void setPrintWriters(PrintWriter outputWriter, PrintWriter errorWriter) { + mOutputWriter = outputWriter; + mErrorWriter = errorWriter; + } + + private PrintWriter getOutputWriter() { + return (mOutputWriter != null) ? mOutputWriter : getOutPrintWriter(); + } + + private PrintWriter getErrorWriter() { + return (mErrorWriter != null) ? mErrorWriter : getErrPrintWriter(); + } + + @Override + public int onCommand(String cmd) { + // Treat no command as help command. + if (TextUtils.isEmpty(cmd)) { + cmd = "help"; + } + + final PrintWriter pw = getOutputWriter(); + final PrintWriter perr = getErrorWriter(); + + // Explicit exclusion from root permission + if (!NON_PRIVILEGED_COMMANDS.contains(cmd)) { + final int uid = Binder.getCallingUid(); + + if (uid != Process.ROOT_UID) { + perr.println( + "Uid " + + uid + + " does not have access to " + + cmd + + " thread command " + + "(or such command doesn't exist)"); + return -1; + } + } + + switch (cmd) { + case "force-country-code": + boolean enabled; + + if (mCountryCode == null) { + perr.println("Thread country code operations are not supported"); + return -1; + } + + try { + enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled"); + } catch (IllegalArgumentException e) { + perr.println("Invalid argument: " + e.getMessage()); + return -1; + } + + if (enabled) { + String countryCode = getNextArgRequired(); + if (!ThreadNetworkCountryCode.isValidCountryCode(countryCode)) { + perr.println( + "Invalid argument: Country code must be a 2-Character" + + " string. But got country code " + + countryCode + + " instead"); + return -1; + } + mCountryCode.setOverrideCountryCode(countryCode); + pw.println("Set Thread country code: " + countryCode); + + } else { + mCountryCode.clearOverrideCountryCode(); + } + return 0; + case "get-country-code": + if (mCountryCode == null) { + perr.println("Thread country code operations are not supported"); + return -1; + } + + pw.println("Thread country code = " + mCountryCode.getCountryCode()); + return 0; + default: + return handleDefaultCommands(cmd); + } + } + + private static boolean argTrueOrFalse(String arg, String trueString, String falseString) { + if (trueString.equals(arg)) { + return true; + } else if (falseString.equals(arg)) { + return false; + } else { + throw new IllegalArgumentException( + "Expected '" + + trueString + + "' or '" + + falseString + + "' as next arg but got '" + + arg + + "'"); + } + } + + private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString) { + String nextArg = getNextArgRequired(); + return argTrueOrFalse(nextArg, trueString, falseString); + } + + private void onHelpNonPrivileged(PrintWriter pw) { + pw.println(" get-country-code"); + pw.println(" Gets country code as a two-letter string"); + } + + private void onHelpPrivileged(PrintWriter pw) { + pw.println(" force-country-code enabled <two-letter code> | disabled "); + pw.println(" Sets country code to <two-letter code> or left for normal value"); + } + + @Override + public void onHelp() { + final PrintWriter pw = getOutputWriter(); + pw.println("Thread network commands:"); + pw.println(" help or -h"); + pw.println(" Print this help text."); + onHelpNonPrivileged(pw); + if (Binder.getCallingUid() == Process.ROOT_UID) { + onHelpPrivileged(pw); + } + pw.println(); + } +} diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java new file mode 100644 index 0000000000..d32f0bf028 --- /dev/null +++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2024 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.thread; + +import android.annotation.Nullable; +import android.os.PersistableBundle; +import android.util.AtomicFile; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Store persistent data for Thread network settings. These are key (string) / value pairs that are + * stored in ThreadPersistentSetting.xml file. The values allowed are those that can be serialized + * via {@link PersistableBundle}. + */ +public class ThreadPersistentSettings { + private static final String TAG = "ThreadPersistentSettings"; + /** File name used for storing settings. */ + public static final String FILE_NAME = "ThreadPersistentSettings.xml"; + /** Current config store data version. This will be incremented for any additions. */ + private static final int CURRENT_SETTINGS_STORE_DATA_VERSION = 1; + /** + * Stores the version of the data. This can be used to handle migration of data if some + * non-backward compatible change introduced. + */ + private static final String VERSION_KEY = "version"; + + /******** Thread persistent setting keys ***************/ + /** Stores the Thread feature toggle state, true for enabled and false for disabled. */ + public static final Key<Boolean> THREAD_ENABLED = new Key<>("Thread_enabled", true); + + /******** Thread persistent setting keys ***************/ + + @GuardedBy("mLock") + private final AtomicFile mAtomicFile; + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private final PersistableBundle mSettings = new PersistableBundle(); + + public ThreadPersistentSettings(AtomicFile atomicFile) { + mAtomicFile = atomicFile; + } + + /** Initialize the settings by reading from the settings file. */ + public void initialize() { + readFromStoreFile(); + synchronized (mLock) { + if (mSettings.isEmpty()) { + put(THREAD_ENABLED.key, THREAD_ENABLED.defaultValue); + } + } + } + + private void putObject(String key, @Nullable Object value) { + synchronized (mLock) { + if (value == null) { + mSettings.putString(key, null); + } else if (value instanceof Boolean) { + mSettings.putBoolean(key, (Boolean) value); + } else if (value instanceof Integer) { + mSettings.putInt(key, (Integer) value); + } else if (value instanceof Long) { + mSettings.putLong(key, (Long) value); + } else if (value instanceof Double) { + mSettings.putDouble(key, (Double) value); + } else if (value instanceof String) { + mSettings.putString(key, (String) value); + } else { + throw new IllegalArgumentException("Unsupported type " + value.getClass()); + } + } + } + + private <T> T getObject(String key, T defaultValue) { + Object value; + synchronized (mLock) { + if (defaultValue instanceof Boolean) { + value = mSettings.getBoolean(key, (Boolean) defaultValue); + } else if (defaultValue instanceof Integer) { + value = mSettings.getInt(key, (Integer) defaultValue); + } else if (defaultValue instanceof Long) { + value = mSettings.getLong(key, (Long) defaultValue); + } else if (defaultValue instanceof Double) { + value = mSettings.getDouble(key, (Double) defaultValue); + } else if (defaultValue instanceof String) { + value = mSettings.getString(key, (String) defaultValue); + } else { + throw new IllegalArgumentException("Unsupported type " + defaultValue.getClass()); + } + } + return (T) value; + } + + /** + * Store a value to the stored settings. + * + * @param key One of the settings keys. + * @param value Value to be stored. + */ + public <T> void put(String key, @Nullable T value) { + putObject(key, value); + writeToStoreFile(); + } + + /** + * Retrieve a value from the stored settings. + * + * @param key One of the settings keys. + * @return value stored in settings, defValue if the key does not exist. + */ + public <T> T get(Key<T> key) { + return getObject(key.key, key.defaultValue); + } + + /** + * Base class to store string key and its default value. + * + * @param <T> Type of the value. + */ + public static class Key<T> { + public final String key; + public final T defaultValue; + + private Key(String key, T defaultValue) { + this.key = key; + this.defaultValue = defaultValue; + } + + @Override + public String toString() { + return "[Key: " + key + ", DefaultValue: " + defaultValue + "]"; + } + } + + private void writeToStoreFile() { + try { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + final PersistableBundle bundleToWrite; + synchronized (mLock) { + bundleToWrite = new PersistableBundle(mSettings); + } + bundleToWrite.putInt(VERSION_KEY, CURRENT_SETTINGS_STORE_DATA_VERSION); + bundleToWrite.writeToStream(outputStream); + synchronized (mLock) { + writeToAtomicFile(mAtomicFile, outputStream.toByteArray()); + } + } catch (IOException e) { + Log.wtf(TAG, "Write to store file failed", e); + } + } + + private void readFromStoreFile() { + try { + final byte[] readData; + synchronized (mLock) { + Log.i(TAG, "Reading from store file: " + mAtomicFile.getBaseFile()); + readData = readFromAtomicFile(mAtomicFile); + } + final ByteArrayInputStream inputStream = new ByteArrayInputStream(readData); + final PersistableBundle bundleRead = PersistableBundle.readFromStream(inputStream); + // Version unused for now. May be needed in the future for handling migrations. + bundleRead.remove(VERSION_KEY); + synchronized (mLock) { + mSettings.putAll(bundleRead); + } + } catch (FileNotFoundException e) { + Log.e(TAG, "No store file to read", e); + } catch (IOException e) { + Log.e(TAG, "Read from store file failed", e); + } + } + + /** + * Read raw data from the atomic file. Note: This is a copy of {@link AtomicFile#readFully()} + * modified to use the passed in {@link InputStream} which was returned using {@link + * AtomicFile#openRead()}. + */ + private static byte[] readFromAtomicFile(AtomicFile file) throws IOException { + FileInputStream stream = null; + try { + stream = file.openRead(); + int pos = 0; + int avail = stream.available(); + byte[] data = new byte[avail]; + while (true) { + int amt = stream.read(data, pos, data.length - pos); + if (amt <= 0) { + return data; + } + pos += amt; + avail = stream.available(); + if (avail > data.length - pos) { + byte[] newData = new byte[pos + avail]; + System.arraycopy(data, 0, newData, 0, pos); + data = newData; + } + } + } finally { + if (stream != null) stream.close(); + } + } + + /** Write the raw data to the atomic file. */ + private static void writeToAtomicFile(AtomicFile file, byte[] data) throws IOException { + // Write the data to the atomic file. + FileOutputStream out = null; + try { + out = file.startWrite(); + out.write(data); + file.finishWrite(out); + } catch (IOException e) { + if (out != null) { + file.failWrite(out); + } + throw e; + } + } +} diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java index 362ff3973c..e02e74da67 100644 --- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java +++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java @@ -521,7 +521,7 @@ public class ThreadNetworkControllerTest { } @Test - public void scheduleMigration_withPrivilegedPermission_success() throws Exception { + public void scheduleMigration_withPrivilegedPermission_newDatasetApplied() throws Exception { grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); for (ThreadNetworkController controller : getAllControllers()) { @@ -548,11 +548,32 @@ public class ThreadNetworkControllerTest { controller.scheduleMigration( pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture)); - migrateFuture.get(); - Thread.sleep(35 * 1000); - assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2); - assertThat(getPendingOperationalDataset(controller)).isNull(); + + SettableFuture<Boolean> dataset2IsApplied = SettableFuture.create(); + SettableFuture<Boolean> pendingDatasetIsRemoved = SettableFuture.create(); + OperationalDatasetCallback datasetCallback = + new OperationalDatasetCallback() { + @Override + public void onActiveOperationalDatasetChanged( + ActiveOperationalDataset activeDataset) { + if (activeDataset.equals(activeDataset2)) { + dataset2IsApplied.set(true); + } + } + + @Override + public void onPendingOperationalDatasetChanged( + PendingOperationalDataset pendingDataset) { + if (pendingDataset == null) { + pendingDatasetIsRemoved.set(true); + } + } + }; + controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback); + assertThat(dataset2IsApplied.get()).isTrue(); + assertThat(pendingDatasetIsRemoved.get()).isTrue(); + controller.unregisterOperationalDatasetCallback(datasetCallback); } } @@ -629,7 +650,8 @@ public class ThreadNetworkControllerTest { } @Test - public void scheduleMigration_secondRequestHasLargerTimestamp_success() throws Exception { + public void scheduleMigration_secondRequestHasLargerTimestamp_newDatasetApplied() + throws Exception { grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED); for (ThreadNetworkController controller : getAllControllers()) { @@ -669,11 +691,32 @@ public class ThreadNetworkControllerTest { migrateFuture1.get(); controller.scheduleMigration( pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2)); - migrateFuture2.get(); - Thread.sleep(35 * 1000); - assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2); - assertThat(getPendingOperationalDataset(controller)).isNull(); + + SettableFuture<Boolean> dataset2IsApplied = SettableFuture.create(); + SettableFuture<Boolean> pendingDatasetIsRemoved = SettableFuture.create(); + OperationalDatasetCallback datasetCallback = + new OperationalDatasetCallback() { + @Override + public void onActiveOperationalDatasetChanged( + ActiveOperationalDataset activeDataset) { + if (activeDataset.equals(activeDataset2)) { + dataset2IsApplied.set(true); + } + } + + @Override + public void onPendingOperationalDatasetChanged( + PendingOperationalDataset pendingDataset) { + if (pendingDataset == null) { + pendingDatasetIsRemoved.set(true); + } + } + }; + controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback); + assertThat(dataset2IsApplied.get()).isTrue(); + assertThat(pendingDatasetIsRemoved.get()).isTrue(); + controller.unregisterOperationalDatasetCallback(datasetCallback); } } diff --git a/thread/tests/integration/Android.bp b/thread/tests/integration/Android.bp new file mode 100644 index 0000000000..405fb76f72 --- /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 0000000000..a347654857 --- /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 0000000000..5d3818a4b5 --- /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 0000000000..01638f3574 --- /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 0000000000..43a800dafa --- /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 0000000000..9d9a4ff749 --- /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/Android.bp b/thread/tests/unit/Android.bp index 80926936dc..291475e788 100644 --- a/thread/tests/unit/Android.bp +++ b/thread/tests/unit/Android.bp @@ -31,20 +31,35 @@ android_test { "general-tests", ], static_libs: [ - "androidx.test.ext.junit", - "compatibility-device-util-axt", + "frameworks-base-testutils", "framework-connectivity-pre-jarjar", "framework-connectivity-t-pre-jarjar", + "framework-location.stubs.module_lib", "guava", "guava-android-testlib", - "mockito-target-minus-junit4", + "mockito-target-extended-minus-junit4", "net-tests-utils", + "ot-daemon-aidl-java", + "ot-daemon-testing", + "service-connectivity-pre-jarjar", + "service-thread-pre-jarjar", "truth", + "service-thread-pre-jarjar", ], libs: [ "android.test.base", "android.test.runner", + "ServiceConnectivityResources", + "framework-wifi", ], + jni_libs: [ + "libservice-thread-jni", + + // these are needed for Extended Mockito + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + jni_uses_platform_apis: true, jarjar_rules: ":connectivity-jarjar-rules", // Test coverage system runs on different devices. Need to // compile for all architectures. diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml index 597c6a82cc..26813c1ab0 100644 --- a/thread/tests/unit/AndroidTest.xml +++ b/thread/tests/unit/AndroidTest.xml @@ -30,5 +30,8 @@ <option name="hidden-api-checks" value="false"/> <!-- Ignores tests introduced by guava-android-testlib --> <option name="exclude-annotation" value="org.junit.Ignore"/> + <!-- Ignores tests introduced by frameworks-base-testutils --> + <option name="exclude-filter" value="android.os.test.TestLooperTest"/> + <option name="exclude-filter" value="com.android.test.filters.SelectTestTests"/> </test> </configuration> diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java index 7284968337..e92dcb9dfa 100644 --- a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java +++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java @@ -33,12 +33,8 @@ import com.google.common.primitives.Bytes; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.security.SecureRandom; -import java.util.Random; - /** Unit tests for {@link ActiveOperationalDataset}. */ @SmallTest @RunWith(AndroidJUnit4.class) @@ -62,9 +58,6 @@ public class ActiveOperationalDatasetTest { + "642D643961300102D9A00410A245479C836D551B9CA557F7" + "B9D351B40C0402A0FFF8"); - @Mock private Random mockRandom; - @Mock private SecureRandom mockSecureRandom; - @Before public void setUp() { MockitoAnnotations.initMocks(this); diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java index 2f120b2475..75eb043180 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.any; 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 @@ public final class ThreadNetworkControllerTest { 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 @@ public final class ThreadNetworkControllerTest { 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()); + } } diff --git a/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java new file mode 100644 index 0000000000..11aabb803e --- /dev/null +++ b/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2024 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.thread; + +import static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.validateMockitoUsage; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.PersistableBundle; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.AtomicFile; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; + +/** Unit tests for {@link ThreadPersistentSettings}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ThreadPersistentSettingsTest { + @Mock private AtomicFile mAtomicFile; + + private ThreadPersistentSettings mThreadPersistentSetting; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + FileOutputStream fos = mock(FileOutputStream.class); + when(mAtomicFile.startWrite()).thenReturn(fos); + mThreadPersistentSetting = new ThreadPersistentSettings(mAtomicFile); + } + + /** Called after each test */ + @After + public void tearDown() { + validateMockitoUsage(); + } + + @Test + public void put_ThreadFeatureEnabledTrue_returnsTrue() throws Exception { + mThreadPersistentSetting.put(THREAD_ENABLED.key, true); + + assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isTrue(); + // Confirm that file writes have been triggered. + verify(mAtomicFile).startWrite(); + verify(mAtomicFile).finishWrite(any()); + } + + @Test + public void put_ThreadFeatureEnabledFalse_returnsFalse() throws Exception { + mThreadPersistentSetting.put(THREAD_ENABLED.key, false); + + assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isFalse(); + // Confirm that file writes have been triggered. + verify(mAtomicFile).startWrite(); + verify(mAtomicFile).finishWrite(any()); + } + + @Test + public void initialize_readsFromFile() throws Exception { + byte[] data = createXmlForParsing(THREAD_ENABLED.key, false); + setupAtomicFileMockForRead(data); + + // Trigger file read. + mThreadPersistentSetting.initialize(); + + assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isFalse(); + verify(mAtomicFile, never()).startWrite(); + } + + private byte[] createXmlForParsing(String key, Boolean value) throws Exception { + PersistableBundle bundle = new PersistableBundle(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + bundle.putBoolean(key, value); + bundle.writeToStream(outputStream); + return outputStream.toByteArray(); + } + + private void setupAtomicFileMockForRead(byte[] dataToRead) throws Exception { + FileInputStream is = mock(FileInputStream.class); + when(mAtomicFile.openRead()).thenReturn(is); + when(is.available()).thenReturn(dataToRead.length).thenReturn(0); + doAnswer( + invocation -> { + byte[] data = invocation.getArgument(0); + int pos = invocation.getArgument(1); + if (pos == dataToRead.length) return 0; // read complete. + System.arraycopy(dataToRead, 0, data, 0, dataToRead.length); + return dataToRead.length; + }) + .when(is) + .read(any(), anyInt(), anyInt()); + } +} diff --git a/thread/tests/unit/src/com/android/server/thread/BinderUtil.java b/thread/tests/unit/src/com/android/server/thread/BinderUtil.java new file mode 100644 index 0000000000..3614bcea8f --- /dev/null +++ b/thread/tests/unit/src/com/android/server/thread/BinderUtil.java @@ -0,0 +1,31 @@ +/* + * 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.thread; + +import android.os.Binder; + +/** Utilities for faking the calling uid in Binder. */ +public class BinderUtil { + /** + * Fake the calling uid in Binder. + * + * @param uid the calling uid that Binder should return from now on + */ + public static void setUid(int uid) { + Binder.restoreCallingIdentity((((long) uid) << 32) | Binder.getCallingPid()); + } +} diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java new file mode 100644 index 0000000000..44a8ab7892 --- /dev/null +++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java @@ -0,0 +1,163 @@ +/* + * 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.thread; + +import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR; +import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED; + +import static com.android.testutils.TestPermissionUtil.runAsShell; + +import static com.google.common.io.BaseEncoding.base16; +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +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.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkAgent; +import android.net.NetworkProvider; +import android.net.thread.ActiveOperationalDataset; +import android.net.thread.IOperationReceiver; +import android.os.Handler; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.test.TestLooper; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.server.thread.openthread.testing.FakeOtDaemon; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Unit tests for {@link ThreadNetworkControllerService}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class ThreadNetworkControllerServiceTest { + // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new": + // Active Timestamp: 1 + // Channel: 19 + // Channel Mask: 0x07FFF800 + // Ext PAN ID: ACC214689BC40BDF + // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64 + // Network Key: F26B3153760F519A63BAFDDFFC80D2AF + // Network Name: OpenThread-d9a0 + // PAN ID: 0xD9A0 + // PSKc: A245479C836D551B9CA557F7B9D351B4 + // Security Policy: 672 onrcb + private static final byte[] DEFAULT_ACTIVE_DATASET_TLVS = + base16().decode( + "0E080000000000010000000300001335060004001FFFE002" + + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31" + + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561" + + "642D643961300102D9A00410A245479C836D551B9CA557F7" + + "B9D351B40C0402A0FFF8"); + private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET = + ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS); + + @Mock private ConnectivityManager mMockConnectivityManager; + @Mock private NetworkAgent mMockNetworkAgent; + @Mock private TunInterfaceController mMockTunIfController; + @Mock private ParcelFileDescriptor mMockTunFd; + @Mock private InfraInterfaceController mMockInfraIfController; + private Context mContext; + private TestLooper mTestLooper; + private FakeOtDaemon mFakeOtDaemon; + private ThreadNetworkControllerService mService; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = ApplicationProvider.getApplicationContext(); + mTestLooper = new TestLooper(); + final Handler handler = new Handler(mTestLooper.getLooper()); + NetworkProvider networkProvider = + new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider"); + + mFakeOtDaemon = new FakeOtDaemon(handler); + + when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd); + + mService = + new ThreadNetworkControllerService( + ApplicationProvider.getApplicationContext(), + handler, + networkProvider, + () -> mFakeOtDaemon, + mMockConnectivityManager, + mMockTunIfController, + mMockInfraIfController); + mService.setTestNetworkAgent(mMockNetworkAgent); + } + + @Test + public void initialize_tunInterfaceSetToOtDaemon() throws Exception { + when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd); + + mService.initialize(); + mTestLooper.dispatchAll(); + + verify(mMockTunIfController, times(1)).createTunInterface(); + assertThat(mFakeOtDaemon.getTunFd()).isEqualTo(mMockTunFd); + } + + @Test + public void join_otDaemonRemoteFailure_returnsInternalError() throws Exception { + mService.initialize(); + final IOperationReceiver mockReceiver = mock(IOperationReceiver.class); + mFakeOtDaemon.setJoinException(new RemoteException("ot-daemon join() throws")); + + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver)); + mTestLooper.dispatchAll(); + + verify(mockReceiver, never()).onSuccess(); + verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString()); + } + + @Test + public void join_succeed_threadNetworkRegistered() throws Exception { + mService.initialize(); + final IOperationReceiver mockReceiver = mock(IOperationReceiver.class); + + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver)); + // Here needs to call Testlooper#dispatchAll twices because TestLooper#moveTimeForward + // operates on only currently enqueued messages but the delayed message is posted from + // another Handler task. + mTestLooper.dispatchAll(); + mTestLooper.moveTimeForward(FakeOtDaemon.JOIN_DELAY.toMillis() + 100); + mTestLooper.dispatchAll(); + + verify(mockReceiver, times(1)).onSuccess(); + verify(mMockNetworkAgent, times(1)).register(); + } +} diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java new file mode 100644 index 0000000000..17cdd01529 --- /dev/null +++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java @@ -0,0 +1,409 @@ +/* + * 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.thread; + +import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR; + +import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyDouble; +import static org.mockito.Mockito.anyFloat; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.location.Address; +import android.location.Geocoder; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.net.thread.IOperationReceiver; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.connectivity.resources.R; +import com.android.server.connectivity.ConnectivityResources; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; + +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +/** Unit tests for {@link ThreadNetworkCountryCode}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ThreadNetworkCountryCodeTest { + private static final String TEST_COUNTRY_CODE_US = "US"; + private static final String TEST_COUNTRY_CODE_CN = "CN"; + private static final int TEST_SIM_SLOT_INDEX_0 = 0; + private static final int TEST_SIM_SLOT_INDEX_1 = 1; + + @Mock Context mContext; + @Mock LocationManager mLocationManager; + @Mock Geocoder mGeocoder; + @Mock ThreadNetworkControllerService mThreadNetworkControllerService; + @Mock PackageManager mPackageManager; + @Mock Location mLocation; + @Mock Resources mResources; + @Mock ConnectivityResources mConnectivityResources; + @Mock WifiManager mWifiManager; + @Mock SubscriptionManager mSubscriptionManager; + @Mock TelephonyManager mTelephonyManager; + @Mock List<SubscriptionInfo> mSubscriptionInfoList; + @Mock SubscriptionInfo mSubscriptionInfo0; + @Mock SubscriptionInfo mSubscriptionInfo1; + + private ThreadNetworkCountryCode mThreadNetworkCountryCode; + private boolean mErrorSetCountryCode; + + @Captor private ArgumentCaptor<LocationListener> mLocationListenerCaptor; + @Captor private ArgumentCaptor<Geocoder.GeocodeListener> mGeocodeListenerCaptor; + @Captor private ArgumentCaptor<IOperationReceiver> mOperationReceiverCaptor; + @Captor private ArgumentCaptor<ActiveCountryCodeChangedCallback> mWifiCountryCodeReceiverCaptor; + @Captor private ArgumentCaptor<BroadcastReceiver> mTelephonyCountryCodeReceiverCaptor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + when(mConnectivityResources.get()).thenReturn(mResources); + when(mResources.getBoolean(anyInt())).thenReturn(true); + + when(mSubscriptionManager.getActiveSubscriptionInfoList()) + .thenReturn(mSubscriptionInfoList); + Iterator<SubscriptionInfo> iteratorMock = mock(Iterator.class); + when(mSubscriptionInfoList.size()).thenReturn(2); + when(mSubscriptionInfoList.iterator()).thenReturn(iteratorMock); + when(iteratorMock.hasNext()).thenReturn(true).thenReturn(true).thenReturn(false); + when(iteratorMock.next()).thenReturn(mSubscriptionInfo0).thenReturn(mSubscriptionInfo1); + when(mSubscriptionInfo0.getSimSlotIndex()).thenReturn(TEST_SIM_SLOT_INDEX_0); + when(mSubscriptionInfo1.getSimSlotIndex()).thenReturn(TEST_SIM_SLOT_INDEX_1); + + when(mLocation.getLatitude()).thenReturn(0.0); + when(mLocation.getLongitude()).thenReturn(0.0); + + Answer setCountryCodeCallback = + invocation -> { + Object[] args = invocation.getArguments(); + IOperationReceiver cb = (IOperationReceiver) args[1]; + + if (mErrorSetCountryCode) { + cb.onError(ERROR_INTERNAL_ERROR, new String("Invalid country code")); + } else { + cb.onSuccess(); + } + return new Object(); + }; + + doAnswer(setCountryCodeCallback) + .when(mThreadNetworkControllerService) + .setCountryCode(any(), any(IOperationReceiver.class)); + + mThreadNetworkCountryCode = + new ThreadNetworkCountryCode( + mLocationManager, + mThreadNetworkControllerService, + mGeocoder, + mConnectivityResources, + mWifiManager, + mContext, + mTelephonyManager, + mSubscriptionManager); + } + + private static Address newAddress(String countryCode) { + Address address = new Address(Locale.ROOT); + address.setCountryCode(countryCode); + return address; + } + + @Test + public void initialize_defaultCountryCodeIsUsed() { + mThreadNetworkCountryCode.initialize(); + + assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE); + } + + @Test + public void initialize_locationUseIsDisabled_locationFunctionIsNotCalled() { + when(mResources.getBoolean(R.bool.config_thread_location_use_for_country_code_enabled)) + .thenReturn(false); + + mThreadNetworkCountryCode.initialize(); + + verifyNoMoreInteractions(mGeocoder); + verifyNoMoreInteractions(mLocationManager); + } + + @Test + public void locationCountryCode_locationChanged_locationCountryCodeIsUsed() { + mThreadNetworkCountryCode.initialize(); + + verify(mLocationManager) + .requestLocationUpdates( + anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture()); + mLocationListenerCaptor.getValue().onLocationChanged(mLocation); + verify(mGeocoder) + .getFromLocation( + anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture()); + mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US))); + + assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US); + } + + @Test + public void wifiCountryCode_bothWifiAndLocationAreAvailable_wifiCountryCodeIsUsed() { + mThreadNetworkCountryCode.initialize(); + verify(mLocationManager) + .requestLocationUpdates( + anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture()); + mLocationListenerCaptor.getValue().onLocationChanged(mLocation); + verify(mGeocoder) + .getFromLocation( + anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture()); + + Address mockAddress = mock(Address.class); + when(mockAddress.getCountryCode()).thenReturn(TEST_COUNTRY_CODE_US); + List<Address> addresses = List.of(mockAddress); + mGeocodeListenerCaptor.getValue().onGeocode(addresses); + + verify(mWifiManager) + .registerActiveCountryCodeChangedCallback( + any(), mWifiCountryCodeReceiverCaptor.capture()); + mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_CN); + + assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN); + } + + @Test + public void wifiCountryCode_wifiCountryCodeIsActive_wifiCountryCodeIsUsed() { + mThreadNetworkCountryCode.initialize(); + + verify(mWifiManager) + .registerActiveCountryCodeChangedCallback( + any(), mWifiCountryCodeReceiverCaptor.capture()); + mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_US); + + assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US); + } + + @Test + public void wifiCountryCode_wifiCountryCodeIsInactive_defaultCountryCodeIsUsed() { + mThreadNetworkCountryCode.initialize(); + verify(mWifiManager) + .registerActiveCountryCodeChangedCallback( + any(), mWifiCountryCodeReceiverCaptor.capture()); + mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_US); + + mWifiCountryCodeReceiverCaptor.getValue().onCountryCodeInactive(); + + assertThat(mThreadNetworkCountryCode.getCountryCode()) + .isEqualTo(ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE); + } + + @Test + public void telephonyCountryCode_bothTelephonyAndLocationAvailable_telephonyCodeIsUsed() { + mThreadNetworkCountryCode.initialize(); + verify(mLocationManager) + .requestLocationUpdates( + anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture()); + mLocationListenerCaptor.getValue().onLocationChanged(mLocation); + verify(mGeocoder) + .getFromLocation( + anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture()); + mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US))); + + verify(mContext) + .registerReceiver( + mTelephonyCountryCodeReceiverCaptor.capture(), + any(), + eq(Context.RECEIVER_EXPORTED)); + Intent intent = + new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED) + .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN) + .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0); + mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent); + + assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN); + } + + @Test + public void telephonyCountryCode_locationIsAvailable_lastKnownTelephonyCodeIsUsed() { + mThreadNetworkCountryCode.initialize(); + verify(mLocationManager) + .requestLocationUpdates( + anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture()); + mLocationListenerCaptor.getValue().onLocationChanged(mLocation); + verify(mGeocoder) + .getFromLocation( + anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture()); + mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US))); + + verify(mContext) + .registerReceiver( + mTelephonyCountryCodeReceiverCaptor.capture(), + any(), + eq(Context.RECEIVER_EXPORTED)); + Intent intent = + new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED) + .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, "") + .putExtra( + TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY, + TEST_COUNTRY_CODE_US) + .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0); + mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent); + + assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US); + } + + @Test + public void telephonyCountryCode_lastKnownCountryCodeAvailable_telephonyCodeIsUsed() { + mThreadNetworkCountryCode.initialize(); + verify(mContext) + .registerReceiver( + mTelephonyCountryCodeReceiverCaptor.capture(), + any(), + eq(Context.RECEIVER_EXPORTED)); + Intent intent0 = + new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED) + .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, "") + .putExtra( + TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY, + TEST_COUNTRY_CODE_US) + .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0); + mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent0); + + verify(mContext) + .registerReceiver( + mTelephonyCountryCodeReceiverCaptor.capture(), + any(), + eq(Context.RECEIVER_EXPORTED)); + Intent intent1 = + new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED) + .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN) + .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_1); + mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent1); + + assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN); + } + + @Test + public void telephonyCountryCode_multipleSims_firstSimIsUsed() { + mThreadNetworkCountryCode.initialize(); + verify(mContext) + .registerReceiver( + mTelephonyCountryCodeReceiverCaptor.capture(), + any(), + eq(Context.RECEIVER_EXPORTED)); + Intent intent1 = + new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED) + .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN) + .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_1); + mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent1); + + Intent intent0 = + new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED) + .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN) + .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0); + mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent0); + + assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN); + } + + @Test + public void updateCountryCode_noForceUpdateDefaultCountryCode_noCountryCodeIsUpdated() { + mThreadNetworkCountryCode.initialize(); + clearInvocations(mThreadNetworkControllerService); + + mThreadNetworkCountryCode.updateCountryCode(false /* forceUpdate */); + + verify(mThreadNetworkControllerService, never()).setCountryCode(any(), any()); + } + + @Test + public void updateCountryCode_forceUpdateDefaultCountryCode_countryCodeIsUpdated() { + mThreadNetworkCountryCode.initialize(); + clearInvocations(mThreadNetworkControllerService); + + mThreadNetworkCountryCode.updateCountryCode(true /* forceUpdate */); + + verify(mThreadNetworkControllerService) + .setCountryCode(eq(DEFAULT_COUNTRY_CODE), mOperationReceiverCaptor.capture()); + } + + @Test + public void setOverrideCountryCode_defaultCountryCodeAvailable_overrideCountryCodeIsUsed() { + mThreadNetworkCountryCode.initialize(); + + mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN); + + assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN); + } + + @Test + public void clearOverrideCountryCode_defaultCountryCodeAvailable_defaultCountryCodeIsUsed() { + mThreadNetworkCountryCode.initialize(); + mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN); + + mThreadNetworkCountryCode.clearOverrideCountryCode(); + + assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE); + } + + @Test + public void setCountryCodeFailed_defaultCountryCodeAvailable_countryCodeIsNotUpdated() { + mThreadNetworkCountryCode.initialize(); + + mErrorSetCountryCode = true; + mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN); + + verify(mThreadNetworkControllerService) + .setCountryCode(eq(TEST_COUNTRY_CODE_CN), mOperationReceiverCaptor.capture()); + assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE); + } +} diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java new file mode 100644 index 0000000000..c7e0eca18b --- /dev/null +++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java @@ -0,0 +1,140 @@ +/* + * 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.thread; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.contains; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.validateMockitoUsage; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Binder; +import android.os.Process; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** Unit tests for {@link ThreadNetworkShellCommand}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ThreadNetworkShellCommandTest { + private static final String TAG = "ThreadNetworkShellCommandTTest"; + @Mock ThreadNetworkService mThreadNetworkService; + @Mock ThreadNetworkCountryCode mThreadNetworkCountryCode; + @Mock PrintWriter mErrorWriter; + @Mock PrintWriter mOutputWriter; + + ThreadNetworkShellCommand mThreadNetworkShellCommand; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mThreadNetworkShellCommand = new ThreadNetworkShellCommand(mThreadNetworkCountryCode); + mThreadNetworkShellCommand.setPrintWriters(mOutputWriter, mErrorWriter); + } + + @After + public void tearDown() throws Exception { + validateMockitoUsage(); + } + + @Test + public void getCountryCode_executeInUnrootedShell_allowed() { + BinderUtil.setUid(Process.SHELL_UID); + when(mThreadNetworkCountryCode.getCountryCode()).thenReturn("US"); + + mThreadNetworkShellCommand.exec( + new Binder(), + new FileDescriptor(), + new FileDescriptor(), + new FileDescriptor(), + new String[] {"get-country-code"}); + + verify(mOutputWriter).println(contains("US")); + } + + @Test + public void forceSetCountryCodeEnabled_executeInUnrootedShell_notAllowed() { + BinderUtil.setUid(Process.SHELL_UID); + + mThreadNetworkShellCommand.exec( + new Binder(), + new FileDescriptor(), + new FileDescriptor(), + new FileDescriptor(), + new String[] {"force-country-code", "enabled", "US"}); + + verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(eq("US")); + verify(mErrorWriter).println(contains("force-country-code")); + } + + @Test + public void forceSetCountryCodeEnabled_executeInRootedShell_allowed() { + BinderUtil.setUid(Process.ROOT_UID); + + mThreadNetworkShellCommand.exec( + new Binder(), + new FileDescriptor(), + new FileDescriptor(), + new FileDescriptor(), + new String[] {"force-country-code", "enabled", "US"}); + + verify(mThreadNetworkCountryCode).setOverrideCountryCode(eq("US")); + } + + @Test + public void forceSetCountryCodeDisabled_executeInUnrootedShell_notAllowed() { + BinderUtil.setUid(Process.SHELL_UID); + + mThreadNetworkShellCommand.exec( + new Binder(), + new FileDescriptor(), + new FileDescriptor(), + new FileDescriptor(), + new String[] {"force-country-code", "disabled"}); + + verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(any()); + verify(mErrorWriter).println(contains("force-country-code")); + } + + @Test + public void forceSetCountryCodeDisabled_executeInRootedShell_allowed() { + BinderUtil.setUid(Process.ROOT_UID); + + mThreadNetworkShellCommand.exec( + new Binder(), + new FileDescriptor(), + new FileDescriptor(), + new FileDescriptor(), + new String[] {"force-country-code", "disabled"}); + + verify(mThreadNetworkCountryCode).clearOverrideCountryCode(); + } +} |