Merge "Skip all of NetworkAgentTest in instant mode."
diff --git a/Cronet/tests/common/Android.bp b/Cronet/tests/common/Android.bp
index 5d2f6e5..e17081a 100644
--- a/Cronet/tests/common/Android.bp
+++ b/Cronet/tests/common/Android.bp
@@ -26,7 +26,6 @@
// go with merging NetHttp and Tethering targets.
android_test {
name: "NetHttpCoverageTests",
- defaults: ["CronetTestJavaDefaults"],
enforce_default_target_sdk_version: true,
min_sdk_version: "30",
test_suites: ["general-tests", "mts-tethering"],
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index 22eccf9..44b3364 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -18,38 +18,10 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-// cronet_test_java_defaults can be used to specify a java_defaults target that
-// either enables or disables Cronet tests. This is used to disable Cronet
-// tests on tm-mainline-prod where the required APIs are not present.
-cronet_test_java_defaults = "CronetTestJavaDefaultsEnabled"
-// This is a placeholder comment to avoid merge conflicts
-// as cronet_test_java_defaults may have different values
-// depending on the branch
-
-java_defaults {
- name: "CronetTestJavaDefaultsEnabled",
- enabled: true,
- // TODO(danstahr): move to unconditional static_libs once the T branch is abandoned
- static_libs: [
- "truth",
- ],
-}
-
-java_defaults {
- name: "CronetTestJavaDefaultsDisabled",
- enabled: false,
-}
-
-java_defaults {
- name: "CronetTestJavaDefaults",
- defaults: [cronet_test_java_defaults],
-}
-
android_library {
name: "CtsNetHttpTestsLib",
defaults: [
"cts_defaults",
- "CronetTestJavaDefaults",
],
sdk_version: "test_current",
min_sdk_version: "30",
@@ -61,10 +33,11 @@
"androidx.test.ext.junit",
"ctstestrunner-axt",
"ctstestserver",
- "junit",
"hamcrest-library",
+ "junit",
"kotlin-test",
"mockito-target",
+ "truth",
],
libs: [
"android.test.base",
@@ -79,7 +52,6 @@
name: "CtsNetHttpTestCases",
defaults: [
"cts_defaults",
- "CronetTestJavaDefaults",
],
sdk_version: "test_current",
static_libs: ["CtsNetHttpTestsLib"],
diff --git a/Cronet/tests/mts/Android.bp b/Cronet/tests/mts/Android.bp
index ecf4b7f..93564e4 100644
--- a/Cronet/tests/mts/Android.bp
+++ b/Cronet/tests/mts/Android.bp
@@ -19,7 +19,6 @@
java_genrule {
name: "net-http-test-jarjar-rules",
- defaults: ["CronetTestJavaDefaults"],
tool_files: [
":NetHttpTestsLibPreJarJar{.jar}",
"jarjar_excludes.txt",
@@ -37,7 +36,6 @@
android_library {
name: "NetHttpTestsLibPreJarJar",
- defaults: ["CronetTestJavaDefaults"],
srcs: [":cronet_aml_javatests_sources"],
sdk_version: "module_current",
min_sdk_version: "30",
@@ -46,6 +44,7 @@
"androidx.test.ext.junit",
"androidx.test.rules",
"junit",
+ "truth",
],
libs: [
"android.test.base",
@@ -59,7 +58,6 @@
android_test {
name: "NetHttpTests",
defaults: [
- "CronetTestJavaDefaults",
"mts-target-sdk-version-current",
],
static_libs: ["NetHttpTestsLibPreJarJar"],
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 4506e5a..4bfd1fb 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -18,13 +18,6 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-prebuilt_etc {
- name: "TetheringOutOfProcessFlag",
- src: "out-of-process",
- filename_from_src: true,
- sub_dir: "flag",
-}
-
// Defaults to enable/disable java targets which uses development APIs. "enabled" may have a
// different value depending on the branch.
java_defaults {
@@ -50,22 +43,8 @@
// as the above target may have different "enabled" values
// depending on the branch
-// cronet_in_tethering_apex_defaults can be used to specify an apex_defaults target that either
-// enables or disables inclusion of Cronet in the Tethering apex. This is used to disable Cronet
-// on tm-mainline-prod. Note: in order for Cronet APIs to work Cronet must also be enabled
-// by the cronet_java_*_defaults in common/TetheringLib/Android.bp.
-cronet_in_tethering_apex_defaults = "CronetInTetheringApexDefaultsEnabled"
-// This is a placeholder comment to avoid merge conflicts
-// as cronet_apex_defaults may have different values
-// depending on the branch
-
apex_defaults {
name: "CronetInTetheringApexDefaults",
- defaults: [cronet_in_tethering_apex_defaults],
-}
-
-apex_defaults {
- name: "CronetInTetheringApexDefaultsEnabled",
jni_libs: [
"cronet_aml_components_cronet_android_cronet",
"//external/cronet/third_party/boringssl:libcrypto",
@@ -83,10 +62,6 @@
},
}
-apex_defaults {
- name: "CronetInTetheringApexDefaultsDisabled",
-}
-
apex {
name: "com.android.tethering",
defaults: [
@@ -140,7 +115,6 @@
prebuilts: [
"current_sdkinfo",
"privapp_allowlist_com.android.tethering",
- "TetheringOutOfProcessFlag",
],
manifest: "manifest.json",
key: "com.android.tethering.key",
diff --git a/Tethering/apex/in-process b/Tethering/apex/in-process
deleted file mode 100644
index e69de29..0000000
--- a/Tethering/apex/in-process
+++ /dev/null
diff --git a/Tethering/apex/out-of-process b/Tethering/apex/out-of-process
deleted file mode 100644
index e69de29..0000000
--- a/Tethering/apex/out-of-process
+++ /dev/null
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index 6b62da9..a4db776 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -17,16 +17,6 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-// Both cronet_java_defaults and cronet_java_prejarjar_defaults can be used to
-// specify a java_defaults target that either enables or disables Cronet. This
-// is used to disable Cronet on tm-mainline-prod.
-// Note: they must either both be enabled or disabled.
-cronet_java_defaults = "CronetJavaDefaultsEnabled"
-cronet_java_prejarjar_defaults = "CronetJavaPrejarjarDefaultsEnabled"
-// This is a placeholder comment to avoid merge conflicts
-// as cronet_defaults may have different values
-// depending on the branch
-
java_sdk_library {
name: "framework-tethering",
defaults: [
@@ -67,44 +57,6 @@
lint: { strict_updatability_linting: true },
}
-java_defaults {
- name: "CronetJavaDefaults",
- defaults: [cronet_java_defaults],
-}
-
-java_defaults {
- name: "CronetJavaDefaultsEnabled",
- srcs: [":cronet_aml_api_sources"],
- libs: [
- "androidx.annotation_annotation",
- ],
- impl_only_static_libs: [
- "cronet_aml_java",
- ],
-}
-
-java_defaults {
- name: "CronetJavaDefaultsDisabled",
- api_dir: "cronet_disabled/api",
-}
-
-java_defaults {
- name: "CronetJavaPrejarjarDefaults",
- defaults: [cronet_java_prejarjar_defaults],
-}
-
-java_defaults {
- name: "CronetJavaPrejarjarDefaultsDisabled",
-}
-
-java_defaults {
- name: "CronetJavaPrejarjarDefaultsEnabled",
- static_libs: [
- "cronet_aml_api_java",
- "cronet_aml_java"
- ],
-}
-
java_library {
name: "framework-tethering-pre-jarjar",
defaults: [
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index 6d502ce..b88b13b 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -459,8 +459,9 @@
}
@VisibleForTesting
- PendingIntent createRecheckAlarmIntent() {
+ PendingIntent createRecheckAlarmIntent(final String pkgName) {
final Intent intent = new Intent(ACTION_PROVISIONING_ALARM);
+ intent.setPackage(pkgName);
return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
}
@@ -470,7 +471,7 @@
final int period = config.provisioningCheckPeriod;
if (period <= 0) return;
- mProvisioningRecheckAlarm = createRecheckAlarmIntent();
+ mProvisioningRecheckAlarm = createRecheckAlarmIntent(mContext.getPackageName());
AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
Context.ALARM_SERVICE);
long triggerAtMillis = SystemClock.elapsedRealtime() + (period * MS_PER_HOUR);
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 21927df..4ee5c42 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -39,7 +39,6 @@
import static com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -871,10 +870,10 @@
final ByteBuffer packet = ByteBuffer.wrap(ZeroLengthDhcpPacket);
tester.sendUploadPacket(packet);
- // Send DHCPDISCOVER packet from another downstream tethered device to verify that upstream
- // DHCP server has closed the listening socket and stopped reading, then we will not receive
- // any DHCPOFFER in this case.
+ // Send DHCPDISCOVER packet from another downstream tethered device to verify that
+ // upstream DHCP server doesn't close the listening socket and stop reading, then we
+ // can still receive the next DHCP packet from server.
final MacAddress macAddress = MacAddress.fromString("11:22:33:44:55:66");
- assertFalse(tester.testDhcpServerAlive(macAddress));
+ assertTrue(tester.testDhcpServerAlive(macAddress));
}
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index e4263db..c2e1617 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -213,7 +213,8 @@
}
@Override
- PendingIntent createRecheckAlarmIntent() {
+ PendingIntent createRecheckAlarmIntent(final String pkgName) {
+ assertEquals(TEST_PACKAGE_NAME, pkgName);
return mAlarmIntent;
}
}
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 c15b85e..bd8b325 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -2988,9 +2988,9 @@
final MacAddress testMac1 = MacAddress.fromString("11:11:11:11:11:11");
final DhcpLeaseParcelable p2pLease = createDhcpLeaseParcelable("clientId1", testMac1,
"192.168.50.24", 24, Long.MAX_VALUE, "test1");
- final List<TetheredClient> p2pClients = notifyDhcpLeasesChanged(TETHERING_WIFI_P2P,
+ final List<TetheredClient> connectedClients = notifyDhcpLeasesChanged(TETHERING_WIFI_P2P,
eventCallbacks, p2pLease);
- callback.expectTetheredClientChanged(p2pClients);
+ callback.expectTetheredClientChanged(connectedClients);
reset(mDhcpServer);
// Run wifi tethering.
@@ -2999,21 +2999,11 @@
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
any(), dhcpEventCbsCaptor.capture());
eventCallbacks = dhcpEventCbsCaptor.getValue();
- // Update mac address from softAp callback before getting dhcp lease.
final MacAddress testMac2 = MacAddress.fromString("22:22:22:22:22:22");
- final TetheredClient noAddrClient = notifyConnectedWifiClientsChanged(testMac2,
- false /* isLocalOnly */);
- final List<TetheredClient> p2pAndNoAddrClients = new ArrayList<>(p2pClients);
- p2pAndNoAddrClients.add(noAddrClient);
- callback.expectTetheredClientChanged(p2pAndNoAddrClients);
-
- // Update dhcp lease for wifi tethering.
final DhcpLeaseParcelable wifiLease = createDhcpLeaseParcelable("clientId2", testMac2,
"192.168.43.24", 24, Long.MAX_VALUE, "test2");
- final List<TetheredClient> p2pAndWifiClients = new ArrayList<>(p2pClients);
- p2pAndWifiClients.addAll(notifyDhcpLeasesChanged(TETHERING_WIFI,
- eventCallbacks, wifiLease));
- callback.expectTetheredClientChanged(p2pAndWifiClients);
+ verifyHotspotClientUpdate(false /* isLocalOnly */, testMac2, wifiLease, connectedClients,
+ eventCallbacks, callback);
// Test onStarted callback that register second callback when tethering is running.
TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
@@ -3021,7 +3011,7 @@
mTethering.registerTetheringEventCallback(callback2);
mLooper.dispatchAll();
});
- callback2.expectTetheredClientChanged(p2pAndWifiClients);
+ callback2.expectTetheredClientChanged(connectedClients);
}
@Test
@@ -3043,26 +3033,34 @@
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
any(), dhcpEventCbsCaptor.capture());
final IDhcpEventCallbacks eventCallbacks = dhcpEventCbsCaptor.getValue();
- // Update mac address from softAp callback before getting dhcp lease.
- final MacAddress testMac = MacAddress.fromString("22:22:22:22:22:22");
- final TetheredClient noAddrClient = notifyConnectedWifiClientsChanged(testMac,
- true /* isLocalOnly */);
- final List<TetheredClient> noAddrLocalOnlyClients = new ArrayList<>();
- noAddrLocalOnlyClients.add(noAddrClient);
- callback.expectTetheredClientChanged(noAddrLocalOnlyClients);
- // Update dhcp lease for local only hotspot.
+ final List<TetheredClient> connectedClients = new ArrayList<>();
+ final MacAddress testMac = MacAddress.fromString("22:22:22:22:22:22");
final DhcpLeaseParcelable wifiLease = createDhcpLeaseParcelable("clientId", testMac,
"192.168.43.24", 24, Long.MAX_VALUE, "test");
- final List<TetheredClient> localOnlyClients = notifyDhcpLeasesChanged(TETHERING_WIFI,
- eventCallbacks, wifiLease);
- callback.expectTetheredClientChanged(localOnlyClients);
+ verifyHotspotClientUpdate(true /* isLocalOnly */, testMac, wifiLease, connectedClients,
+ eventCallbacks, callback);
// Client disconnect from local only hotspot.
mLocalOnlyHotspotCallback.onConnectedClientsChanged(Collections.emptyList());
callback.expectTetheredClientChanged(Collections.emptyList());
}
+ private void verifyHotspotClientUpdate(final boolean isLocalOnly, final MacAddress testMac,
+ final DhcpLeaseParcelable dhcpLease, final List<TetheredClient> currentClients,
+ final IDhcpEventCallbacks dhcpCallback, final TestTetheringEventCallback callback)
+ throws Exception {
+ // Update mac address from softAp callback before getting dhcp lease.
+ final TetheredClient noAddrClient = notifyConnectedWifiClientsChanged(testMac, isLocalOnly);
+ final List<TetheredClient> withNoAddrClients = new ArrayList<>(currentClients);
+ withNoAddrClients.add(noAddrClient);
+ callback.expectTetheredClientChanged(withNoAddrClients);
+
+ // Update dhcp lease for hotspot.
+ currentClients.addAll(notifyDhcpLeasesChanged(TETHERING_WIFI, dhcpCallback, dhcpLease));
+ callback.expectTetheredClientChanged(currentClients);
+ }
+
private TetheredClient notifyConnectedWifiClientsChanged(final MacAddress mac,
boolean isLocalOnly) throws Exception {
final ArrayList<WifiClient> wifiClients = new ArrayList<>();
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 839ca40..245ad7a 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -412,10 +412,8 @@
// Always allow and never count clat traffic. Only the IPv4 traffic on the stacked
// interface is accounted for and subject to usage restrictions.
- // TODO: remove sock_uid check once Nat464Xlat javaland adds the socket tag AID_CLAT for clat.
- if (sock_uid == AID_CLAT || uid == AID_CLAT) {
- return PASS;
- }
+ // CLAT IPv6 TX sockets are *always* tagged with CLAT uid, see tagSocketAsClat()
+ if (uid == AID_CLAT) return PASS;
int match = bpf_owner_match(skb, sock_uid, egress, kver);
@@ -441,17 +439,17 @@
uint32_t mapSettingKey = CURRENT_STATS_MAP_CONFIGURATION_KEY;
uint32_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey);
- // Use asm("%0 &= 1" : "+r"(match)) before return match,
- // to help kernel's bpf verifier, so that it can be 100% certain
- // that the returned value is always BPF_NOMATCH(0) or BPF_MATCH(1).
- if (!selectedMap) {
- asm("%0 &= 1" : "+r"(match));
- return match;
- }
+ if (!selectedMap) return PASS; // cannot happen, needed to keep bpf verifier happy
do_packet_tracing(skb, egress, uid, tag, enable_tracing, kver);
update_stats_with_config(*selectedMap, skb, &key, egress, kver);
update_app_uid_stats_map(skb, &uid, egress, kver);
+
+ // We've already handled DROP_UNLESS_DNS up above, thus when we reach here the only
+ // possible values of match are DROP(0) or PASS(1), however we need to use
+ // "match &= 1" before 'return match' to help the kernel's bpf verifier,
+ // so that it can be 100% certain that the returned value is always 0 or 1.
+ // We use assembly so that it cannot be optimized out by a too smart compiler.
asm("%0 &= 1" : "+r"(match));
return match;
}
@@ -502,9 +500,8 @@
// Clat daemon does not generate new traffic, all its traffic is accounted for already
// on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead,
// but that can be corrected for later when merging v4-foo stats into interface foo's).
- // TODO: remove sock_uid check once Nat464Xlat javaland adds the socket tag AID_CLAT for clat.
+ // CLAT sockets are created by system server and tagged as uid CLAT, see tagSocketAsClat()
uint32_t sock_uid = bpf_get_socket_uid(skb);
- if (sock_uid == AID_CLAT) return BPF_NOMATCH;
if (sock_uid == AID_SYSTEM) {
uint64_t cookie = bpf_get_socket_cookie(skb);
UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
diff --git a/framework/Android.bp b/framework/Android.bp
index d7eaf9b..123f02a 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -107,8 +107,11 @@
name: "framework-connectivity-pre-jarjar",
defaults: [
"framework-connectivity-defaults",
- "CronetJavaPrejarjarDefaults",
- ],
+ ],
+ static_libs: [
+ "cronet_aml_api_java",
+ "cronet_aml_java",
+ ],
libs: [
// This cannot be in the defaults clause above because if it were, it would be used
// to generate the connectivity stubs. That would create a circular dependency
@@ -120,6 +123,17 @@
visibility: ["//packages/modules/Connectivity:__subpackages__"]
}
+java_defaults {
+ name: "CronetJavaDefaults",
+ srcs: [":cronet_aml_api_sources"],
+ libs: [
+ "androidx.annotation_annotation",
+ ],
+ impl_only_static_libs: [
+ "cronet_aml_java",
+ ],
+}
+
java_sdk_library {
name: "framework-connectivity",
defaults: [
diff --git a/framework/src/android/net/ConnectivitySettingsManager.java b/framework/src/android/net/ConnectivitySettingsManager.java
index 822e67d..67dacb8 100644
--- a/framework/src/android/net/ConnectivitySettingsManager.java
+++ b/framework/src/android/net/ConnectivitySettingsManager.java
@@ -28,6 +28,7 @@
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.net.ConnectivityManager.MultipathPreference;
import android.os.Binder;
import android.os.Build;
@@ -36,6 +37,7 @@
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArraySet;
+import android.util.Log;
import android.util.Range;
import com.android.net.module.util.ConnectivitySettingsUtils;
@@ -55,6 +57,7 @@
*/
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public class ConnectivitySettingsManager {
+ private static final String TAG = ConnectivitySettingsManager.class.getSimpleName();
private ConnectivitySettingsManager() {}
@@ -696,10 +699,20 @@
/**
* Set global http proxy settings from given {@link ProxyInfo}.
*
+ * <p class="note">
+ * While a {@link ProxyInfo} for a PAC proxy can be specified, not all devices support
+ * PAC proxies. In particular, smaller devices like watches often do not have the capabilities
+ * necessary to interpret the PAC file. In such cases, calling this API with a PAC proxy
+ * results in undefined behavior, including possibly breaking networking for applications.
+ * You can test for this by checking for the presence of {@link PackageManager.FEATURE_WEBVIEW}.
+ * </p>
+ *
* @param context The {@link Context} to set the setting.
* @param proxyInfo The {@link ProxyInfo} for global http proxy settings which build from
* {@link ProxyInfo#buildPacProxy(Uri)} or
* {@link ProxyInfo#buildDirectProxy(String, int, List)}
+ * @throws UnsupportedOperationException if |proxyInfo| codes for a PAC proxy but the system
+ * does not support PAC proxies.
*/
public static void setGlobalProxy(@NonNull Context context, @NonNull ProxyInfo proxyInfo) {
final String host = proxyInfo.getHost();
@@ -707,6 +720,14 @@
final String exclusionList = proxyInfo.getExclusionListAsString();
final String pacFileUrl = proxyInfo.getPacFileUrl().toString();
+
+ if (!TextUtils.isEmpty(pacFileUrl)) {
+ final PackageManager pm = context.getPackageManager();
+ if (null != pm && !pm.hasSystemFeature(PackageManager.FEATURE_WEBVIEW)) {
+ Log.wtf(TAG, "PAC proxy can't be installed on a device without FEATURE_WEBVIEW");
+ }
+ }
+
if (TextUtils.isEmpty(pacFileUrl)) {
Settings.Global.putString(context.getContentResolver(), GLOBAL_HTTP_PROXY_HOST, host);
Settings.Global.putInt(context.getContentResolver(), GLOBAL_HTTP_PROXY_PORT, port);
diff --git a/framework/src/android/net/LinkProperties.java b/framework/src/android/net/LinkProperties.java
index e0926e9..9fb9bc6 100644
--- a/framework/src/android/net/LinkProperties.java
+++ b/framework/src/android/net/LinkProperties.java
@@ -1456,6 +1456,10 @@
* @hide
*/
public boolean isIdenticalPcscfs(@NonNull LinkProperties target) {
+ // Per 3GPP TS 24.229, B.2.2.1 PDP context activation and P-CSCF discovery
+ // list order is important, so on U+ compare one by one
+ if (SdkLevel.isAtLeastU()) return target.getPcscfServers().equals(mPcscfs);
+ // but for safety old behaviour on pre-U:
Collection<InetAddress> targetPcscfs = target.getPcscfServers();
return (mPcscfs.size() == targetPcscfs.size()) ?
mPcscfs.containsAll(targetPcscfs) : false;
diff --git a/framework/src/android/net/NetworkInfo.java b/framework/src/android/net/NetworkInfo.java
index b7ec519..7aa9847 100644
--- a/framework/src/android/net/NetworkInfo.java
+++ b/framework/src/android/net/NetworkInfo.java
@@ -334,6 +334,24 @@
}
/**
+ * Indicates whether this network is suspended.
+ * @deprecated Apps should instead use the
+ * {@link android.net.ConnectivityManager.NetworkCallback} API to
+ * learn about connectivity changes. See
+ * {@link ConnectivityManager#registerDefaultNetworkCallback} and
+ * {@link ConnectivityManager#registerNetworkCallback}. These will
+ * give a more accurate picture of the connectivity state of
+ * the device and let apps react more easily and quickly to changes.
+ * @hide
+ */
+ @Deprecated
+ public boolean isSuspended() {
+ synchronized (this) {
+ return mState == State.SUSPENDED;
+ }
+ }
+
+ /**
* Indicates whether network connectivity is possible. A network is unavailable
* when a persistent or semi-persistent condition prevents the possibility
* of connecting to that network. Examples include
diff --git a/framework/src/android/net/SocketKeepalive.java b/framework/src/android/net/SocketKeepalive.java
index 10daf17..f915e72 100644
--- a/framework/src/android/net/SocketKeepalive.java
+++ b/framework/src/android/net/SocketKeepalive.java
@@ -149,9 +149,7 @@
public static final int ERROR_INSUFFICIENT_RESOURCES = -32;
/**
- * There was no such slot. This should only be internally as it indicates
- * a programming error in the system server. It should not propagate to
- * applications.
+ * There was no such slot, or no keepalive running on this slot.
* @hide
*/
@SystemApi
diff --git a/netd/BpfBaseTest.cpp b/netd/BpfBaseTest.cpp
index 624d216..c979a7b 100644
--- a/netd/BpfBaseTest.cpp
+++ b/netd/BpfBaseTest.cpp
@@ -93,7 +93,7 @@
ASSERT_EQ(TEST_TAG, tagResult.value().tag);
ASSERT_EQ(0, close(sock));
// Check map periodically until sk destroy handler have done its job.
- for (int i = 0; i < 10; i++) {
+ for (int i = 0; i < 1000; i++) {
usleep(5000); // 5ms
tagResult = cookieTagMap.readValue(cookie);
if (!tagResult.ok()) {
@@ -101,7 +101,7 @@
return;
}
}
- FAIL() << "socket tag still exist after 50ms";
+ FAIL() << "socket tag still exist after 5s";
}
}
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index f40d388..0dfd0af 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -30,6 +30,7 @@
],
shared_libs: [
"libbase",
+ "libcutils",
"liblog",
],
static_libs: [
@@ -81,6 +82,7 @@
shared_libs: [
"libbase",
"liblog",
+ "libcutils",
"libandroid_net",
],
compile_multilib: "both",
diff --git a/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
index 6aa0fb4..c5f9631 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
@@ -149,6 +149,18 @@
if (mIsTest) return; // Don't touch non-hermetic bpf in test.
if (mStarted) sPoller.Stop();
mStarted = false;
+
+ // Although this shouldn't be required, there seems to be some cases when we
+ // don't fill enough of a Perfetto Chunk for Perfetto to automatically commit
+ // the traced data. This manually flushes OnStop so we commit at least once.
+ NetworkTraceHandler::Trace([&](NetworkTraceHandler::TraceContext ctx) {
+ perfetto::LockedHandle<NetworkTraceHandler> handle =
+ ctx.GetDataSourceLocked();
+ // Trace is called for all active handlers, only flush our context. Since
+ // handle doesn't have a `.get()`, use `*` and `&` to get what it points to.
+ if (&(*handle) != this) return;
+ ctx.Flush();
+ });
}
void NetworkTraceHandler::Write(const std::vector<PacketTrace>& packets,
diff --git a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
index 3de9897..d538368 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
@@ -15,10 +15,12 @@
*/
#define LOG_TAG "NetworkTrace"
+#define ATRACE_TAG ATRACE_TAG_NETWORK
#include "netdbpf/NetworkTracePoller.h"
#include <bpf/BpfUtils.h>
+#include <cutils/trace.h>
#include <log/log.h>
#include <perfetto/tracing/platform.h>
#include <perfetto/tracing/tracing.h>
@@ -133,6 +135,8 @@
return false;
}
+ ATRACE_INT("NetworkTracePackets", packets.size());
+
mCallback(packets);
return true;
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 1e0d544..29a03d1 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -19,6 +19,7 @@
import static android.net.ConnectivityManager.NETID_UNSET;
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.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -54,6 +55,7 @@
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
@@ -83,6 +85,7 @@
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -242,14 +245,14 @@
public void onServiceNameDiscovered(@NonNull MdnsServiceInfo serviceInfo) {
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
NsdManager.SERVICE_FOUND,
- new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo));
+ new MdnsEvent(mClientId, serviceInfo));
}
@Override
public void onServiceNameRemoved(@NonNull MdnsServiceInfo serviceInfo) {
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
NsdManager.SERVICE_LOST,
- new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo));
+ new MdnsEvent(mClientId, serviceInfo));
}
}
@@ -264,7 +267,7 @@
public void onServiceFound(MdnsServiceInfo serviceInfo) {
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
NsdManager.RESOLVE_SERVICE_SUCCEEDED,
- new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo));
+ new MdnsEvent(mClientId, serviceInfo));
}
}
@@ -279,21 +282,21 @@
public void onServiceFound(@NonNull MdnsServiceInfo serviceInfo) {
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
NsdManager.SERVICE_UPDATED,
- new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo));
+ new MdnsEvent(mClientId, serviceInfo));
}
@Override
public void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo) {
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
NsdManager.SERVICE_UPDATED,
- new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo));
+ new MdnsEvent(mClientId, serviceInfo));
}
@Override
public void onServiceRemoved(@NonNull MdnsServiceInfo serviceInfo) {
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
NsdManager.SERVICE_UPDATED_LOST,
- new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo));
+ new MdnsEvent(mClientId, serviceInfo));
}
}
@@ -303,14 +306,10 @@
private static class MdnsEvent {
final int mClientId;
@NonNull
- final String mRequestedServiceType;
- @NonNull
final MdnsServiceInfo mMdnsServiceInfo;
- MdnsEvent(int clientId, @NonNull String requestedServiceType,
- @NonNull MdnsServiceInfo mdnsServiceInfo) {
+ MdnsEvent(int clientId, @NonNull MdnsServiceInfo mdnsServiceInfo) {
mClientId = clientId;
- mRequestedServiceType = requestedServiceType;
mMdnsServiceInfo = mdnsServiceInfo;
}
}
@@ -601,7 +600,10 @@
final NsdServiceInfo info = args.serviceInfo;
id = getUniqueId();
- final String serviceType = constructServiceType(info.getServiceType());
+ final Pair<String, String> typeAndSubtype =
+ parseTypeAndSubtype(info.getServiceType());
+ final String serviceType = typeAndSubtype == null
+ ? null : typeAndSubtype.first;
if (clientInfo.mUseJavaBackend
|| mDeps.isMdnsDiscoveryManagerEnabled(mContext)
|| useDiscoveryManagerForType(serviceType)) {
@@ -615,12 +617,17 @@
maybeStartMonitoringSockets();
final MdnsListener listener =
new DiscoveryListener(clientId, id, info, listenServiceType);
- final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
- .setNetwork(info.getNetwork())
- .setIsPassiveMode(true)
- .build();
+ final MdnsSearchOptions.Builder optionsBuilder =
+ MdnsSearchOptions.newBuilder()
+ .setNetwork(info.getNetwork())
+ .setIsPassiveMode(true);
+ if (typeAndSubtype.second != null) {
+ // The parsing ensures subtype starts with an underscore.
+ // MdnsSearchOptions expects the underscore to not be present.
+ optionsBuilder.addSubtype(typeAndSubtype.second.substring(1));
+ }
mMdnsDiscoveryManager.registerListener(
- listenServiceType, listener, options);
+ listenServiceType, listener, optionsBuilder.build());
storeDiscoveryManagerRequestMap(clientId, id, listener, clientInfo);
clientInfo.onDiscoverServicesStarted(clientId, info);
clientInfo.log("Register a DiscoveryListener " + id
@@ -699,7 +706,9 @@
id = getUniqueId();
final NsdServiceInfo serviceInfo = args.serviceInfo;
final String serviceType = serviceInfo.getServiceType();
- final String registerServiceType = constructServiceType(serviceType);
+ final Pair<String, String> typeSubtype = parseTypeAndSubtype(serviceType);
+ final String registerServiceType = typeSubtype == null
+ ? null : typeSubtype.first;
if (clientInfo.mUseJavaBackend
|| mDeps.isMdnsAdvertiserEnabled(mContext)
|| useAdvertiserForType(registerServiceType)) {
@@ -714,7 +723,11 @@
serviceInfo.getServiceName()));
maybeStartMonitoringSockets();
- mAdvertiser.addService(id, serviceInfo);
+ // TODO: pass in the subtype as well. Including the subtype in the
+ // service type would generate service instance names like
+ // Name._subtype._sub._type._tcp, which is incorrect
+ // (it should be Name._type._tcp).
+ mAdvertiser.addService(id, serviceInfo, typeSubtype.second);
storeAdvertiserRequestMap(clientId, id, clientInfo);
} else {
maybeStartDaemon();
@@ -780,7 +793,10 @@
final NsdServiceInfo info = args.serviceInfo;
id = getUniqueId();
- final String serviceType = constructServiceType(info.getServiceType());
+ final Pair<String, String> typeSubtype =
+ parseTypeAndSubtype(info.getServiceType());
+ final String serviceType = typeSubtype == null
+ ? null : typeSubtype.first;
if (clientInfo.mUseJavaBackend
|| mDeps.isMdnsDiscoveryManagerEnabled(mContext)
|| useDiscoveryManagerForType(serviceType)) {
@@ -873,7 +889,10 @@
final NsdServiceInfo info = args.serviceInfo;
id = getUniqueId();
- final String serviceType = constructServiceType(info.getServiceType());
+ final Pair<String, String> typeAndSubtype =
+ parseTypeAndSubtype(info.getServiceType());
+ final String serviceType = typeAndSubtype == null
+ ? null : typeAndSubtype.first;
if (serviceType == null) {
clientInfo.onServiceInfoCallbackRegistrationFailed(clientId,
NsdManager.FAILURE_BAD_PARAMETERS);
@@ -1104,9 +1123,38 @@
return true;
}
- private NsdServiceInfo buildNsdServiceInfoFromMdnsEvent(final MdnsEvent event) {
+ @Nullable
+ private NsdServiceInfo buildNsdServiceInfoFromMdnsEvent(
+ final MdnsEvent event, int code) {
final MdnsServiceInfo serviceInfo = event.mMdnsServiceInfo;
- final String serviceType = event.mRequestedServiceType;
+ final String[] typeArray = serviceInfo.getServiceType();
+ final String joinedType;
+ if (typeArray.length == 0
+ || !typeArray[typeArray.length - 1].equals(LOCAL_DOMAIN_NAME)) {
+ Log.wtf(TAG, "MdnsServiceInfo type does not end in .local: "
+ + Arrays.toString(typeArray));
+ return null;
+ } else {
+ joinedType = TextUtils.join(".",
+ Arrays.copyOfRange(typeArray, 0, typeArray.length - 1));
+ }
+ final String serviceType;
+ switch (code) {
+ case NsdManager.SERVICE_FOUND:
+ case NsdManager.SERVICE_LOST:
+ // For consistency with historical behavior, discovered service types have
+ // a dot at the end.
+ serviceType = joinedType + ".";
+ break;
+ case RESOLVE_SERVICE_SUCCEEDED:
+ // For consistency with historical behavior, resolved service types have
+ // a dot at the beginning.
+ serviceType = "." + joinedType;
+ break;
+ default:
+ serviceType = joinedType;
+ break;
+ }
final String serviceName = serviceInfo.getServiceInstanceName();
final NsdServiceInfo servInfo = new NsdServiceInfo(serviceName, serviceType);
final Network network = serviceInfo.getNetwork();
@@ -1131,7 +1179,9 @@
final MdnsEvent event = (MdnsEvent) obj;
final int clientId = event.mClientId;
- final NsdServiceInfo info = buildNsdServiceInfoFromMdnsEvent(event);
+ final NsdServiceInfo info = buildNsdServiceInfoFromMdnsEvent(event, code);
+ // Errors are already logged if null
+ if (info == null) return false;
if (DBG) {
Log.d(TAG, String.format("MdnsDiscoveryManager event code=%s transactionId=%d",
NsdManager.nameOf(code), transactionId));
@@ -1150,8 +1200,6 @@
break;
}
final MdnsServiceInfo serviceInfo = event.mMdnsServiceInfo;
- // Add '.' in front of the service type that aligns with historical behavior
- info.setServiceType("." + event.mRequestedServiceType);
info.setPort(serviceInfo.getPort());
Map<String, String> attrs = serviceInfo.getAttributes();
@@ -1288,28 +1336,39 @@
* Check the given service type is valid and construct it to a service type
* which can use for discovery / resolution service.
*
- * <p> The valid service type should be 2 labels, or 3 labels if the query is for a
+ * <p>The valid service type should be 2 labels, or 3 labels if the query is for a
* subtype (see RFC6763 7.1). Each label is up to 63 characters and must start with an
* 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.
+ *
* @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 String constructServiceType(String serviceType) {
+ public static Pair<String, String> parseTypeAndSubtype(String serviceType) {
if (TextUtils.isEmpty(serviceType)) return null;
+ final String typeOrSubtypePattern = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
final Pattern serviceTypePattern = Pattern.compile(
- "^(_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]\\.)?"
- + "(_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]\\._(?:tcp|udp))"
+ // 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 Matcher matcher = serviceTypePattern.matcher(serviceType);
if (!matcher.matches()) return null;
- return matcher.group(1) == null
- ? matcher.group(2)
- : matcher.group(1) + "_sub." + matcher.group(2);
+ // 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);
}
@VisibleForTesting
@@ -1335,7 +1394,8 @@
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider);
mMdnsDiscoveryManager = deps.makeMdnsDiscoveryManager(new ExecutorProvider(),
- mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"));
+ mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"),
+ handler.getLooper());
handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider,
new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"));
@@ -1393,8 +1453,9 @@
*/
public MdnsDiscoveryManager makeMdnsDiscoveryManager(
@NonNull ExecutorProvider executorProvider,
- @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog) {
- return new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog);
+ @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog,
+ @NonNull Looper looper) {
+ return new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog, looper);
}
/**
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 655c364..5f27b6a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -253,8 +253,9 @@
int getConflictingService(@NonNull NsdServiceInfo info) {
for (int i = 0; i < mPendingRegistrations.size(); i++) {
final NsdServiceInfo other = mPendingRegistrations.valueAt(i).getServiceInfo();
- if (info.getServiceName().equals(other.getServiceName())
- && info.getServiceType().equals(other.getServiceType())) {
+ if (MdnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
+ && MdnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
+ other.getServiceType())) {
return mPendingRegistrations.keyAt(i);
}
}
@@ -270,7 +271,8 @@
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
try {
- mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo());
+ mAdvertisers.valueAt(i).addService(
+ id, registration.getServiceInfo(), registration.getSubtype());
} catch (NameConflictException e) {
Log.wtf(TAG, "Name conflict adding services that should have unique names", e);
}
@@ -298,9 +300,10 @@
}
mAdvertisers.put(socket, advertiser);
for (int i = 0; i < mPendingRegistrations.size(); i++) {
+ final Registration registration = mPendingRegistrations.valueAt(i);
try {
advertiser.addService(mPendingRegistrations.keyAt(i),
- mPendingRegistrations.valueAt(i).getServiceInfo());
+ registration.getServiceInfo(), registration.getSubtype());
} catch (NameConflictException e) {
Log.wtf(TAG, "Name conflict adding services that should have unique names", e);
}
@@ -329,10 +332,13 @@
private int mConflictCount;
@NonNull
private NsdServiceInfo mServiceInfo;
+ @Nullable
+ private final String mSubtype;
- private Registration(@NonNull NsdServiceInfo serviceInfo) {
+ private Registration(@NonNull NsdServiceInfo serviceInfo, @Nullable String subtype) {
this.mOriginalName = serviceInfo.getServiceName();
this.mServiceInfo = serviceInfo;
+ this.mSubtype = subtype;
}
/**
@@ -387,6 +393,11 @@
public NsdServiceInfo getServiceInfo() {
return mServiceInfo;
}
+
+ @Nullable
+ public String getSubtype() {
+ return mSubtype;
+ }
}
/**
@@ -443,8 +454,9 @@
* Add a service to advertise.
* @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.
*/
- public void addService(int id, NsdServiceInfo service) {
+ public void addService(int id, NsdServiceInfo service, @Nullable String subtype) {
checkThread();
if (mRegistrations.get(id) != null) {
Log.e(TAG, "Adding duplicate registration for " + service);
@@ -453,10 +465,10 @@
return;
}
- mSharedLog.i("Adding service " + service + " with ID " + id);
+ mSharedLog.i("Adding service " + service + " with ID " + id + " and subtype " + subtype);
final Network network = service.getNetwork();
- final Registration registration = new Registration(service);
+ final Registration registration = new Registration(service, subtype);
final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
if (network == null) {
// If registering on all networks, no advertiser must have conflicts
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 6455044..0154827 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,20 +16,24 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
import static com.android.server.connectivity.mdns.util.MdnsUtils.isNetworkMatched;
+import static com.android.server.connectivity.mdns.util.MdnsUtils.isRunningOnHandlerThread;
import android.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.net.Network;
+import android.os.Handler;
+import android.os.Looper;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
-import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.util.ArrayList;
@@ -47,8 +51,8 @@
private final MdnsSocketClientBase socketClient;
@NonNull private final SharedLog sharedLog;
- @GuardedBy("this")
@NonNull private final PerNetworkServiceTypeClients perNetworkServiceTypeClients;
+ @NonNull private final Handler handler;
private static class PerNetworkServiceTypeClients {
private final ArrayMap<Pair<String, Network>, MdnsServiceTypeClient> clients =
@@ -56,21 +60,26 @@
public void put(@NonNull String serviceType, @Nullable Network network,
@NonNull MdnsServiceTypeClient client) {
- final Pair<String, Network> perNetworkServiceType = new Pair<>(serviceType, network);
+ final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
+ final Pair<String, Network> perNetworkServiceType = new Pair<>(dnsLowerServiceType,
+ network);
clients.put(perNetworkServiceType, client);
}
@Nullable
public MdnsServiceTypeClient get(@NonNull String serviceType, @Nullable Network network) {
- final Pair<String, Network> perNetworkServiceType = new Pair<>(serviceType, network);
+ final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
+ final Pair<String, Network> perNetworkServiceType = new Pair<>(dnsLowerServiceType,
+ network);
return clients.getOrDefault(perNetworkServiceType, null);
}
public List<MdnsServiceTypeClient> getByServiceType(@NonNull String serviceType) {
+ final String dnsLowerServiceType = MdnsUtils.toDnsLowerCase(serviceType);
final List<MdnsServiceTypeClient> list = new ArrayList<>();
for (int i = 0; i < clients.size(); i++) {
final Pair<String, Network> perNetworkServiceType = clients.keyAt(i);
- if (serviceType.equals(perNetworkServiceType.first)) {
+ if (dnsLowerServiceType.equals(perNetworkServiceType.first)) {
list.add(clients.valueAt(i));
}
}
@@ -103,11 +112,21 @@
}
public MdnsDiscoveryManager(@NonNull ExecutorProvider executorProvider,
- @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog) {
+ @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog,
+ @NonNull Looper looper) {
this.executorProvider = executorProvider;
this.socketClient = socketClient;
this.sharedLog = sharedLog;
perNetworkServiceTypeClients = new PerNetworkServiceTypeClients();
+ handler = new Handler(looper);
+ }
+
+ private void checkAndRunOnHandlerThread(@NonNull Runnable function) {
+ if (isRunningOnHandlerThread(handler)) {
+ function.run();
+ } else {
+ handler.post(function);
+ }
}
/**
@@ -120,11 +139,19 @@
* serviceType}.
*/
@RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
- public synchronized void registerListener(
+ public void registerListener(
@NonNull String serviceType,
@NonNull MdnsServiceBrowserListener listener,
@NonNull MdnsSearchOptions searchOptions) {
sharedLog.i("Registering listener for serviceType: " + serviceType);
+ checkAndRunOnHandlerThread(() ->
+ handleRegisterListener(serviceType, listener, searchOptions));
+ }
+
+ private void handleRegisterListener(
+ @NonNull String serviceType,
+ @NonNull MdnsServiceBrowserListener listener,
+ @NonNull MdnsSearchOptions searchOptions) {
if (perNetworkServiceTypeClients.isEmpty()) {
// First listener. Starts the socket client.
try {
@@ -139,30 +166,28 @@
new MdnsSocketClientBase.SocketCreationCallback() {
@Override
public void onSocketCreated(@Nullable Network network) {
- synchronized (MdnsDiscoveryManager.this) {
- // All listeners of the same service types shares the same
- // MdnsServiceTypeClient.
- MdnsServiceTypeClient serviceTypeClient =
- perNetworkServiceTypeClients.get(serviceType, network);
- if (serviceTypeClient == null) {
- serviceTypeClient = createServiceTypeClient(serviceType, network);
- perNetworkServiceTypeClients.put(serviceType, network,
- serviceTypeClient);
- }
- serviceTypeClient.startSendAndReceive(listener, searchOptions);
+ ensureRunningOnHandlerThread(handler);
+ // All listeners of the same service types shares the same
+ // MdnsServiceTypeClient.
+ MdnsServiceTypeClient serviceTypeClient =
+ perNetworkServiceTypeClients.get(serviceType, network);
+ if (serviceTypeClient == null) {
+ serviceTypeClient = createServiceTypeClient(serviceType, network);
+ perNetworkServiceTypeClients.put(serviceType, network,
+ serviceTypeClient);
}
+ serviceTypeClient.startSendAndReceive(listener, searchOptions);
}
@Override
- public void onSocketDestroyed(@Nullable Network network) {
- synchronized (MdnsDiscoveryManager.this) {
- final MdnsServiceTypeClient serviceTypeClient =
- perNetworkServiceTypeClients.get(serviceType, network);
- if (serviceTypeClient == null) return;
- // Notify all listeners that all services are removed from this socket.
- serviceTypeClient.notifyAllServicesRemoved();
- perNetworkServiceTypeClients.remove(serviceTypeClient);
- }
+ public void onAllSocketsDestroyed(@Nullable Network network) {
+ ensureRunningOnHandlerThread(handler);
+ final MdnsServiceTypeClient serviceTypeClient =
+ perNetworkServiceTypeClients.get(serviceType, network);
+ if (serviceTypeClient == null) return;
+ // Notify all listeners that all services are removed from this socket.
+ serviceTypeClient.notifySocketDestroyed();
+ perNetworkServiceTypeClients.remove(serviceTypeClient);
}
});
}
@@ -175,9 +200,14 @@
* @param listener The {@link MdnsServiceBrowserListener} listener.
*/
@RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
- public synchronized void unregisterListener(
+ public void unregisterListener(
@NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
sharedLog.i("Unregistering listener for serviceType:" + serviceType);
+ checkAndRunOnHandlerThread(() -> handleUnregisterListener(serviceType, listener));
+ }
+
+ private void handleUnregisterListener(
+ @NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
final List<MdnsServiceTypeClient> serviceTypeClients =
perNetworkServiceTypeClients.getByServiceType(serviceType);
if (serviceTypeClients.isEmpty()) {
@@ -200,8 +230,14 @@
}
@Override
- public synchronized void onResponseReceived(@NonNull MdnsPacket packet,
- int interfaceIndex, Network network) {
+ public void onResponseReceived(@NonNull MdnsPacket packet,
+ int interfaceIndex, @Nullable Network network) {
+ checkAndRunOnHandlerThread(() ->
+ handleOnResponseReceived(packet, interfaceIndex, network));
+ }
+
+ private void handleOnResponseReceived(@NonNull MdnsPacket packet, int interfaceIndex,
+ @Nullable Network network) {
for (MdnsServiceTypeClient serviceTypeClient
: perNetworkServiceTypeClients.getByMatchingNetwork(network)) {
serviceTypeClient.processResponse(packet, interfaceIndex, network);
@@ -209,8 +245,14 @@
}
@Override
- public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
- Network network) {
+ public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
+ @Nullable Network network) {
+ checkAndRunOnHandlerThread(() ->
+ handleOnFailedToParseMdnsResponse(receivedPacketNumber, errorCode, network));
+ }
+
+ private void handleOnFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
+ @Nullable Network network) {
for (MdnsServiceTypeClient serviceTypeClient
: perNetworkServiceTypeClients.getByMatchingNetwork(network)) {
serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
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 4e09515..724a704 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -212,8 +212,9 @@
*
* @throws NameConflictException There is already a service being advertised with that name.
*/
- public void addService(int id, NsdServiceInfo service) throws NameConflictException {
- final int replacedExitingService = mRecordRepository.addService(id, service);
+ public void addService(int id, NsdServiceInfo service, @Nullable String subtype)
+ throws NameConflictException {
+ final int replacedExitingService = mRecordRepository.addService(id, service, subtype);
// 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/MdnsMultinetworkSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
index 6414453..52c8622 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -34,7 +34,6 @@
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.util.List;
-import java.util.Map;
/**
* The {@link MdnsMultinetworkSocketClient} manages the multinetwork socket for mDns
@@ -48,9 +47,8 @@
@NonNull private final Handler mHandler;
@NonNull private final MdnsSocketProvider mSocketProvider;
- private final Map<MdnsServiceBrowserListener, InterfaceSocketCallback> mRequestedNetworks =
+ private final ArrayMap<MdnsServiceBrowserListener, InterfaceSocketCallback> mRequestedNetworks =
new ArrayMap<>();
- private final ArrayMap<MdnsInterfaceSocket, Network> mActiveNetworkSockets = new ArrayMap<>();
private final ArrayMap<MdnsInterfaceSocket, ReadPacketHandler> mSocketPacketHandlers =
new ArrayMap<>();
private MdnsSocketClientBase.Callback mCallback = null;
@@ -63,7 +61,11 @@
}
private class InterfaceSocketCallback implements MdnsSocketProvider.SocketCallback {
+ @NonNull
private final SocketCreationCallback mSocketCreationCallback;
+ @NonNull
+ private final ArrayMap<MdnsInterfaceSocket, Network> mActiveNetworkSockets =
+ new ArrayMap<>();
InterfaceSocketCallback(SocketCreationCallback socketCreationCallback) {
mSocketCreationCallback = socketCreationCallback;
@@ -88,10 +90,59 @@
@Override
public void onInterfaceDestroyed(@Nullable Network network,
@NonNull MdnsInterfaceSocket socket) {
- mSocketPacketHandlers.remove(socket);
- mActiveNetworkSockets.remove(socket);
- mSocketCreationCallback.onSocketDestroyed(network);
+ notifySocketDestroyed(socket);
+ maybeCleanupPacketHandler(socket);
}
+
+ private void notifySocketDestroyed(@NonNull MdnsInterfaceSocket socket) {
+ final Network network = mActiveNetworkSockets.remove(socket);
+ if (!isAnySocketActive(network)) {
+ mSocketCreationCallback.onAllSocketsDestroyed(network);
+ }
+ }
+
+ void onNetworkUnrequested() {
+ for (int i = mActiveNetworkSockets.size() - 1; i >= 0; i--) {
+ // Iterate from the end so the socket can be removed
+ final MdnsInterfaceSocket socket = mActiveNetworkSockets.keyAt(i);
+ notifySocketDestroyed(socket);
+ maybeCleanupPacketHandler(socket);
+ }
+ }
+ }
+
+ private boolean isSocketActive(@NonNull MdnsInterfaceSocket socket) {
+ for (int i = 0; i < mRequestedNetworks.size(); i++) {
+ final InterfaceSocketCallback isc = mRequestedNetworks.valueAt(i);
+ if (isc.mActiveNetworkSockets.containsKey(socket)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isAnySocketActive(@Nullable Network network) {
+ for (int i = 0; i < mRequestedNetworks.size(); i++) {
+ final InterfaceSocketCallback isc = mRequestedNetworks.valueAt(i);
+ if (isc.mActiveNetworkSockets.containsValue(network)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private ArrayMap<MdnsInterfaceSocket, Network> getActiveSockets() {
+ final ArrayMap<MdnsInterfaceSocket, Network> sockets = new ArrayMap<>();
+ for (int i = 0; i < mRequestedNetworks.size(); i++) {
+ final InterfaceSocketCallback isc = mRequestedNetworks.valueAt(i);
+ sockets.putAll(isc.mActiveNetworkSockets);
+ }
+ return sockets;
+ }
+
+ private void maybeCleanupPacketHandler(@NonNull MdnsInterfaceSocket socket) {
+ if (isSocketActive(socket)) return;
+ mSocketPacketHandlers.remove(socket);
}
private class ReadPacketHandler implements MulticastPacketReader.PacketHandler {
@@ -143,11 +194,14 @@
@Override
public void notifyNetworkUnrequested(@NonNull MdnsServiceBrowserListener listener) {
ensureRunningOnHandlerThread(mHandler);
- final InterfaceSocketCallback callback = mRequestedNetworks.remove(listener);
+ final InterfaceSocketCallback callback = mRequestedNetworks.get(listener);
if (callback == null) {
Log.e(TAG, "Can not be unrequested with unknown listener=" + listener);
return;
}
+ callback.onNetworkUnrequested();
+ // onNetworkUnrequested does cleanups based on mRequestedNetworks, only remove afterwards
+ mRequestedNetworks.remove(listener);
mSocketProvider.unrequestSocket(callback);
}
@@ -156,9 +210,10 @@
instanceof Inet6Address;
final boolean isIpv4 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
instanceof Inet4Address;
- for (int i = 0; i < mActiveNetworkSockets.size(); i++) {
- final MdnsInterfaceSocket socket = mActiveNetworkSockets.keyAt(i);
- final Network network = mActiveNetworkSockets.valueAt(i);
+ final ArrayMap<MdnsInterfaceSocket, Network> activeSockets = getActiveSockets();
+ for (int i = 0; i < activeSockets.size(); i++) {
+ final MdnsInterfaceSocket socket = activeSockets.keyAt(i);
+ final Network network = activeSockets.valueAt(i);
// Check ip capability and network before sending packet
if (((isIpv6 && socket.hasJoinedIpv6()) || (isIpv4 && socket.hasJoinedIpv4()))
&& isNetworkMatched(targetNetwork, network)) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
index 6ec2f99..1239180 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsNsecRecord.java
@@ -20,6 +20,7 @@
import android.text.TextUtils;
import com.android.net.module.util.CollectionUtils;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.util.ArrayList;
@@ -152,7 +153,8 @@
@Override
public int hashCode() {
return Objects.hash(super.hashCode(),
- Arrays.hashCode(mNextDomain), Arrays.hashCode(mTypes));
+ Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(mNextDomain)),
+ Arrays.hashCode(mTypes));
}
@Override
@@ -165,7 +167,8 @@
}
return super.equals(other)
- && Arrays.equals(mNextDomain, ((MdnsNsecRecord) other).mNextDomain)
+ && MdnsUtils.equalsDnsLabelIgnoreDnsCase(mNextDomain,
+ ((MdnsNsecRecord) other).mNextDomain)
&& Arrays.equals(mTypes, ((MdnsNsecRecord) other).mTypes);
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
index c0f9b8b..6879a64 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -17,11 +17,11 @@
package com.android.server.connectivity.mdns;
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.SocketAddress;
-import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@@ -180,7 +180,7 @@
int existingOffset = entry.getKey();
String[] existingLabels = entry.getValue();
- if (Arrays.equals(existingLabels, labels)) {
+ if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(existingLabels, labels)) {
writePointer(existingOffset);
return;
} else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
index 2c7b26b..c88ead0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -19,6 +19,7 @@
import android.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.util.Arrays;
@@ -60,7 +61,8 @@
}
public boolean hasSubtype() {
- return (name != null) && (name.length > 2) && name[1].equals(MdnsConstants.SUBTYPE_LABEL);
+ return (name != null) && (name.length > 2) && MdnsUtils.equalsIgnoreDnsCase(name[1],
+ MdnsConstants.SUBTYPE_LABEL);
}
public String getSubtype() {
@@ -74,7 +76,7 @@
@Override
public int hashCode() {
- return (super.hashCode() * 31) + Arrays.hashCode(pointer);
+ return (super.hashCode() * 31) + Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(pointer));
}
@Override
@@ -86,6 +88,7 @@
return false;
}
- return super.equals(other) && Arrays.equals(pointer, ((MdnsPointerRecord) other).pointer);
+ return super.equals(other) && MdnsUtils.equalsDnsLabelIgnoreDnsCase(pointer,
+ ((MdnsPointerRecord) other).pointer);
}
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
index 669b323..ecf846e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
@@ -21,9 +21,9 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.CollectionUtils;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -113,7 +113,8 @@
*/
private static boolean containsName(@NonNull List<MdnsRecord> records,
@NonNull String[] name) {
- return CollectionUtils.any(records, r -> Arrays.equals(name, r.getName()));
+ return CollectionUtils.any(records,
+ r -> MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, r.getName()));
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
index 19630e3..28bd1b4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -24,6 +24,7 @@
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.util.Arrays;
@@ -136,7 +137,7 @@
}
for (int i = 0; i < list1.length; ++i) {
- if (!list1[i].equals(list2[i + offset])) {
+ if (!MdnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
return false;
}
}
@@ -271,12 +272,13 @@
MdnsRecord otherRecord = (MdnsRecord) other;
- return Arrays.equals(name, otherRecord.name) && (type == otherRecord.type);
+ return MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
+ == otherRecord.type);
}
@Override
public int hashCode() {
- return Objects.hash(Arrays.hashCode(name), type);
+ return Objects.hash(Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(name)), type);
}
/**
@@ -297,7 +299,7 @@
public Key(int recordType, String[] recordName) {
this.recordType = recordType;
- this.recordName = recordName;
+ this.recordName = MdnsUtils.toDnsLabelsLowerCase(recordName);
}
@Override
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 1329172..1375279 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -30,6 +30,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.HexDump;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.net.Inet4Address;
@@ -69,6 +70,8 @@
// 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 =
@@ -156,13 +159,15 @@
@NonNull
public final List<RecordInfo<?>> allRecords;
@NonNull
- public final RecordInfo<MdnsPointerRecord> ptrRecord;
+ public final List<RecordInfo<MdnsPointerRecord>> ptrRecords;
@NonNull
public final RecordInfo<MdnsServiceRecord> srvRecord;
@NonNull
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.
@@ -175,14 +180,16 @@
* @param deviceHostname Hostname of the device (for the interface used)
* @param serviceInfo Service to advertise
*/
- ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo) {
+ ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
+ @Nullable String subtype) {
this.serviceInfo = serviceInfo;
+ this.subtype = subtype;
final String[] serviceType = splitServiceType(serviceInfo);
final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
// Service PTR record
- ptrRecord = new RecordInfo<>(
+ final RecordInfo<MdnsPointerRecord> ptrRecord = new RecordInfo<>(
serviceInfo,
new MdnsPointerRecord(
serviceType,
@@ -192,6 +199,26 @@
serviceName),
true /* sharedName */, true /* probing */);
+ 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 */, true /* probing */);
+
+ this.ptrRecords = List.of(ptrRecord, subtypeRecord);
+ }
+
srvRecord = new RecordInfo<>(
serviceInfo,
new MdnsServiceRecord(serviceName,
@@ -211,8 +238,8 @@
attrsToTextEntries(serviceInfo.getAttributes())),
false /* sharedName */, true /* probing */);
- final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(4);
- allRecords.add(ptrRecord);
+ final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(5);
+ allRecords.addAll(ptrRecords);
allRecords.add(srvRecord);
allRecords.add(txtRecord);
// Service type enumeration record (RFC6763 9.)
@@ -275,7 +302,8 @@
* ID of the replaced service.
* @throws NameConflictException There is already a (non-exiting) service using the name.
*/
- public int addService(int serviceId, NsdServiceInfo serviceInfo) throws NameConflictException {
+ public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable String subtype)
+ throws NameConflictException {
if (mServices.contains(serviceId)) {
throw new IllegalArgumentException(
"Service ID must not be reused across registrations: " + serviceId);
@@ -288,7 +316,7 @@
}
final ServiceRegistration registration = new ServiceRegistration(
- mDeviceHostname, serviceInfo);
+ mDeviceHostname, serviceInfo, subtype);
mServices.put(serviceId, registration);
// Remove existing exiting service
@@ -302,7 +330,8 @@
private int getServiceByName(@NonNull String serviceName) {
for (int i = 0; i < mServices.size(); i++) {
final ServiceRegistration registration = mServices.valueAt(i);
- if (serviceName.equals(registration.serviceInfo.getServiceName())) {
+ if (MdnsUtils.equalsIgnoreDnsCase(serviceName,
+ registration.serviceInfo.getServiceName())) {
return mServices.keyAt(i);
}
}
@@ -344,24 +373,25 @@
if (registration == null) return null;
if (registration.exiting) return null;
- // Send exit (TTL 0) for the PTR record, if the record was sent (in particular don't send
+ // Send exit (TTL 0) for the PTR records, if at least one was sent (in particular don't send
// if still probing)
- if (registration.ptrRecord.lastSentTimeMs == 0L) {
+ if (CollectionUtils.all(registration.ptrRecords, r -> r.lastSentTimeMs == 0L)) {
return null;
}
registration.exiting = true;
- final MdnsPointerRecord expiredRecord = new MdnsPointerRecord(
- registration.ptrRecord.record.getName(),
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- 0L /* ttlMillis */,
- registration.ptrRecord.record.getPointer());
+ final List<MdnsRecord> expiredRecords = CollectionUtils.map(registration.ptrRecords,
+ r -> new MdnsPointerRecord(
+ r.record.getName(),
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 0L /* ttlMillis */,
+ r.record.getPointer()));
// Exit should be skipped if the record is still advertised by another service, but that
// would be a conflict (2 service registrations with the same service name), so it would
// not have been allowed by the repository.
- return new MdnsAnnouncer.ExitAnnouncementInfo(id, Collections.singletonList(expiredRecord));
+ return new MdnsAnnouncer.ExitAnnouncementInfo(id, expiredRecords);
}
public void removeService(int id) {
@@ -442,7 +472,7 @@
for (int i = 0; i < mServices.size(); i++) {
final ServiceRegistration registration = mServices.valueAt(i);
if (registration.exiting) continue;
- addReplyFromService(question, registration.allRecords, registration.ptrRecord,
+ addReplyFromService(question, registration.allRecords, registration.ptrRecords,
registration.srvRecord, registration.txtRecord, replyUnicast, now,
answerInfo, additionalAnswerRecords);
}
@@ -499,7 +529,7 @@
*/
private void addReplyFromService(@NonNull MdnsRecord question,
@NonNull List<RecordInfo<?>> serviceRecords,
- @Nullable RecordInfo<MdnsPointerRecord> servicePtrRecord,
+ @Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords,
@Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
@Nullable RecordInfo<MdnsTextRecord> serviceTxtRecord,
boolean replyUnicast, long now, @NonNull List<RecordInfo<?>> answerInfo,
@@ -517,7 +547,9 @@
must match the question qtype unless the qtype is "ANY" (255) or the rrtype is
"CNAME" (5), and the record rrclass must match the question qclass unless the
qclass is "ANY" (255) */
- if (!Arrays.equals(info.record.getName(), question.getName())) continue;
+ if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(info.record.getName(), question.getName())) {
+ continue;
+ }
hasFullyOwnedNameMatch |= !info.isSharedName;
// The repository does not store CNAME records
@@ -531,7 +563,8 @@
}
hasKnownAnswer = true;
- hasDnsSdPtrRecordAnswer |= (info == servicePtrRecord);
+ hasDnsSdPtrRecordAnswer |= (servicePtrRecords != null
+ && CollectionUtils.any(servicePtrRecords, r -> info == r));
hasDnsSdSrvRecordAnswer |= (info == serviceSrvRecord);
// TODO: responses to probe queries should bypass this check and only ensure the
@@ -718,7 +751,10 @@
// happens. In fact probing is only done for the service name in the SRV record.
// This means only SRV and TXT records need to be checked.
final RecordInfo<MdnsServiceRecord> srvRecord = registration.srvRecord;
- if (!Arrays.equals(record.getName(), srvRecord.record.getName())) continue;
+ if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(record.getName(),
+ srvRecord.record.getName())) {
+ continue;
+ }
// As per RFC6762 9., it's fine if the "conflict" is an identical record with same
// data.
@@ -791,10 +827,11 @@
*/
@Nullable
public MdnsProber.ProbingInfo renameServiceForConflict(int serviceId, NsdServiceInfo newInfo) {
- if (!mServices.contains(serviceId)) return null;
+ final ServiceRegistration existing = mServices.get(serviceId);
+ if (existing == null) return null;
final ServiceRegistration newService = new ServiceRegistration(
- mDeviceHostname, newInfo);
+ mDeviceHostname, newInfo, existing.subtype);
mServices.put(serviceId, newService);
return makeProbingInfo(serviceId, newService.srvRecord.record);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
index e0100f6..28aa640 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -21,10 +21,10 @@
import android.net.Network;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
@@ -103,7 +103,7 @@
* pointer record is already present in the response with the same TTL.
*/
public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
- if (!Arrays.equals(serviceName, pointerRecord.getPointer())) {
+ if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceName, pointerRecord.getPointer())) {
throw new IllegalArgumentException(
"Pointer records for different service names cannot be added");
}
@@ -301,13 +301,13 @@
boolean dropAddressRecords = false;
for (MdnsInetAddressRecord inetAddressRecord : getInet4AddressRecords()) {
- if (!Arrays.equals(
+ if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
dropAddressRecords = true;
}
}
for (MdnsInetAddressRecord inetAddressRecord : getInet6AddressRecords()) {
- if (!Arrays.equals(
+ if (!MdnsUtils.equalsDnsLabelIgnoreDnsCase(
this.serviceRecord.getServiceHost(), inetAddressRecord.getName())) {
dropAddressRecords = true;
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
index 129db7e..77b5c58 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -23,10 +23,10 @@
import android.util.ArraySet;
import com.android.server.connectivity.mdns.util.MdnsLogger;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.EOFException;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@@ -50,7 +50,7 @@
List<MdnsResponse> responses, String[] pointer) {
if (responses != null) {
for (MdnsResponse response : responses) {
- if (Arrays.equals(response.getServiceName(), pointer)) {
+ if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(response.getServiceName(), pointer)) {
return response;
}
}
@@ -66,7 +66,8 @@
if (serviceRecord == null) {
continue;
}
- if (Arrays.equals(serviceRecord.getServiceHost(), hostName)) {
+ if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(),
+ hostName)) {
return response;
}
}
@@ -164,11 +165,8 @@
for (MdnsRecord record : records) {
if (record instanceof MdnsPointerRecord) {
String[] name = record.getName();
- if ((serviceType == null)
- || Arrays.equals(name, serviceType)
- || ((name.length == (serviceType.length + 2))
- && name[1].equals(MdnsConstants.SUBTYPE_LABEL)
- && MdnsRecord.labelsAreSuffix(serviceType, name))) {
+ if ((serviceType == null) || MdnsUtils.typeEqualsOrIsSubtype(
+ serviceType, name)) {
MdnsPointerRecord pointerRecord = (MdnsPointerRecord) record;
// Group PTR records that refer to the same service instance name into a single
// response.
@@ -296,7 +294,7 @@
if (serviceRecord == null) {
continue;
}
- if (Arrays.equals(serviceRecord.getServiceHost(), hostName)) {
+ if (MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceRecord.getServiceHost(), hostName)) {
if (result == null) {
result = new ArrayList<>(/* initialCapacity= */ responses.size());
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
index ebd8b77..f851b35 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -19,6 +19,7 @@
import android.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.util.Arrays;
@@ -142,7 +143,8 @@
@Override
public int hashCode() {
return (super.hashCode() * 31)
- + Objects.hash(servicePriority, serviceWeight, Arrays.hashCode(serviceHost),
+ + Objects.hash(servicePriority, serviceWeight,
+ Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(serviceHost)),
servicePort);
}
@@ -159,7 +161,7 @@
return super.equals(other)
&& (servicePriority == otherRecord.servicePriority)
&& (serviceWeight == otherRecord.serviceWeight)
- && Arrays.equals(serviceHost, otherRecord.serviceHost)
+ && MdnsUtils.equalsDnsLabelIgnoreDnsCase(serviceHost, otherRecord.serviceHost)
&& (servicePort == otherRecord.servicePort);
}
}
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 35f9b04..e56bd5b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -28,13 +28,16 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -175,6 +178,7 @@
@NonNull MdnsSearchOptions searchOptions) {
synchronized (lock) {
this.searchOptions = searchOptions;
+ boolean hadReply = false;
if (listeners.put(listener, searchOptions) == null) {
for (MdnsResponse existingResponse : instanceNameToResponse.values()) {
if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
@@ -183,6 +187,7 @@
listener.onServiceNameDiscovered(info);
if (existingResponse.isComplete()) {
listener.onServiceFound(info);
+ hadReply = true;
}
}
}
@@ -192,22 +197,37 @@
}
// Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
// interested anymore.
- requestTaskFuture =
- executor.submit(
- new QueryTask(
- new QueryTaskConfig(
- searchOptions.getSubtypes(),
- searchOptions.isPassiveMode(),
- ++currentSessionId,
- searchOptions.getNetwork())));
+ final QueryTaskConfig taskConfig = new QueryTaskConfig(
+ searchOptions.getSubtypes(),
+ searchOptions.isPassiveMode(),
+ ++currentSessionId,
+ searchOptions.getNetwork());
+ if (hadReply) {
+ requestTaskFuture = scheduleNextRunLocked(taskConfig);
+ } else {
+ requestTaskFuture = executor.submit(new QueryTask(taskConfig));
+ }
}
}
private boolean responseMatchesOptions(@NonNull MdnsResponse response,
@NonNull MdnsSearchOptions options) {
- if (options.getResolveInstanceName() == null) return true;
- // DNS is case-insensitive, so ignore case in the comparison
- return options.getResolveInstanceName().equalsIgnoreCase(response.getServiceInstanceName());
+ final boolean matchesInstanceName = options.getResolveInstanceName() == null
+ // DNS is case-insensitive, so ignore case in the comparison
+ || MdnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
+ response.getServiceInstanceName());
+
+ // If discovery is requiring some subtypes, the response must have one that matches a
+ // requested one.
+ final List<String> responseSubtypes = response.getSubtypes() == null
+ ? Collections.emptyList() : response.getSubtypes();
+ final boolean matchesSubtype = options.getSubtypes().size() == 0
+ || CollectionUtils.any(options.getSubtypes(), requiredSub ->
+ CollectionUtils.any(responseSubtypes, actualSub ->
+ MdnsUtils.equalsIgnoreDnsCase(
+ MdnsConstants.SUBTYPE_PREFIX + requiredSub, actualSub)));
+
+ return matchesInstanceName && matchesSubtype;
}
/**
@@ -264,7 +284,7 @@
}
/** Notify all services are removed because the socket is destroyed. */
- public void notifyAllServicesRemoved() {
+ public void notifySocketDestroyed() {
synchronized (lock) {
for (MdnsResponse response : instanceNameToResponse.values()) {
final String name = response.getServiceInstanceName();
@@ -282,6 +302,11 @@
listener.onServiceNameRemoved(serviceInfo);
}
}
+
+ if (requestTaskFuture != null) {
+ requestTaskFuture.cancel(true);
+ requestTaskFuture = null;
+ }
}
}
@@ -574,11 +599,14 @@
}
}
}
- QueryTaskConfig config = this.config.getConfigForNextRun();
- requestTaskFuture =
- executor.schedule(
- new QueryTask(config), config.timeToRunNextTaskInMs, MILLISECONDS);
+ requestTaskFuture = scheduleNextRunLocked(this.config);
}
}
}
+
+ @NonNull
+ private Future<?> scheduleNextRunLocked(@NonNull QueryTaskConfig lastRunConfig) {
+ QueryTaskConfig config = lastRunConfig.getConfigForNextRun();
+ return executor.schedule(new QueryTask(config), config.timeToRunNextTaskInMs, MILLISECONDS);
+ }
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
index 6bcad01..c614cd3 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
@@ -88,6 +88,6 @@
void onSocketCreated(@Nullable Network network);
/*** Notify requested socket is destroyed */
- void onSocketDestroyed(@Nullable Network network);
+ void onAllSocketsDestroyed(@Nullable Network network);
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
index ca61d34..2fa1ae4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
@@ -414,8 +414,10 @@
if (networkKey == LOCAL_NET) {
transports = new int[0];
} else {
- transports = mActiveNetworksTransports.getOrDefault(
- ((NetworkAsKey) networkKey).mNetwork, new int[0]);
+ transports = mActiveNetworksTransports.get(((NetworkAsKey) networkKey).mNetwork);
+ if (transports == null) {
+ Log.wtf(TAG, "transports is missing for key: " + networkKey);
+ }
}
if (networkInterface == null || !isMdnsCapableInterface(networkInterface, transports)) {
return;
@@ -599,8 +601,6 @@
if (matchRequestedNetwork(network)) continue;
final SocketInfo info = mNetworkSockets.removeAt(i);
info.mSocket.destroy();
- // Still notify to unrequester for socket destroy.
- cb.onInterfaceDestroyed(network, info.mSocket);
mSharedLog.log("Remove socket on net:" + network + " after unrequestSocket");
}
@@ -610,8 +610,6 @@
for (int i = mTetherInterfaceSockets.size() - 1; i >= 0; i--) {
final SocketInfo info = mTetherInterfaceSockets.valueAt(i);
info.mSocket.destroy();
- // Still notify to unrequester for socket destroy.
- cb.onInterfaceDestroyed(null /* network */, info.mSocket);
mSharedLog.log("Remove socket on ifName:" + mTetherInterfaceSockets.keyAt(i)
+ " after unrequestSocket");
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java b/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java
index 40191cf..451909c 100644
--- a/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java
+++ b/service-t/src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java
@@ -61,13 +61,11 @@
final StructIfaddrMsg ifaddrMsg = msg.getIfaddrHeader();
final LinkAddress la = new LinkAddress(msg.getIpAddress(), ifaddrMsg.prefixLen,
msg.getFlags(), ifaddrMsg.scope);
- if (!la.isPreferred()) {
- // Skip the unusable ip address.
- return;
- }
switch (msg.getHeader().nlmsg_type) {
case NetlinkConstants.RTM_NEWADDR:
- mCb.addOrUpdateInterfaceAddress(ifaddrMsg.index, la);
+ if (la.isPreferred()) {
+ mCb.addOrUpdateInterfaceAddress(ifaddrMsg.index, la);
+ }
break;
case NetlinkConstants.RTM_DELADDR:
mCb.deleteInterfaceAddress(ifaddrMsg.index, la);
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 5cc789f..bc94869 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
@@ -21,6 +21,9 @@
import android.net.Network;
import android.os.Handler;
+import com.android.server.connectivity.mdns.MdnsConstants;
+import com.android.server.connectivity.mdns.MdnsRecord;
+
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
@@ -50,6 +53,17 @@
}
/**
+ * Convert the array of labels to DNS case-insensitive lowercase.
+ */
+ public static String[] toDnsLabelsLowerCase(@NonNull String[] labels) {
+ final String[] outStrings = new String[labels.length];
+ for (int i = 0; i < labels.length; ++i) {
+ outStrings[i] = toDnsLowerCase(labels[i]);
+ }
+ return outStrings;
+ }
+
+ /**
* Compare two strings by DNS case-insensitive lowercase.
*/
public static boolean equalsIgnoreDnsCase(@NonNull String a, @NonNull String b) {
@@ -62,18 +76,59 @@
return true;
}
+ /**
+ * Compare two set of DNS labels by DNS case-insensitive lowercase.
+ */
+ public static boolean equalsDnsLabelIgnoreDnsCase(@NonNull String[] a, @NonNull String[] b) {
+ if (a == b) {
+ return true;
+ }
+ int length = a.length;
+ if (b.length != length) {
+ return false;
+ }
+ for (int i = 0; i < length; i++) {
+ if (!equalsIgnoreDnsCase(a[i], b[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Compare labels a equals b or a is suffix of b.
+ *
+ * @param a the type or subtype.
+ * @param b the base type
+ */
+ public static boolean typeEqualsOrIsSubtype(@NonNull String[] a,
+ @NonNull String[] b) {
+ return MdnsUtils.equalsDnsLabelIgnoreDnsCase(a, b)
+ || ((b.length == (a.length + 2))
+ && MdnsUtils.equalsIgnoreDnsCase(b[1], MdnsConstants.SUBTYPE_LABEL)
+ && MdnsRecord.labelsAreSuffix(a, b));
+ }
+
private static char toDnsLowerCase(char a) {
return a >= 'A' && a <= 'Z' ? (char) (a + ('a' - 'A')) : a;
}
/*** Ensure that current running thread is same as given handler thread */
public static void ensureRunningOnHandlerThread(@NonNull Handler handler) {
- if (handler.getLooper().getThread() != Thread.currentThread()) {
+ if (!isRunningOnHandlerThread(handler)) {
throw new IllegalStateException(
"Not running on Handler thread: " + Thread.currentThread().getName());
}
}
+ /*** Check that current running thread is same as given handler thread */
+ public static boolean isRunningOnHandlerThread(@NonNull Handler handler) {
+ if (handler.getLooper().getThread() == Thread.currentThread()) {
+ return true;
+ }
+ return false;
+ }
+
/*** Check whether the target network is matched current network */
public static boolean isNetworkMatched(@Nullable Network targetNetwork,
@Nullable Network currentNetwork) {
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
index 3e4c4de..08d31a3 100644
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -38,7 +38,7 @@
#include "jni.h"
#include <android-base/stringprintf.h>
#include <android-base/unique_fd.h>
-#include <bpf/KernelVersion.h>
+#include <bpf/KernelUtils.h>
#include <nativehelper/JNIHelp.h>
#include <nativehelper/ScopedUtfChars.h>
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 84e581e..ec168dd 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -384,7 +384,6 @@
* ALLOWLIST means the firewall denies all by default, uids must be explicitly allowed
* DENYLIST means the firewall allows all by default, uids must be explicitly denyed
*/
- @VisibleForTesting
public boolean isFirewallAllowList(final int chain) {
switch (chain) {
case FIREWALL_CHAIN_DOZABLE:
@@ -745,6 +744,65 @@
}
}
+ private Set<Integer> getUidsMatchEnabled(final int childChain) throws ErrnoException {
+ final long match = getMatchByFirewallChain(childChain);
+ Set<Integer> uids = new ArraySet<>();
+ synchronized (sUidOwnerMap) {
+ sUidOwnerMap.forEach((uid, val) -> {
+ if (val == null) {
+ Log.wtf(TAG, "sUidOwnerMap entry was deleted while holding a lock");
+ } else {
+ if ((val.rule & match) != 0) {
+ uids.add(uid.val);
+ }
+ }
+ });
+ }
+ return uids;
+ }
+
+ /**
+ * Get uids that has FIREWALL_RULE_ALLOW on allowlist chain.
+ * Allowlist means the firewall denies all by default, uids must be explicitly allowed.
+ *
+ * Note that uids that has FIREWALL_RULE_DENY on allowlist chain can not be computed from the
+ * bpf map, since all the uids that does not have explicit FIREWALL_RULE_ALLOW rule in bpf map
+ * are determined to have FIREWALL_RULE_DENY.
+ *
+ * @param childChain target chain
+ * @return Set of uids
+ */
+ public Set<Integer> getUidsWithAllowRuleOnAllowListChain(final int childChain)
+ throws ErrnoException {
+ if (!isFirewallAllowList(childChain)) {
+ throw new IllegalArgumentException("getUidsWithAllowRuleOnAllowListChain is called with"
+ + " denylist chain:" + childChain);
+ }
+ // Corresponding match is enabled for uids that has FIREWALL_RULE_ALLOW on allowlist chain.
+ return getUidsMatchEnabled(childChain);
+ }
+
+ /**
+ * Get uids that has FIREWALL_RULE_DENY on denylist chain.
+ * Denylist means the firewall allows all by default, uids must be explicitly denyed
+ *
+ * Note that uids that has FIREWALL_RULE_ALLOW on denylist chain can not be computed from the
+ * bpf map, since all the uids that does not have explicit FIREWALL_RULE_DENY rule in bpf map
+ * are determined to have the FIREWALL_RULE_ALLOW.
+ *
+ * @param childChain target chain
+ * @return Set of uids
+ */
+ public Set<Integer> getUidsWithDenyRuleOnDenyListChain(final int childChain)
+ throws ErrnoException {
+ if (isFirewallAllowList(childChain)) {
+ throw new IllegalArgumentException("getUidsWithDenyRuleOnDenyListChain is called with"
+ + " allowlist chain:" + childChain);
+ }
+ // Corresponding match is enabled for uids that has FIREWALL_RULE_DENY on denylist chain.
+ return getUidsMatchEnabled(childChain);
+ }
+
/**
* Add ingress interface filtering rules to a list of UIDs
*
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index b5c9b0a..f579093 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -1509,6 +1509,33 @@
throws SocketException, InterruptedIOException, ErrnoException {
InetDiagMessage.destroyLiveTcpSockets(ranges, exemptUids);
}
+
+ /**
+ * Call {@link InetDiagMessage#destroyLiveTcpSocketsByOwnerUids(Set)}
+ *
+ * @param ownerUids target uids to close sockets
+ */
+ public void destroyLiveTcpSocketsByOwnerUids(final Set<Integer> ownerUids)
+ throws SocketException, InterruptedIOException, ErrnoException {
+ InetDiagMessage.destroyLiveTcpSocketsByOwnerUids(ownerUids);
+ }
+
+ /**
+ * Schedule the evaluation timeout.
+ *
+ * When a network connects, it's "not evaluated" yet. Detection events cause the network
+ * to be "evaluated" (typically, validation or detection of a captive portal). If none
+ * of these events happen, this time will run out, after which the network is considered
+ * "evaluated" even if nothing happened to it. Notionally that means the system gave up
+ * on this network and considers it won't provide connectivity. In particular, that means
+ * it's when the system prefers it to cell if it's wifi and configuration says it should
+ * prefer bad wifi to cell.
+ */
+ public void scheduleEvaluationTimeout(@NonNull Handler handler,
+ @NonNull final Network network, final long delayMs) {
+ handler.sendMessageDelayed(
+ handler.obtainMessage(EVENT_INITIAL_EVALUATION_TIMEOUT, network), delayMs);
+ }
}
public ConnectivityService(Context context) {
@@ -4536,7 +4563,7 @@
// because they lost all their requests or because their score isn't good)
// then they would disconnect organically, report their new state and then
// disconnect the channel.
- if (nai.networkInfo.isConnected()) {
+ if (nai.networkInfo.isConnected() || nai.networkInfo.isSuspended()) {
nai.networkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED,
null, null);
}
@@ -5263,8 +5290,7 @@
/** Schedule evaluation timeout */
@VisibleForTesting
public void scheduleEvaluationTimeout(@NonNull final Network network, final long delayMs) {
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(EVENT_INITIAL_EVALUATION_TIMEOUT, network), delayMs);
+ mDeps.scheduleEvaluationTimeout(mHandler, network, delayMs);
}
@Override
@@ -9816,7 +9842,7 @@
networkAgent.networkMonitor().notifyNetworkConnected(params.linkProperties,
params.networkCapabilities);
}
- final long delay = activelyPreferBadWifi()
+ final long delay = !avoidBadWifi() && activelyPreferBadWifi()
? ACTIVELY_PREFER_BAD_WIFI_INITIAL_TIMEOUT_MS
: DONT_ACTIVELY_PREFER_BAD_WIFI_INITIAL_TIMEOUT_MS;
scheduleEvaluationTimeout(networkAgent.network, delay);
@@ -12048,6 +12074,23 @@
return rule;
}
+ private void closeSocketsForFirewallChainLocked(final int chain)
+ throws ErrnoException, SocketException, InterruptedIOException {
+ if (mBpfNetMaps.isFirewallAllowList(chain)) {
+ // Allowlist means the firewall denies all by default, uids must be explicitly allowed
+ // So, close all non-system socket owned by uids that are not explicitly allowed
+ Set<Range<Integer>> ranges = new ArraySet<>();
+ ranges.add(new Range<>(Process.FIRST_APPLICATION_UID, Integer.MAX_VALUE));
+ final Set<Integer> exemptUids = mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(chain);
+ mDeps.destroyLiveTcpSockets(ranges, exemptUids);
+ } else {
+ // Denylist means the firewall allows all by default, uids must be explicitly denied
+ // So, close socket owned by uids that are explicitly denied
+ final Set<Integer> ownerUids = mBpfNetMaps.getUidsWithDenyRuleOnDenyListChain(chain);
+ mDeps.destroyLiveTcpSocketsByOwnerUids(ownerUids);
+ }
+ }
+
@Override
public void setFirewallChainEnabled(final int chain, final boolean enable) {
enforceNetworkStackOrSettingsPermission();
@@ -12057,6 +12100,14 @@
} catch (ServiceSpecificException e) {
throw new IllegalStateException(e);
}
+
+ if (SdkLevel.isAtLeastU() && enable) {
+ try {
+ closeSocketsForFirewallChainLocked(chain);
+ } catch (ErrnoException | SocketException | InterruptedIOException e) {
+ Log.e(TAG, "Failed to close sockets after enabling chain (" + chain + "): " + e);
+ }
+ }
}
@Override
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index e2ef981..f8285ed 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -210,8 +210,12 @@
this.mKi = Objects.requireNonNull(ki);
mCallback = ki.mCallback;
mUnderpinnedNetwork = underpinnedNetwork;
- if (autoOnOff && mDependencies.isFeatureEnabled(AUTOMATIC_ON_OFF_KEEPALIVE_VERSION,
- true /* defaultEnabled */)) {
+ // Reading DeviceConfig will check if the calling uid and calling package name are the
+ // same. Clear calling identity to align the calling uid and package
+ final boolean enabled = BinderUtils.withCleanCallingIdentity(
+ () -> mDependencies.isFeatureEnabled(AUTOMATIC_ON_OFF_KEEPALIVE_VERSION,
+ true /* defaultEnabled */));
+ if (autoOnOff && enabled) {
mAutomaticOnOffState = STATE_ENABLED;
if (null == ki.mFd) {
throw new IllegalArgumentException("fd can't be null with automatic "
@@ -583,8 +587,12 @@
*/
public void dump(IndentingPrintWriter pw) {
mKeepaliveTracker.dump(pw);
- final boolean featureEnabled = mDependencies.isFeatureEnabled(
- AUTOMATIC_ON_OFF_KEEPALIVE_VERSION, true /* defaultEnabled */);
+ // Reading DeviceConfig will check if the calling uid and calling package name are the same.
+ // Clear calling identity to align the calling uid and package so that it won't fail if cts
+ // would like to call dump()
+ final boolean featureEnabled = BinderUtils.withCleanCallingIdentity(
+ () -> mDependencies.isFeatureEnabled(AUTOMATIC_ON_OFF_KEEPALIVE_VERSION,
+ true /* defaultEnabled */));
pw.println("AutomaticOnOff enabled: " + featureEnabled);
pw.increaseIndent();
for (AutomaticOnOffKeepalive autoKi : mAutomaticOnOffKeepalives) {
@@ -837,12 +845,8 @@
* @return whether the feature is enabled
*/
public boolean isFeatureEnabled(@NonNull final String name, final boolean defaultEnabled) {
- // Reading DeviceConfig will check if the calling uid and calling package name are the
- // same. Clear calling identity to align the calling uid and package so that it won't
- // fail if cts would like to do the dump()
- return BinderUtils.withCleanCallingIdentity(() ->
- DeviceConfigUtils.isFeatureEnabled(mContext, NAMESPACE_TETHERING, name,
- DeviceConfigUtils.TETHERING_MODULE_NAME, defaultEnabled));
+ return DeviceConfigUtils.isFeatureEnabled(mContext, NAMESPACE_TETHERING, name,
+ DeviceConfigUtils.TETHERING_MODULE_NAME, defaultEnabled);
}
/**
diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java
index 8c170bc..cc226ce 100644
--- a/service/src/com/android/server/connectivity/KeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveTracker.java
@@ -569,7 +569,20 @@
} else if (reason == ERROR_STOP_REASON_UNINITIALIZED) {
throw new IllegalStateException("Unexpected stop reason: " + reason);
} else if (reason == ERROR_NO_SUCH_SLOT) {
- throw new IllegalStateException("No such slot: " + reason);
+ // There are multiple independent reasons a keepalive can stop. Some
+ // are software (e.g. the app stops the keepalive) and some are hardware
+ // (e.g. the SIM card gets removed). Therefore, there is a very low
+ // probability that both of these happen at the same time, which would
+ // result in the first stop attempt returning SUCCESS and the second
+ // stop attempt returning NO_SUCH_SLOT. Such a race condition can be
+ // ignored with a log.
+ // This should still be reported because if it happens with any frequency
+ // it probably means there is a bug where the system server is trying
+ // to use a non-existing hardware slot.
+ // TODO : separate the non-existing hardware slot from the case where
+ // there is no keepalive running on this slot.
+ Log.wtf(TAG, "Keepalive on slot " + slot + " can't be stopped : " + reason);
+ notifyErrorCallback(ki.mCallback, reason);
} else {
notifyErrorCallback(ki.mCallback, reason);
}
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index f9fe5b0..51ee074 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -61,7 +61,7 @@
// uncomment when b/13249961 is fixed
// sdk_version: "current",
platform_apis: true,
- data: [":ConnectivityChecker"],
+ data: [":ConnectivityTestPreparer"],
per_testcase_directory: true,
host_required: ["net-tests-utils-host-common"],
test_config_template: "AndroidTestTemplate.xml",
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index d2fb04a..8efa99f 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -28,7 +28,7 @@
<option name="cleanup-apks" value="true" />
<option name="test-file-name" value="{MODULE}.apk" />
</target_preparer>
- <target_preparer class="com.android.testutils.ConnectivityCheckTargetPreparer">
+ <target_preparer class="com.android.testutils.ConnectivityTestTargetPreparer">
</target_preparer>
<target_preparer class="com.android.testutils.DisableConfigSyncTargetPreparer">
</target_preparer>
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index 7662ba3..60befca 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -117,7 +117,7 @@
private static final int FAIL_RATE_PERCENTAGE = 100;
private static final int UNKNOWN_DETECTION_METHOD = 4;
private static final int FILTERED_UNKNOWN_DETECTION_METHOD = 0;
- private static final int CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT = 5000;
+ private static final int CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT = 10000;
private static final int DELAY_FOR_BROADCAST_IDLE = 30_000;
private static final Executor INLINE_EXECUTOR = x -> x.run();
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 8b059e3..ee2f6bb 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -225,6 +225,7 @@
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Socket;
+import java.net.SocketException;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
@@ -278,6 +279,7 @@
// TODO(b/252972908): reset the original timer when aosp/2188755 is ramped up.
private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 30_000;
private static final int NO_CALLBACK_TIMEOUT_MS = 100;
+ private static final int NETWORK_REQUEST_TIMEOUT_MS = 3000;
private static final int SOCKET_TIMEOUT_MS = 100;
private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500;
@@ -3553,6 +3555,103 @@
doTestFirewallBlocking(FIREWALL_CHAIN_OEM_DENY_3, DENYLIST);
}
+ private void assertSocketOpen(final Socket socket) throws Exception {
+ mCtsNetUtils.testHttpRequest(socket);
+ }
+
+ private void assertSocketClosed(final Socket socket) throws Exception {
+ try {
+ mCtsNetUtils.testHttpRequest(socket);
+ fail("Socket is expected to be closed");
+ } catch (SocketException expected) {
+ }
+ }
+
+ private static final boolean EXPECT_OPEN = false;
+ private static final boolean EXPECT_CLOSE = true;
+
+ private void doTestFirewallCloseSocket(final int chain, final int rule, final int targetUid,
+ final boolean expectClose) {
+ runWithShellPermissionIdentity(() -> {
+ // Firewall chain status will be restored after the test.
+ final boolean wasChainEnabled = mCm.getFirewallChainEnabled(chain);
+ final int previousUidFirewallRule = mCm.getUidFirewallRule(chain, targetUid);
+ final Socket socket = new Socket(TEST_HOST, HTTP_PORT);
+ socket.setSoTimeout(NETWORK_REQUEST_TIMEOUT_MS);
+ testAndCleanup(() -> {
+ mCm.setFirewallChainEnabled(chain, false /* enable */);
+ assertSocketOpen(socket);
+
+ try {
+ mCm.setUidFirewallRule(chain, targetUid, rule);
+ } catch (IllegalStateException ignored) {
+ // Removing match causes an exception when the rule entry for the uid does
+ // not exist. But this is fine and can be ignored.
+ }
+ mCm.setFirewallChainEnabled(chain, true /* enable */);
+
+ if (expectClose) {
+ assertSocketClosed(socket);
+ } else {
+ assertSocketOpen(socket);
+ }
+ }, /* cleanup */ () -> {
+ // Restore the global chain status
+ mCm.setFirewallChainEnabled(chain, wasChainEnabled);
+ }, /* cleanup */ () -> {
+ // Restore the uid firewall rule status
+ try {
+ mCm.setUidFirewallRule(chain, targetUid, previousUidFirewallRule);
+ } catch (IllegalStateException ignored) {
+ // Removing match causes an exception when the rule entry for the uid does
+ // not exist. But this is fine and can be ignored.
+ }
+ }, /* cleanup */ () -> {
+ socket.close();
+ });
+ }, NETWORK_SETTINGS);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) @ConnectivityModuleTest
+ public void testFirewallCloseSocketAllowlistChainAllow() {
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_DOZABLE, FIREWALL_RULE_ALLOW,
+ Process.myUid(), EXPECT_OPEN);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) @ConnectivityModuleTest
+ public void testFirewallCloseSocketAllowlistChainDeny() {
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_DOZABLE, FIREWALL_RULE_DENY,
+ Process.myUid(), EXPECT_CLOSE);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) @ConnectivityModuleTest
+ public void testFirewallCloseSocketAllowlistChainOtherUid() {
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_DOZABLE, FIREWALL_RULE_ALLOW,
+ Process.myUid() + 1, EXPECT_CLOSE);
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_DOZABLE, FIREWALL_RULE_DENY,
+ Process.myUid() + 1, EXPECT_CLOSE);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) @ConnectivityModuleTest
+ public void testFirewallCloseSocketDenylistChainAllow() {
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_STANDBY, FIREWALL_RULE_ALLOW,
+ Process.myUid(), EXPECT_OPEN);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) @ConnectivityModuleTest
+ public void testFirewallCloseSocketDenylistChainDeny() {
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_STANDBY, FIREWALL_RULE_DENY,
+ Process.myUid(), EXPECT_CLOSE);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) @ConnectivityModuleTest
+ public void testFirewallCloseSocketDenylistChainOtherUid() {
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_STANDBY, FIREWALL_RULE_ALLOW,
+ Process.myUid() + 1, EXPECT_OPEN);
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_STANDBY, FIREWALL_RULE_DENY,
+ Process.myUid() + 1, EXPECT_OPEN);
+ }
+
private void assumeTestSApis() {
// Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
// shims, and @IgnoreUpTo does not check that.
diff --git a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
index f935cef..cc0a5df 100644
--- a/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java
@@ -457,9 +457,8 @@
long newUidRxPackets = TrafficStats.getUidRxPackets(Os.getuid());
assertEquals(expectedTxByteDelta, newUidTxBytes - uidTxBytes);
- assertTrue(
- newUidRxBytes - uidRxBytes >= minRxByteDelta
- && newUidRxBytes - uidRxBytes <= maxRxByteDelta);
+ assertTrue("Not enough bytes", newUidRxBytes - uidRxBytes >= minRxByteDelta);
+ assertTrue("Too many bytes", newUidRxBytes - uidRxBytes <= maxRxByteDelta);
assertEquals(expectedTxPacketDelta, newUidTxPackets - uidTxPackets);
assertEquals(expectedRxPacketDelta, newUidRxPackets - uidRxPackets);
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index d8a0b07..83b9b81 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -799,7 +799,7 @@
// harness, which is untagged, won't cause a failure.
long firstTotal = resultsWithTraffic.get(0).total;
for (QueryResult queryResult : resultsWithTraffic) {
- assertWithinPercentage(queryResult + "", firstTotal, queryResult.total, 10);
+ assertWithinPercentage(queryResult + "", firstTotal, queryResult.total, 12);
}
// Expect to see no traffic when querying for any tag in tagsWithNoTraffic or any
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index db7f38c..9808137 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -73,8 +73,8 @@
import android.system.OsConstants.IPPROTO_UDP
import android.system.OsConstants.SOCK_DGRAM
import android.util.Log
+import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.runner.AndroidJUnit4
import com.android.compatibility.common.util.PollingCheck
import com.android.compatibility.common.util.PropertyUtil
import com.android.modules.utils.build.SdkLevel.isAtLeastU
@@ -84,6 +84,8 @@
import com.android.networkstack.apishim.common.NsdShim
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestableNetworkAgent
@@ -129,12 +131,15 @@
// tried sequentially
private const val REGISTRATION_TIMEOUT_MS = 10_000L
private const val DBG = false
+private const val TEST_PORT = 12345
private val nsdShim = NsdShimImpl.newInstance()
@AppModeFull(reason = "Socket cannot bind in instant app mode")
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
@ConnectivityModuleTest
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class NsdManagerTest {
// Rule used to filter CtsNetTestCasesMaxTargetSdkXX
@get:Rule
@@ -282,13 +287,17 @@
fun waitForServiceDiscovered(
serviceName: String,
+ serviceType: String,
expectedNetwork: Network? = null
): NsdServiceInfo {
- return expectCallbackEventually<ServiceFound> {
+ val serviceFound = expectCallbackEventually<ServiceFound> {
it.serviceInfo.serviceName == serviceName &&
(expectedNetwork == null ||
expectedNetwork == nsdShim.getNetwork(it.serviceInfo))
}.serviceInfo
+ // Discovered service types have a dot at the end
+ assertEquals("$serviceType.", serviceFound.serviceType)
+ return serviceFound
}
}
@@ -432,6 +441,13 @@
return agent
}
+ private fun makeTestServiceInfo(network: Network? = null) = NsdServiceInfo().also {
+ it.serviceType = serviceType
+ it.serviceName = serviceName
+ it.network = network
+ it.port = TEST_PORT
+ }
+
@After
fun tearDown() {
if (TestUtils.shouldTestTApis()) {
@@ -497,6 +513,10 @@
val registeredInfo = registrationRecord.expectCallback<ServiceRegistered>(
REGISTRATION_TIMEOUT_MS).serviceInfo
+ // Only service name is included in ServiceRegistered callbacks
+ assertNull(registeredInfo.serviceType)
+ assertEquals(si.serviceName, registeredInfo.serviceName)
+
val discoveryRecord = NsdDiscoveryRecord()
// Test discovering without an Executor
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
@@ -505,12 +525,15 @@
discoveryRecord.expectCallback<DiscoveryStarted>()
// Expect a service record to be discovered
- val foundInfo = discoveryRecord.waitForServiceDiscovered(registeredInfo.serviceName)
+ val foundInfo = discoveryRecord.waitForServiceDiscovered(
+ registeredInfo.serviceName, serviceType)
// Test resolving without an Executor
val resolveRecord = NsdResolveRecord()
nsdManager.resolveService(foundInfo, resolveRecord)
val resolvedService = resolveRecord.expectCallback<ServiceResolved>().serviceInfo
+ assertEquals(".$serviceType", resolvedService.serviceType)
+ assertEquals(registeredInfo.serviceName, resolvedService.serviceName)
// Check Txt attributes
assertEquals(8, resolvedService.attributes.size)
@@ -538,9 +561,11 @@
registrationRecord.expectCallback<ServiceUnregistered>()
// Expect a callback for service lost
- discoveryRecord.expectCallbackEventually<ServiceLost> {
+ val lostCb = discoveryRecord.expectCallbackEventually<ServiceLost> {
it.serviceInfo.serviceName == serviceName
}
+ // Lost service types have a dot at the end
+ assertEquals("$serviceType.", lostCb.serviceInfo.serviceType)
// Register service again to see if NsdManager can discover it
val si2 = NsdServiceInfo()
@@ -554,7 +579,8 @@
// Expect a service record to be discovered (and filter the ones
// that are unrelated to this test)
- val foundInfo2 = discoveryRecord.waitForServiceDiscovered(registeredInfo2.serviceName)
+ val foundInfo2 = discoveryRecord.waitForServiceDiscovered(
+ registeredInfo2.serviceName, serviceType)
// Resolve the service
val resolveRecord2 = NsdResolveRecord()
@@ -591,7 +617,7 @@
testNetwork1.network, Executor { it.run() }, discoveryRecord)
val foundInfo = discoveryRecord.waitForServiceDiscovered(
- serviceName, testNetwork1.network)
+ serviceName, serviceType, testNetwork1.network)
assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
// Rewind to ensure the service is not found on the other interface
@@ -638,6 +664,8 @@
val serviceDiscovered = discoveryRecord.expectCallback<ServiceFound>()
assertEquals(registeredInfo1.serviceName, serviceDiscovered.serviceInfo.serviceName)
+ // Discovered service types have a dot at the end
+ assertEquals("$serviceType.", serviceDiscovered.serviceInfo.serviceType)
assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered.serviceInfo))
// Unregister, then register the service back: it should be lost and found again
@@ -650,6 +678,7 @@
val registeredInfo2 = registerService(registrationRecord, si, executor)
val serviceDiscovered2 = discoveryRecord.expectCallback<ServiceFound>()
assertEquals(registeredInfo2.serviceName, serviceDiscovered2.serviceInfo.serviceName)
+ assertEquals("$serviceType.", serviceDiscovered2.serviceInfo.serviceType)
assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered2.serviceInfo))
// Teardown, then bring back up a network on the test interface: the service should
@@ -665,6 +694,7 @@
val newNetwork = newAgent.network ?: fail("Registered agent should have a network")
val serviceDiscovered3 = discoveryRecord.expectCallback<ServiceFound>()
assertEquals(registeredInfo2.serviceName, serviceDiscovered3.serviceInfo.serviceName)
+ assertEquals("$serviceType.", serviceDiscovered3.serviceInfo.serviceType)
assertEquals(newNetwork, nsdShim.getNetwork(serviceDiscovered3.serviceInfo))
} cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord)
@@ -740,12 +770,12 @@
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
val foundInfo1 = discoveryRecord.waitForServiceDiscovered(
- serviceName, testNetwork1.network)
+ serviceName, serviceType, testNetwork1.network)
assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo1))
// Rewind as the service could be found on each interface in any order
discoveryRecord.nextEvents.rewind(0)
val foundInfo2 = discoveryRecord.waitForServiceDiscovered(
- serviceName, testNetwork2.network)
+ serviceName, serviceType, testNetwork2.network)
assertEquals(testNetwork2.network, nsdShim.getNetwork(foundInfo2))
nsdShim.resolveService(nsdManager, foundInfo1, Executor { it.run() }, resolveRecord)
@@ -790,7 +820,7 @@
testNetwork1.network, Executor { it.run() }, discoveryRecord)
// Expect that service is found on testNetwork1
val foundInfo = discoveryRecord.waitForServiceDiscovered(
- serviceName, testNetwork1.network)
+ serviceName, serviceType, testNetwork1.network)
assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
// Discover service on testNetwork2.
@@ -805,7 +835,7 @@
null as Network? /* network */, Executor { it.run() }, discoveryRecord3)
// Expect that service is found on testNetwork1
val foundInfo3 = discoveryRecord3.waitForServiceDiscovered(
- serviceName, testNetwork1.network)
+ serviceName, serviceType, testNetwork1.network)
assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo3))
} cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord2)
@@ -835,7 +865,7 @@
nsdManager.discoverServices(
serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord
)
- val foundInfo = discoveryRecord.waitForServiceDiscovered(serviceNames)
+ val foundInfo = discoveryRecord.waitForServiceDiscovered(serviceNames, serviceType)
// Expect that resolving the service name works properly even service name contains
// non-standard characters.
@@ -985,7 +1015,7 @@
nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
testNetwork1.network, Executor { it.run() }, discoveryRecord)
val foundInfo = discoveryRecord.waitForServiceDiscovered(
- serviceName, testNetwork1.network)
+ serviceName, serviceType, testNetwork1.network)
// Register service callback and check the addresses are the same as network addresses
nsdShim.registerServiceInfoCallback(nsdManager, foundInfo, { it.run() }, cbRecord)
@@ -1030,6 +1060,52 @@
assertEquals(NsdManager.FAILURE_OPERATION_NOT_RUNNING, failedCb.errorCode)
}
+ @Test
+ fun testSubtypeAdvertisingAndDiscovery() {
+ val si = makeTestServiceInfo(network = testNetwork1.network)
+ // Test "_type._tcp.local,_subtype" syntax with the registration
+ si.serviceType = si.serviceType + ",_subtype"
+
+ val registrationRecord = NsdRegistrationRecord()
+
+ val baseTypeDiscoveryRecord = NsdDiscoveryRecord()
+ val subtypeDiscoveryRecord = NsdDiscoveryRecord()
+ val otherSubtypeDiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si)
+
+ // Test "_subtype._type._tcp.local" syntax with discovery. Note this is not
+ // "_subtype._sub._type._tcp.local".
+ nsdManager.discoverServices(serviceType,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, baseTypeDiscoveryRecord)
+ nsdManager.discoverServices("_othersubtype.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, otherSubtypeDiscoveryRecord)
+ nsdManager.discoverServices("_subtype.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, subtypeDiscoveryRecord)
+
+ subtypeDiscoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ 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(subtypeDiscoveryRecord)
+ nsdManager.stopServiceDiscovery(otherSubtypeDiscoveryRecord)
+
+ baseTypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+ subtypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+ otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
/**
* Register a service and return its registration record.
*/
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index 0c4f794..ce789fc 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -426,7 +426,7 @@
.build();
}
- private void testHttpRequest(Socket s) throws IOException {
+ public void testHttpRequest(Socket s) throws IOException {
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
@@ -434,7 +434,9 @@
byte[] responseBytes = new byte[4096];
out.write(requestBytes);
in.read(responseBytes);
- assertTrue(new String(responseBytes, "UTF-8").startsWith("HTTP/1.0 204 No Content\r\n"));
+ final String response = new String(responseBytes, "UTF-8");
+ assertTrue("Received unexpected response: " + response,
+ response.startsWith("HTTP/1.0 204 No Content\r\n"));
}
private Socket getBoundSocket(Network network, String host, int port) throws IOException {
diff --git a/tests/cts/tethering/AndroidTestTemplate.xml b/tests/cts/tethering/AndroidTestTemplate.xml
index c842c09..9b33afe 100644
--- a/tests/cts/tethering/AndroidTestTemplate.xml
+++ b/tests/cts/tethering/AndroidTestTemplate.xml
@@ -25,7 +25,7 @@
<option name="cleanup-apks" value="true" />
<option name="test-file-name" value="{MODULE}.apk" />
</target_preparer>
- <target_preparer class="com.android.testutils.ConnectivityCheckTargetPreparer">
+ <target_preparer class="com.android.testutils.ConnectivityTestTargetPreparer">
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.tethering.cts" />
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index d189848..19fa41d 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -66,6 +66,7 @@
import android.os.Build;
import android.os.ServiceSpecificException;
import android.system.ErrnoException;
+import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import androidx.test.filters.SmallTest;
@@ -1151,4 +1152,33 @@
mCookieTagMap.updateEntry(new CookieTagMapKey(123), new CookieTagMapValue(456, 0x789));
assertDumpContains(getDump(), "cookie=123 tag=0x789 uid=456");
}
+
+ @Test
+ public void testGetUids() throws ErrnoException {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+ final long match0 = DOZABLE_MATCH | POWERSAVE_MATCH;
+ final long match1 = DOZABLE_MATCH | STANDBY_MATCH;
+ mUidOwnerMap.updateEntry(new S32(uid0), new UidOwnerValue(NULL_IIF, match0));
+ mUidOwnerMap.updateEntry(new S32(uid1), new UidOwnerValue(NULL_IIF, match1));
+
+ assertEquals(new ArraySet<>(List.of(uid0, uid1)),
+ mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(FIREWALL_CHAIN_DOZABLE));
+ assertEquals(new ArraySet<>(List.of(uid0)),
+ mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(FIREWALL_CHAIN_POWERSAVE));
+
+ assertEquals(new ArraySet<>(List.of(uid1)),
+ mBpfNetMaps.getUidsWithDenyRuleOnDenyListChain(FIREWALL_CHAIN_STANDBY));
+ assertEquals(new ArraySet<>(),
+ mBpfNetMaps.getUidsWithDenyRuleOnDenyListChain(FIREWALL_CHAIN_OEM_DENY_1));
+ }
+
+ @Test
+ public void testGetUidsIllegalArgument() {
+ final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
+ assertThrows(expected,
+ () -> mBpfNetMaps.getUidsWithDenyRuleOnDenyListChain(FIREWALL_CHAIN_DOZABLE));
+ assertThrows(expected,
+ () -> mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(FIREWALL_CHAIN_OEM_DENY_1));
+ }
}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 9d7b21f..e434649 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -387,6 +387,7 @@
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo;
import com.android.server.ConnectivityService.NetworkRequestInfo;
+import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.DestroySocketsWrapper;
import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
import com.android.server.connectivity.ApplicationSelfCertifiedNetworkCapabilities;
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker;
@@ -615,6 +616,7 @@
@Mock TetheringManager mTetheringManager;
@Mock BroadcastOptionsShim mBroadcastOptionsShim;
@Mock ActivityManager mActivityManager;
+ @Mock DestroySocketsWrapper mDestroySocketsWrapper;
// BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
// underlying binder calls.
@@ -1865,7 +1867,7 @@
final Context mockResContext = mock(Context.class);
doReturn(mResources).when(mockResContext).getResources();
ConnectivityResources.setResourcesContextForTest(mockResContext);
- mDeps = spy(new ConnectivityServiceDependencies(mockResContext));
+ mDeps = new ConnectivityServiceDependencies(mockResContext);
mAutoOnOffKeepaliveDependencies =
new AutomaticOnOffKeepaliveTrackerDependencies(mServiceContext);
mService = new ConnectivityService(mServiceContext,
@@ -1928,8 +1930,7 @@
R.integer.config_networkWakeupPacketMark);
}
- // ConnectivityServiceDependencies is public to use Mockito.spy
- public class ConnectivityServiceDependencies extends ConnectivityService.Dependencies {
+ class ConnectivityServiceDependencies extends ConnectivityService.Dependencies {
final ConnectivityResources mConnRes;
ConnectivityServiceDependencies(final Context mockResContext) {
@@ -2168,10 +2169,33 @@
}
}
- @Override
+ // Class to be mocked and used to verify destroy sockets methods call
+ public class DestroySocketsWrapper {
+ public void destroyLiveTcpSockets(final Set<Range<Integer>> ranges,
+ final Set<Integer> exemptUids){}
+ public void destroyLiveTcpSocketsByOwnerUids(final Set<Integer> ownerUids){}
+ }
+
+ @Override @SuppressWarnings("DirectInvocationOnMock")
public void destroyLiveTcpSockets(final Set<Range<Integer>> ranges,
final Set<Integer> exemptUids) {
- // This function is empty since the invocation of this method is verified by mocks
+ // Call mocked destroyLiveTcpSockets so that test can verify this method call
+ mDestroySocketsWrapper.destroyLiveTcpSockets(ranges, exemptUids);
+ }
+
+ @Override @SuppressWarnings("DirectInvocationOnMock")
+ public void destroyLiveTcpSocketsByOwnerUids(final Set<Integer> ownerUids) {
+ // Call mocked destroyLiveTcpSocketsByOwnerUids so that test can verify this method call
+ mDestroySocketsWrapper.destroyLiveTcpSocketsByOwnerUids(ownerUids);
+ }
+
+ final ArrayTrackRecord<Long>.ReadHead mScheduledEvaluationTimeouts =
+ new ArrayTrackRecord<Long>().newReadHead();
+ @Override
+ public void scheduleEvaluationTimeout(@NonNull Handler handler,
+ @NonNull final Network network, final long delayMs) {
+ mScheduledEvaluationTimeouts.add(delayMs);
+ super.scheduleEvaluationTimeout(handler, network, delayMs);
}
}
@@ -6037,10 +6061,13 @@
wifiCallback.assertNoCallback();
}
- public void doTestPreferBadWifi(final boolean preferBadWifi) throws Exception {
+ public void doTestPreferBadWifi(final boolean avoidBadWifi,
+ final boolean preferBadWifi,
+ @NonNull Predicate<Long> checkUnvalidationTimeout) throws Exception {
// Pretend we're on a carrier that restricts switching away from bad wifi, and
// depending on the parameter one that may indeed prefer bad wifi.
- doReturn(0).when(mResources).getInteger(R.integer.config_networkAvoidBadWifi);
+ doReturn(avoidBadWifi ? 1 : 0).when(mResources)
+ .getInteger(R.integer.config_networkAvoidBadWifi);
doReturn(preferBadWifi ? 1 : 0).when(mResources)
.getInteger(R.integer.config_activelyPreferBadWifi);
mPolicyTracker.reevaluate();
@@ -6062,7 +6089,9 @@
mWiFiAgent.connect(false);
wifiCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
- if (preferBadWifi) {
+ mDeps.mScheduledEvaluationTimeouts.poll(TIMEOUT_MS, t -> checkUnvalidationTimeout.test(t));
+
+ if (!avoidBadWifi && preferBadWifi) {
expectUnvalidationCheckWillNotify(mWiFiAgent, NotificationType.LOST_INTERNET);
mDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
} else {
@@ -6072,15 +6101,31 @@
}
@Test
- public void testPreferBadWifi_doNotPrefer() throws Exception {
+ public void testPreferBadWifi_doNotAvoid_doNotPrefer() throws Exception {
// Starting with U this mode is no longer supported and can't actually be tested
assumeFalse(SdkLevel.isAtLeastU());
- doTestPreferBadWifi(false /* preferBadWifi */);
+ doTestPreferBadWifi(false /* avoidBadWifi */, false /* preferBadWifi */,
+ timeout -> timeout < 14_000);
}
@Test
- public void testPreferBadWifi_doPrefer() throws Exception {
- doTestPreferBadWifi(true /* preferBadWifi */);
+ public void testPreferBadWifi_doNotAvoid_doPrefer() throws Exception {
+ doTestPreferBadWifi(false /* avoidBadWifi */, true /* preferBadWifi */,
+ timeout -> timeout > 14_000);
+ }
+
+ @Test
+ public void testPreferBadWifi_doAvoid_doNotPrefer() throws Exception {
+ // If avoidBadWifi=true, then preferBadWifi should be irrelevant. Test anyway.
+ doTestPreferBadWifi(true /* avoidBadWifi */, false /* preferBadWifi */,
+ timeout -> timeout < 14_000);
+ }
+
+ @Test
+ public void testPreferBadWifi_doAvoid_doPrefer() throws Exception {
+ // If avoidBadWifi=true, then preferBadWifi should be irrelevant. Test anyway.
+ doTestPreferBadWifi(true /* avoidBadWifi */, true /* preferBadWifi */,
+ timeout -> timeout < 14_000);
}
@Test
@@ -10269,6 +10314,50 @@
}
}
+ private void doTestSetFirewallChainEnabledCloseSocket(final int chain,
+ final boolean isAllowList) throws Exception {
+ reset(mDestroySocketsWrapper);
+
+ mCm.setFirewallChainEnabled(chain, true /* enabled */);
+ final Set<Integer> uids =
+ new ArraySet<>(List.of(TEST_PACKAGE_UID, TEST_PACKAGE_UID2));
+ if (isAllowList) {
+ final Set<Range<Integer>> range = new ArraySet<>(
+ List.of(new Range<>(Process.FIRST_APPLICATION_UID, Integer.MAX_VALUE)));
+ verify(mDestroySocketsWrapper).destroyLiveTcpSockets(range, uids);
+ } else {
+ verify(mDestroySocketsWrapper).destroyLiveTcpSocketsByOwnerUids(uids);
+ }
+
+ mCm.setFirewallChainEnabled(chain, false /* enabled */);
+ verifyNoMoreInteractions(mDestroySocketsWrapper);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testSetFirewallChainEnabledCloseSocket() throws Exception {
+ doReturn(new ArraySet<>(Arrays.asList(TEST_PACKAGE_UID, TEST_PACKAGE_UID2)))
+ .when(mBpfNetMaps)
+ .getUidsWithDenyRuleOnDenyListChain(anyInt());
+ doReturn(new ArraySet<>(Arrays.asList(TEST_PACKAGE_UID, TEST_PACKAGE_UID2)))
+ .when(mBpfNetMaps)
+ .getUidsWithAllowRuleOnAllowListChain(anyInt());
+
+ final boolean allowlist = true;
+ final boolean denylist = false;
+
+ doReturn(true).when(mBpfNetMaps).isFirewallAllowList(anyInt());
+ doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_DOZABLE, allowlist);
+ doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_POWERSAVE, allowlist);
+ doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_RESTRICTED, allowlist);
+ doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_LOW_POWER_STANDBY, allowlist);
+
+ doReturn(false).when(mBpfNetMaps).isFirewallAllowList(anyInt());
+ doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_STANDBY, denylist);
+ doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_OEM_DENY_1, denylist);
+ doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_OEM_DENY_2, denylist);
+ doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_OEM_DENY_3, denylist);
+ }
+
private void doTestReplaceFirewallChain(final int chain) {
final int[] uids = new int[] {1001, 1002};
mCm.replaceFirewallChain(chain, uids);
@@ -12578,11 +12667,11 @@
private void assertVpnUidRangesUpdated(boolean add, Set<UidRange> vpnRanges, int exemptUid)
throws Exception {
- InOrder inOrder = inOrder(mMockNetd, mDeps);
+ InOrder inOrder = inOrder(mMockNetd, mDestroySocketsWrapper);
final Set<Integer> exemptUidSet = new ArraySet<>(List.of(exemptUid, Process.VPN_UID));
- inOrder.verify(mDeps).destroyLiveTcpSockets(UidRange.toIntRanges(vpnRanges),
- exemptUidSet);
+ inOrder.verify(mDestroySocketsWrapper).destroyLiveTcpSockets(
+ UidRange.toIntRanges(vpnRanges), exemptUidSet);
if (add) {
inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(
@@ -12594,8 +12683,8 @@
toUidRangeStableParcels(vpnRanges), PREFERENCE_ORDER_VPN));
}
- inOrder.verify(mDeps).destroyLiveTcpSockets(UidRange.toIntRanges(vpnRanges),
- exemptUidSet);
+ inOrder.verify(mDestroySocketsWrapper).destroyLiveTcpSockets(
+ UidRange.toIntRanges(vpnRanges), exemptUidSet);
}
@Test
@@ -17935,7 +18024,36 @@
final UidRange frozenUidRange = new UidRange(TEST_FROZEN_UID, TEST_FROZEN_UID);
final Set<UidRange> ranges = Collections.singleton(frozenUidRange);
- verify(mDeps).destroyLiveTcpSockets(eq(UidRange.toIntRanges(ranges)),
+ verify(mDestroySocketsWrapper).destroyLiveTcpSockets(eq(UidRange.toIntRanges(ranges)),
eq(exemptUids));
}
+
+ @Test
+ public void testDisconnectSuspendedNetworkStopClatd() throws Exception {
+ final TestNetworkCallback networkCallback = new TestNetworkCallback();
+ final NetworkRequest networkRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_DUN)
+ .build();
+ mCm.requestNetwork(networkRequest, networkCallback);
+
+ final IpPrefix nat64Prefix = new IpPrefix(InetAddress.getByName("64:ff9b::"), 96);
+ NetworkCapabilities nc = new NetworkCapabilities().addCapability(NET_CAPABILITY_DUN);
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(MOBILE_IFNAME);
+ lp.addLinkAddress(new LinkAddress("2001:db8:1::1/64"));
+ lp.setNat64Prefix(nat64Prefix);
+ mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, lp, nc);
+ mCellAgent.connect(true /* validated */, false /* hasInternet */,
+ false /* privateDnsProbeSent */);
+
+ verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, mCellAgent.getNetwork().netId,
+ nat64Prefix.toString());
+
+ mCellAgent.suspend();
+ mCm.unregisterNetworkCallback(networkCallback);
+ mCellAgent.expectDisconnected();
+ waitForIdle();
+
+ verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
+ }
}
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 14b4280..955be12 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -23,7 +23,7 @@
import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
-import static com.android.server.NsdService.constructServiceType;
+import static com.android.server.NsdService.parseTypeAndSubtype;
import static com.android.testutils.ContextUtils.mockService;
import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
@@ -77,6 +77,7 @@
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
+import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
@@ -84,6 +85,7 @@
import com.android.server.NsdService.Dependencies;
import com.android.server.connectivity.mdns.MdnsAdvertiser;
import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
+import com.android.server.connectivity.mdns.MdnsSearchOptions;
import com.android.server.connectivity.mdns.MdnsServiceBrowserListener;
import com.android.server.connectivity.mdns.MdnsServiceInfo;
import com.android.server.connectivity.mdns.MdnsSocketProvider;
@@ -104,6 +106,7 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
@@ -175,10 +178,10 @@
doReturn(true).when(mMockMDnsM).resolve(
anyInt(), anyString(), anyString(), anyString(), anyInt());
doReturn(false).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class));
- doReturn(mDiscoveryManager).when(mDeps).makeMdnsDiscoveryManager(any(), any(), any());
+ doReturn(mDiscoveryManager).when(mDeps)
+ .makeMdnsDiscoveryManager(any(), any(), any(), any());
doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any(), any());
doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any(), any());
-
mService = makeService();
}
@@ -908,7 +911,8 @@
listener.onServiceNameDiscovered(foundInfo);
verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(argThat(info ->
info.getServiceName().equals(SERVICE_NAME)
- && info.getServiceType().equals(SERVICE_TYPE)
+ // Service type in discovery callbacks has a dot at the end
+ && info.getServiceType().equals(SERVICE_TYPE + ".")
&& info.getNetwork().equals(network)));
final MdnsServiceInfo removedInfo = new MdnsServiceInfo(
@@ -927,7 +931,8 @@
listener.onServiceNameRemoved(removedInfo);
verify(discListener, timeout(TIMEOUT_MS)).onServiceLost(argThat(info ->
info.getServiceName().equals(SERVICE_NAME)
- && info.getServiceType().equals(SERVICE_TYPE)
+ // Service type in discovery callbacks has a dot at the end
+ && info.getServiceType().equals(SERVICE_TYPE + ".")
&& info.getNetwork().equals(network)));
client.stopServiceDiscovery(discListener);
@@ -967,6 +972,34 @@
}
@Test
+ @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ public void testDiscoveryWithMdnsDiscoveryManager_UsesSubtypes() {
+ final String typeWithSubtype = SERVICE_TYPE + ",_subtype";
+ final NsdManager client = connectClient(mService);
+ final NsdServiceInfo regInfo = new NsdServiceInfo("Instance", typeWithSubtype);
+ final Network network = new Network(999);
+ regInfo.setHostAddresses(List.of(parseNumericAddress("192.0.2.123")));
+ regInfo.setPort(12345);
+ regInfo.setNetwork(network);
+
+ final RegistrationListener regListener = mock(RegistrationListener.class);
+ client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
+ waitForIdle();
+ verify(mAdvertiser).addService(anyInt(), argThat(s ->
+ "Instance".equals(s.getServiceName())
+ && SERVICE_TYPE.equals(s.getServiceType())), eq("_subtype"));
+
+ final DiscoveryListener discListener = mock(DiscoveryListener.class);
+ client.discoverServices(typeWithSubtype, PROTOCOL, network, Runnable::run, discListener);
+ waitForIdle();
+ final ArgumentCaptor<MdnsSearchOptions> optionsCaptor =
+ ArgumentCaptor.forClass(MdnsSearchOptions.class);
+ verify(mDiscoveryManager).registerListener(eq(SERVICE_TYPE + ".local"), any(),
+ optionsCaptor.capture());
+ assertEquals(Collections.singletonList("subtype"), optionsCaptor.getValue().getSubtypes());
+ }
+
+ @Test
public void testResolutionWithMdnsDiscoveryManager() throws UnknownHostException {
setMdnsDiscoveryManagerEnabled();
@@ -974,7 +1007,7 @@
final ResolveListener resolveListener = mock(ResolveListener.class);
final Network network = new Network(999);
final String serviceType = "_nsd._service._tcp";
- final String constructedServiceType = "_nsd._sub._service._tcp.local";
+ final String constructedServiceType = "_service._tcp.local";
final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, serviceType);
@@ -982,10 +1015,14 @@
client.resolveService(request, resolveListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
+ final ArgumentCaptor<MdnsSearchOptions> optionsCaptor =
+ ArgumentCaptor.forClass(MdnsSearchOptions.class);
verify(mDiscoveryManager).registerListener(eq(constructedServiceType),
- listenerCaptor.capture(), argThat(options ->
- network.equals(options.getNetwork())
- && SERVICE_NAME.equals(options.getResolveInstanceName())));
+ listenerCaptor.capture(),
+ optionsCaptor.capture());
+ assertEquals(network, optionsCaptor.getValue().getNetwork());
+ // Subtypes are not used for resolution, only for discovery
+ assertEquals(Collections.emptyList(), optionsCaptor.getValue().getSubtypes());
final MdnsServiceBrowserListener listener = listenerCaptor.getValue();
final MdnsServiceInfo mdnsServiceInfo = new MdnsServiceInfo(
@@ -1009,7 +1046,7 @@
verify(resolveListener, timeout(TIMEOUT_MS)).onServiceResolved(infoCaptor.capture());
final NsdServiceInfo info = infoCaptor.getValue();
assertEquals(SERVICE_NAME, info.getServiceName());
- assertEquals("." + serviceType, info.getServiceType());
+ assertEquals("._service._tcp", info.getServiceType());
assertEquals(PORT, info.getPort());
assertTrue(info.getAttributes().containsKey("key"));
assertEquals(1, info.getAttributes().size());
@@ -1052,7 +1089,7 @@
final ArgumentCaptor<Integer> serviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addService(serviceIdCaptor.capture(),
- argThat(info -> matches(info, regInfo)));
+ argThat(info -> matches(info, regInfo)), eq(null) /* subtype */);
client.unregisterService(regListenerWithoutFeature);
waitForIdle();
@@ -1109,8 +1146,10 @@
waitForIdle();
// The advertiser is enabled for _type2 but not _type1
- verify(mAdvertiser, never()).addService(anyInt(), argThat(info -> matches(info, service1)));
- verify(mAdvertiser).addService(anyInt(), argThat(info -> matches(info, service2)));
+ verify(mAdvertiser, never()).addService(
+ anyInt(), argThat(info -> matches(info, service1)), eq(null) /* subtype */);
+ verify(mAdvertiser).addService(
+ anyInt(), argThat(info -> matches(info, service2)), eq(null) /* subtype */);
}
@Test
@@ -1135,7 +1174,7 @@
verify(mSocketProvider).startMonitoringSockets();
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addService(idCaptor.capture(), argThat(info ->
- matches(info, regInfo)));
+ matches(info, regInfo)), eq(null) /* subtype */);
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1171,7 +1210,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
- verify(mAdvertiser, never()).addService(anyInt(), any());
+ verify(mAdvertiser, never()).addService(anyInt(), any(), any());
verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
@@ -1199,7 +1238,8 @@
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
// Service name is truncated to 63 characters
verify(mAdvertiser).addService(idCaptor.capture(),
- argThat(info -> info.getServiceName().equals("a".repeat(63))));
+ argThat(info -> info.getServiceName().equals("a".repeat(63))),
+ eq(null) /* subtype */);
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1217,7 +1257,7 @@
final ResolveListener resolveListener = mock(ResolveListener.class);
final Network network = new Network(999);
final String serviceType = "_nsd._service._tcp";
- final String constructedServiceType = "_nsd._sub._service._tcp.local";
+ final String constructedServiceType = "_service._tcp.local";
final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, serviceType);
@@ -1225,8 +1265,14 @@
client.resolveService(request, resolveListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
+ final ArgumentCaptor<MdnsSearchOptions> optionsCaptor =
+ ArgumentCaptor.forClass(MdnsSearchOptions.class);
verify(mDiscoveryManager).registerListener(eq(constructedServiceType),
- listenerCaptor.capture(), argThat(options -> network.equals(options.getNetwork())));
+ listenerCaptor.capture(),
+ optionsCaptor.capture());
+ assertEquals(network, optionsCaptor.getValue().getNetwork());
+ // Subtypes are not used for resolution, only for discovery
+ assertEquals(Collections.emptyList(), optionsCaptor.getValue().getSubtypes());
client.stopServiceResolution(resolveListener);
waitForIdle();
@@ -1241,16 +1287,22 @@
}
@Test
- public void testConstructServiceType() {
+ public void testParseTypeAndSubtype() {
final String serviceType1 = "test._tcp";
final String serviceType2 = "_test._quic";
- final String serviceType3 = "_123._udp.";
- final String serviceType4 = "_TEST._999._tcp.";
+ final String serviceType3 = "_test._quic,_test1,_test2";
+ final String serviceType4 = "_123._udp.";
+ final String serviceType5 = "_TEST._999._tcp.";
+ final String serviceType6 = "_998._tcp.,_TEST";
+ final String serviceType7 = "_997._tcp,_TEST";
- assertEquals(null, constructServiceType(serviceType1));
- assertEquals(null, constructServiceType(serviceType2));
- assertEquals("_123._udp", constructServiceType(serviceType3));
- assertEquals("_TEST._sub._999._tcp", constructServiceType(serviceType4));
+ 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));
}
@Test
@@ -1269,7 +1321,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
- verify(mAdvertiser).addService(anyInt(), any());
+ verify(mAdvertiser).addService(anyInt(), any(), any());
// Verify the discovery uses MdnsDiscoveryManager
final DiscoveryListener discListener = mock(DiscoveryListener.class);
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 3bb08a6..d9acc61 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -51,12 +51,14 @@
private const val SERVICE_ID_2 = 2
private const val LONG_SERVICE_ID_1 = 3
private const val LONG_SERVICE_ID_2 = 4
+private const val CASE_INSENSITIVE_TEST_SERVICE_ID = 5
private const val TIMEOUT_MS = 10_000L
private val TEST_ADDR = parseNumericAddress("2001:db8::123")
private val TEST_LINKADDR = LinkAddress(TEST_ADDR, 64 /* prefixLength */)
private val TEST_NETWORK_1 = mock(Network::class.java)
private val TEST_NETWORK_2 = mock(Network::class.java)
private val TEST_HOSTNAME = arrayOf("Android_test", "local")
+private const val TEST_SUBTYPE = "_subtype"
private val SERVICE_1 = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
port = 12345
@@ -77,6 +79,13 @@
network = null
}
+private val ALL_NETWORKS_SERVICE_2 =
+ NsdServiceInfo("TESTSERVICENAME", "_ADVERTISERTEST._tcp").apply {
+ port = 12345
+ hostAddresses = listOf(TEST_ADDR)
+ network = null
+ }
+
private val LONG_ALL_NETWORKS_SERVICE =
NsdServiceInfo("a".repeat(48) + "TestServiceName", "_longadvertisertest._tcp").apply {
port = 12345
@@ -130,7 +139,7 @@
@Test
fun testAddService_OneNetwork() {
val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog)
- postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1) }
+ postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1, null /* subtype */) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -161,7 +170,7 @@
@Test
fun testAddService_AllNetworks() {
val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog)
- postSync { advertiser.addService(SERVICE_ID_1, ALL_NETWORKS_SERVICE) }
+ postSync { advertiser.addService(SERVICE_ID_1, ALL_NETWORKS_SERVICE, TEST_SUBTYPE) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE.network),
@@ -179,6 +188,10 @@
verify(mockDeps).makeAdvertiser(eq(mockSocket2), eq(listOf(TEST_LINKADDR)),
eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any()
)
+ verify(mockInterfaceAdvertiser1).addService(
+ anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+ verify(mockInterfaceAdvertiser2).addService(
+ anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor1.value.onRegisterServiceSucceeded(
@@ -207,20 +220,24 @@
@Test
fun testAddService_Conflicts() {
val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog)
- postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1) }
+ postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1, null /* subtype */) }
val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
val oneNetSocketCb = oneNetSocketCbCaptor.value
// Register a service with the same name on all networks (name conflict)
- postSync { advertiser.addService(SERVICE_ID_2, ALL_NETWORKS_SERVICE) }
+ postSync { advertiser.addService(SERVICE_ID_2, ALL_NETWORKS_SERVICE, null /* subtype */) }
val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
val allNetSocketCb = allNetSocketCbCaptor.value
- postSync { advertiser.addService(LONG_SERVICE_ID_1, LONG_SERVICE_1) }
- postSync { advertiser.addService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE) }
+ postSync { advertiser.addService(LONG_SERVICE_ID_1, LONG_SERVICE_1, null /* subtype */) }
+ postSync { advertiser.addService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
+ null /* subtype */) }
+
+ postSync { advertiser.addService(CASE_INSENSITIVE_TEST_SERVICE_ID, ALL_NETWORKS_SERVICE_2,
+ null /* subtype */) }
// Callbacks for matching network and all networks both get the socket
postSync {
@@ -243,18 +260,28 @@
network = LONG_ALL_NETWORKS_SERVICE.network
}
+ val expectedCaseInsensitiveRenamed = NsdServiceInfo(
+ "${ALL_NETWORKS_SERVICE_2.serviceName} (3)", ALL_NETWORKS_SERVICE_2.serviceType
+ ).apply {
+ port = ALL_NETWORKS_SERVICE_2.port
+ hostAddresses = ALL_NETWORKS_SERVICE_2.hostAddresses
+ network = ALL_NETWORKS_SERVICE_2.network
+ }
+
val intAdvCbCaptor = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any()
)
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(SERVICE_1) })
+ argThat { it.matches(SERVICE_1) }, eq(null))
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
- argThat { it.matches(expectedRenamed) })
+ argThat { it.matches(expectedRenamed) }, eq(null))
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
- argThat { it.matches(LONG_SERVICE_1) })
+ argThat { it.matches(LONG_SERVICE_1) }, eq(null))
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
- argThat { it.matches(expectedLongRenamed) })
+ argThat { it.matches(expectedLongRenamed) }, eq(null))
+ verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
+ argThat { it.matches(expectedCaseInsensitiveRenamed) }, eq(null))
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onRegisterServiceSucceeded(
@@ -278,7 +305,7 @@
fun testRemoveService_whenAllServiceRemoved_thenUpdateHostName() {
val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog)
verify(mockDeps, times(1)).generateHostname()
- postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1) }
+ postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1, null /* subtype */) }
postSync { advertiser.removeService(SERVICE_ID_1) }
verify(mockDeps, times(2)).generateHostname()
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index 63357f1..89776e2 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -27,6 +27,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.Network;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.text.TextUtils;
import android.util.Pair;
@@ -34,7 +36,9 @@
import com.android.server.connectivity.mdns.MdnsSocketClientBase.SocketCreationCallback;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -53,7 +57,7 @@
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MdnsDiscoveryManagerTests {
-
+ private static final long DEFAULT_TIMEOUT = 2000L;
private static final String SERVICE_TYPE_1 = "_googlecast._tcp.local";
private static final String SERVICE_TYPE_2 = "_test._tcp.local";
private static final Network NETWORK_1 = Mockito.mock(Network.class);
@@ -78,12 +82,18 @@
@Mock MdnsServiceBrowserListener mockListenerTwo;
@Mock SharedLog sharedLog;
private MdnsDiscoveryManager discoveryManager;
+ private HandlerThread thread;
+ private Handler handler;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog) {
+ thread = new HandlerThread("MdnsDiscoveryManagerTests");
+ thread.start();
+ handler = new Handler(thread.getLooper());
+ discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog,
+ thread.getLooper()) {
@Override
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
@Nullable Network network) {
@@ -103,11 +113,23 @@
};
}
+ @After
+ public void tearDown() {
+ if (thread != null) {
+ thread.quitSafely();
+ }
+ }
+
+ private void runOnHandler(Runnable r) {
+ handler.post(r);
+ HandlerUtils.waitForIdle(handler, DEFAULT_TIMEOUT);
+ }
+
private SocketCreationCallback expectSocketCreationCallback(String serviceType,
MdnsServiceBrowserListener listener, MdnsSearchOptions options) throws IOException {
final ArgumentCaptor<SocketCreationCallback> callbackCaptor =
ArgumentCaptor.forClass(SocketCreationCallback.class);
- discoveryManager.registerListener(serviceType, listener, options);
+ runOnHandler(() -> discoveryManager.registerListener(serviceType, listener, options));
verify(socketClient).startDiscovery();
verify(socketClient).notifyNetworkRequested(
eq(listener), eq(options.getNetwork()), callbackCaptor.capture());
@@ -120,11 +142,11 @@
MdnsSearchOptions.newBuilder().setNetwork(null /* network */).build();
final SocketCreationCallback callback = expectSocketCreationCallback(
SERVICE_TYPE_1, mockListenerOne, options);
- callback.onSocketCreated(null /* network */);
+ runOnHandler(() -> callback.onSocketCreated(null /* network */));
verify(mockServiceTypeClientOne).startSendAndReceive(mockListenerOne, options);
when(mockServiceTypeClientOne.stopSendAndReceive(mockListenerOne)).thenReturn(true);
- discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne);
+ runOnHandler(() -> discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne));
verify(mockServiceTypeClientOne).stopSendAndReceive(mockListenerOne);
verify(socketClient).stopDiscovery();
}
@@ -135,16 +157,16 @@
MdnsSearchOptions.newBuilder().setNetwork(null /* network */).build();
final SocketCreationCallback callback = expectSocketCreationCallback(
SERVICE_TYPE_1, mockListenerOne, options);
- callback.onSocketCreated(null /* network */);
+ runOnHandler(() -> callback.onSocketCreated(null /* network */));
verify(mockServiceTypeClientOne).startSendAndReceive(mockListenerOne, options);
- callback.onSocketCreated(NETWORK_1);
+ runOnHandler(() -> callback.onSocketCreated(NETWORK_1));
verify(mockServiceTypeClientOne1).startSendAndReceive(mockListenerOne, options);
final SocketCreationCallback callback2 = expectSocketCreationCallback(
SERVICE_TYPE_2, mockListenerTwo, options);
- callback2.onSocketCreated(null /* network */);
+ runOnHandler(() -> callback2.onSocketCreated(null /* network */));
verify(mockServiceTypeClientTwo).startSendAndReceive(mockListenerTwo, options);
- callback2.onSocketCreated(NETWORK_2);
+ runOnHandler(() -> callback2.onSocketCreated(NETWORK_2));
verify(mockServiceTypeClientTwo2).startSendAndReceive(mockListenerTwo, options);
}
@@ -154,21 +176,22 @@
MdnsSearchOptions.newBuilder().setNetwork(null /* network */).build();
final SocketCreationCallback callback = expectSocketCreationCallback(
SERVICE_TYPE_1, mockListenerOne, options1);
- callback.onSocketCreated(null /* network */);
+ runOnHandler(() -> callback.onSocketCreated(null /* network */));
verify(mockServiceTypeClientOne).startSendAndReceive(mockListenerOne, options1);
- callback.onSocketCreated(NETWORK_1);
+ runOnHandler(() -> callback.onSocketCreated(NETWORK_1));
verify(mockServiceTypeClientOne1).startSendAndReceive(mockListenerOne, options1);
final MdnsSearchOptions options2 =
MdnsSearchOptions.newBuilder().setNetwork(NETWORK_2).build();
final SocketCreationCallback callback2 = expectSocketCreationCallback(
SERVICE_TYPE_2, mockListenerTwo, options2);
- callback2.onSocketCreated(NETWORK_2);
+ runOnHandler(() -> callback2.onSocketCreated(NETWORK_2));
verify(mockServiceTypeClientTwo2).startSendAndReceive(mockListenerTwo, options2);
final MdnsPacket responseForServiceTypeOne = createMdnsPacket(SERVICE_TYPE_1);
final int ifIndex = 1;
- discoveryManager.onResponseReceived(responseForServiceTypeOne, ifIndex, null /* network */);
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ responseForServiceTypeOne, ifIndex, null /* network */));
verify(mockServiceTypeClientOne).processResponse(responseForServiceTypeOne, ifIndex,
null /* network */);
verify(mockServiceTypeClientOne1).processResponse(responseForServiceTypeOne, ifIndex,
@@ -177,7 +200,8 @@
null /* network */);
final MdnsPacket responseForServiceTypeTwo = createMdnsPacket(SERVICE_TYPE_2);
- discoveryManager.onResponseReceived(responseForServiceTypeTwo, ifIndex, NETWORK_1);
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ responseForServiceTypeTwo, ifIndex, NETWORK_1));
verify(mockServiceTypeClientOne).processResponse(responseForServiceTypeTwo, ifIndex,
NETWORK_1);
verify(mockServiceTypeClientOne1).processResponse(responseForServiceTypeTwo, ifIndex,
@@ -187,7 +211,8 @@
final MdnsPacket responseForSubtype =
createMdnsPacket("subtype._sub._googlecast._tcp.local");
- discoveryManager.onResponseReceived(responseForSubtype, ifIndex, NETWORK_2);
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ responseForSubtype, ifIndex, NETWORK_2));
verify(mockServiceTypeClientOne).processResponse(responseForSubtype, ifIndex, NETWORK_2);
verify(mockServiceTypeClientOne1, never()).processResponse(
responseForSubtype, ifIndex, NETWORK_2);
@@ -201,7 +226,7 @@
MdnsSearchOptions.newBuilder().setNetwork(NETWORK_1).build();
final SocketCreationCallback callback = expectSocketCreationCallback(
SERVICE_TYPE_1, mockListenerOne, options1);
- callback.onSocketCreated(NETWORK_1);
+ runOnHandler(() -> callback.onSocketCreated(NETWORK_1));
verify(mockServiceTypeClientOne1).startSendAndReceive(mockListenerOne, options1);
// Create a ServiceTypeClient for SERVICE_TYPE_2 and NETWORK_2
@@ -209,26 +234,28 @@
MdnsSearchOptions.newBuilder().setNetwork(NETWORK_2).build();
final SocketCreationCallback callback2 = expectSocketCreationCallback(
SERVICE_TYPE_2, mockListenerTwo, options2);
- callback2.onSocketCreated(NETWORK_2);
+ runOnHandler(() -> callback2.onSocketCreated(NETWORK_2));
verify(mockServiceTypeClientTwo2).startSendAndReceive(mockListenerTwo, options2);
// Receive a response, it should be processed on both clients.
final MdnsPacket response = createMdnsPacket(SERVICE_TYPE_1);
final int ifIndex = 1;
- discoveryManager.onResponseReceived(response, ifIndex, null /* network */);
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ response, ifIndex, null /* network */));
verify(mockServiceTypeClientOne1).processResponse(response, ifIndex, null /* network */);
verify(mockServiceTypeClientTwo2).processResponse(response, ifIndex, null /* network */);
// The client for NETWORK_1 receives the callback that the NETWORK_1 has been destroyed,
// mockServiceTypeClientOne1 should send service removed notifications and remove from the
// list of clients.
- callback.onSocketDestroyed(NETWORK_1);
- verify(mockServiceTypeClientOne1).notifyAllServicesRemoved();
+ runOnHandler(() -> callback.onAllSocketsDestroyed(NETWORK_1));
+ verify(mockServiceTypeClientOne1).notifySocketDestroyed();
// Receive a response again, it should be processed only on mockServiceTypeClientTwo2.
// Because the mockServiceTypeClientOne1 is removed from the list of clients, it is no
// longer able to process responses.
- discoveryManager.onResponseReceived(response, ifIndex, null /* network */);
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ response, ifIndex, null /* network */));
verify(mockServiceTypeClientOne1, times(1))
.processResponse(response, ifIndex, null /* network */);
verify(mockServiceTypeClientTwo2, times(2))
@@ -236,12 +263,13 @@
// The client for NETWORK_2 receives the callback that the NETWORK_1 has been destroyed,
// mockServiceTypeClientTwo2 shouldn't send any notifications.
- callback2.onSocketDestroyed(NETWORK_1);
- verify(mockServiceTypeClientTwo2, never()).notifyAllServicesRemoved();
+ runOnHandler(() -> callback2.onAllSocketsDestroyed(NETWORK_1));
+ verify(mockServiceTypeClientTwo2, never()).notifySocketDestroyed();
// Receive a response again, mockServiceTypeClientTwo2 is still in the list of clients, it's
// still able to process responses.
- discoveryManager.onResponseReceived(response, ifIndex, null /* network */);
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ response, ifIndex, null /* network */));
verify(mockServiceTypeClientOne1, times(1))
.processResponse(response, ifIndex, null /* network */);
verify(mockServiceTypeClientTwo2, times(3))
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 ee190af..dd458b8 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -117,7 +117,7 @@
knownServices.add(inv.getArgument(0))
-1
- }.`when`(repository).addService(anyInt(), any())
+ }.`when`(repository).addService(anyInt(), any(), any())
doAnswer { inv ->
knownServices.remove(inv.getArgument(0))
null
@@ -278,8 +278,8 @@
doReturn(serviceId).`when`(testProbingInfo).serviceId
doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId)
- advertiser.addService(serviceId, serviceInfo)
- verify(repository).addService(serviceId, serviceInfo)
+ advertiser.addService(serviceId, serviceInfo, null /* subtype */)
+ verify(repository).addService(serviceId, serviceInfo, null /* subtype */)
verify(prober).startProbing(testProbingInfo)
// Simulate probing success: continues to announcing
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index 90c43e5..c6137c6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -24,8 +24,12 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.net.InetAddresses;
import android.net.Network;
@@ -77,12 +81,17 @@
}
private SocketCallback expectSocketCallback() {
+ return expectSocketCallback(mListener, mNetwork);
+ }
+
+ private SocketCallback expectSocketCallback(MdnsServiceBrowserListener listener,
+ Network requestedNetwork) {
final ArgumentCaptor<SocketCallback> callbackCaptor =
ArgumentCaptor.forClass(SocketCallback.class);
mHandler.post(() -> mSocketClient.notifyNetworkRequested(
- mListener, mNetwork, mSocketCreationCallback));
+ listener, requestedNetwork, mSocketCreationCallback));
verify(mProvider, timeout(DEFAULT_TIMEOUT))
- .requestSocket(eq(mNetwork), callbackCaptor.capture());
+ .requestSocket(eq(requestedNetwork), callbackCaptor.capture());
return callbackCaptor.getValue();
}
@@ -169,4 +178,103 @@
new String[] { "Android", "local" } /* serviceHost */)
), response.answers);
}
+
+ @Test
+ public void testSocketRemovedAfterNetworkUnrequested() throws IOException {
+ // Request a socket
+ final SocketCallback callback = expectSocketCallback(mListener, mNetwork);
+ final DatagramPacket ipv4Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
+ InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
+ doReturn(true).when(mSocket).hasJoinedIpv4();
+ doReturn(true).when(mSocket).hasJoinedIpv6();
+ doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+ // Notify socket created
+ callback.onSocketCreated(mNetwork, mSocket, List.of());
+ verify(mSocketCreationCallback).onSocketCreated(mNetwork);
+
+ // Send IPv4 packet and verify sending has been called.
+ mSocketClient.sendMulticastPacket(ipv4Packet);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ verify(mSocket).send(ipv4Packet);
+
+ // Request another socket with null network
+ final MdnsServiceBrowserListener listener2 = mock(MdnsServiceBrowserListener.class);
+ final Network network2 = mock(Network.class);
+ final MdnsInterfaceSocket socket2 = mock(MdnsInterfaceSocket.class);
+ final SocketCallback callback2 = expectSocketCallback(listener2, null);
+ doReturn(true).when(socket2).hasJoinedIpv4();
+ doReturn(true).when(socket2).hasJoinedIpv6();
+ doReturn(createEmptyNetworkInterface()).when(socket2).getInterface();
+ // Notify socket created for two networks.
+ callback2.onSocketCreated(mNetwork, mSocket, List.of());
+ callback2.onSocketCreated(network2, socket2, List.of());
+ verify(mSocketCreationCallback, times(2)).onSocketCreated(mNetwork);
+ verify(mSocketCreationCallback).onSocketCreated(network2);
+
+ // Send IPv4 packet and verify sending to two sockets.
+ mSocketClient.sendMulticastPacket(ipv4Packet);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ verify(mSocket, times(2)).send(ipv4Packet);
+ verify(socket2).send(ipv4Packet);
+
+ // Unrequest another socket
+ mHandler.post(() -> mSocketClient.notifyNetworkUnrequested(listener2));
+ verify(mProvider, timeout(DEFAULT_TIMEOUT)).unrequestSocket(callback2);
+
+ // Send IPv4 packet again and verify only sending via mSocket
+ mSocketClient.sendMulticastPacket(ipv4Packet);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ verify(mSocket, times(3)).send(ipv4Packet);
+ verify(socket2).send(ipv4Packet);
+
+ // Unrequest remaining socket
+ mHandler.post(() -> mSocketClient.notifyNetworkUnrequested(mListener));
+ verify(mProvider, timeout(DEFAULT_TIMEOUT)).unrequestSocket(callback);
+
+ // Send IPv4 packet and verify no more sending.
+ mSocketClient.sendMulticastPacket(ipv4Packet);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ verify(mSocket, times(3)).send(ipv4Packet);
+ verify(socket2).send(ipv4Packet);
+ }
+
+ @Test
+ public void testNotifyNetworkUnrequested_SocketsOnNullNetwork() {
+ final MdnsInterfaceSocket otherSocket = mock(MdnsInterfaceSocket.class);
+ final SocketCallback callback = expectSocketCallback(
+ mListener, null /* requestedNetwork */);
+ doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+ doReturn(createEmptyNetworkInterface()).when(otherSocket).getInterface();
+
+ callback.onSocketCreated(null /* network */, mSocket, List.of());
+ verify(mSocketCreationCallback).onSocketCreated(null);
+ callback.onSocketCreated(null /* network */, otherSocket, List.of());
+ verify(mSocketCreationCallback, times(2)).onSocketCreated(null);
+
+ verify(mSocketCreationCallback, never()).onAllSocketsDestroyed(null /* network */);
+ mHandler.post(() -> mSocketClient.notifyNetworkUnrequested(mListener));
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+
+ verify(mProvider).unrequestSocket(callback);
+ verify(mSocketCreationCallback).onAllSocketsDestroyed(null /* network */);
+ }
+
+ @Test
+ public void testSocketCreatedAndDestroyed_NullNetwork() throws IOException {
+ final MdnsInterfaceSocket otherSocket = mock(MdnsInterfaceSocket.class);
+ final SocketCallback callback = expectSocketCallback(mListener, null /* network */);
+ doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+ doReturn(createEmptyNetworkInterface()).when(otherSocket).getInterface();
+
+ callback.onSocketCreated(null /* network */, mSocket, List.of());
+ verify(mSocketCreationCallback).onSocketCreated(null);
+ callback.onSocketCreated(null /* network */, otherSocket, List.of());
+ verify(mSocketCreationCallback, times(2)).onSocketCreated(null);
+
+ // Notify socket destroyed
+ callback.onInterfaceDestroyed(null /* network */, mSocket);
+ verifyNoMoreInteractions(mSocketCreationCallback);
+ callback.onInterfaceDestroyed(null /* network */, otherSocket);
+ verify(mSocketCreationCallback).onAllSocketsDestroyed(null /* network */);
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketWriterTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketWriterTest.kt
index 5c9c294..a545373 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketWriterTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketWriterTest.kt
@@ -31,7 +31,7 @@
@Test
fun testNameCompression() {
val writer = MdnsPacketWriter(ByteArray(1000))
- writer.writeLabels(arrayOf("my", "first", "name"))
+ writer.writeLabels(arrayOf("my", "FIRST", "name"))
writer.writeLabels(arrayOf("my", "second", "name"))
writer.writeLabels(arrayOf("other", "first", "name"))
writer.writeLabels(arrayOf("my", "second", "name"))
@@ -41,7 +41,7 @@
InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::123"), 123))
// Each label takes length + 1. So "first.name" offset = 3, "name" offset = 9
- val expected = "my".label() + "first".label() + "name".label() + 0x00.toByte() +
+ val expected = "my".label() + "FIRST".label() + "name".label() + 0x00.toByte() +
// "my.second.name" offset = 15
"my".label() + "second".label() + byteArrayOf(0xC0.toByte(), 9) +
"other".label() + byteArrayOf(0xC0.toByte(), 3) +
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
index 2b5423b..0a8d78d 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
@@ -48,6 +48,7 @@
private val TEST_SERVICE_NAME_1 = arrayOf("testservice", "_nmt", "_tcp", "local")
private val TEST_SERVICE_NAME_2 = arrayOf("testservice2", "_nmt", "_tcp", "local")
+private val TEST_SERVICE_NAME_3 = arrayOf("Testservice", "_nmt", "_tcp", "local")
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -129,6 +130,15 @@
}
@Test
+ fun testCreateProberCaseInsensitive() {
+ val probeInfo = TestProbeInfo(
+ listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890),
+ makeServiceRecord(TEST_SERVICE_NAME_2, 37890),
+ makeServiceRecord(TEST_SERVICE_NAME_3, 37890)))
+ assertEquals(2, probeInfo.getPacket(0).questions.size)
+ }
+
+ @Test
fun testProbeMultipleRecords() {
val replySender = MdnsReplySender("testiface", thread.looper, socket, buffer)
val prober = TestProber(thread.looper, replySender, cb)
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 44e0d08..0033b5a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -43,7 +43,9 @@
private const val TEST_SERVICE_ID_1 = 42
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 val TEST_HOSTNAME = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
private val TEST_ADDRESSES = listOf(
LinkAddress(parseNumericAddress("192.0.2.111"), 24),
@@ -62,6 +64,12 @@
port = TEST_PORT
}
+private val TEST_SERVICE_3 = NsdServiceInfo().apply {
+ serviceType = "_TESTSERVICE._tcp"
+ serviceName = "MyTESTSERVICE"
+ port = TEST_PORT
+}
+
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsRecordRepositoryTest {
@@ -86,7 +94,8 @@
fun testAddServiceAndProbe() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
assertEquals(0, repository.servicesCount)
- assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
+ assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+ null /* subtype */))
assertEquals(1, repository.servicesCount)
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -118,18 +127,21 @@
@Test
fun testAddAndConflicts() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
assertFailsWith(NameConflictException::class) {
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */)
+ }
+ assertFailsWith(NameConflictException::class) {
+ repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* subtype */)
}
}
@Test
fun testInvalidReuseOfServiceId() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
assertFailsWith(IllegalArgumentException::class) {
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* subtype */)
}
}
@@ -138,7 +150,7 @@
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -180,13 +192,49 @@
}
@Test
+ fun testExitAnnouncements_WithSubtype() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
+ repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+
+ val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
+ assertNotNull(exitAnnouncement)
+ assertEquals(1, repository.servicesCount)
+ val packet = exitAnnouncement.getPacket(0)
+
+ assertEquals(0x8400 /* response, authoritative */, packet.flags)
+ assertEquals(0, packet.questions.size)
+ assertEquals(0, packet.authorityRecords.size)
+ assertEquals(0, packet.additionalRecords.size)
+
+ assertContentEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 0L /* ttlMillis */,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")),
+ MdnsPointerRecord(
+ arrayOf("_subtype", "_sub", "_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 0L /* ttlMillis */,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")),
+ ), packet.answers)
+
+ repository.removeService(TEST_SERVICE_ID_1)
+ assertEquals(0, repository.servicesCount)
+ }
+
+ @Test
fun testExitingServiceReAdded() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.onAdvertisementSent(TEST_SERVICE_ID_1)
repository.exitService(TEST_SERVICE_ID_1)
- assertEquals(TEST_SERVICE_ID_1, repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1))
+ assertEquals(TEST_SERVICE_ID_1,
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */))
assertEquals(1, repository.servicesCount)
repository.removeService(TEST_SERVICE_ID_2)
@@ -196,7 +244,8 @@
@Test
fun testOnProbingSucceeded() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
- val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+ TEST_SUBTYPE)
repository.onAdvertisementSent(TEST_SERVICE_ID_1)
val packet = announcementInfo.getPacket(0)
@@ -205,6 +254,7 @@
assertEquals(0, packet.authorityRecords.size)
val serviceType = arrayOf("_testservice", "_tcp", "local")
+ val serviceSubtype = arrayOf(TEST_SUBTYPE, "_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)
@@ -250,6 +300,13 @@
false /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName),
+ MdnsPointerRecord(
+ serviceSubtype,
+ 0L /* receiptTimeMillis */,
+ // Not a unique name owned by the announcer, so cacheFlush=false
+ false /* cacheFlush */,
+ 4500000L /* ttlMillis */,
+ serviceName),
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
@@ -318,10 +375,43 @@
}
@Test
- fun testGetReply() {
+ fun testGetReplyCaseInsensitive() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- val questions = listOf(MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"),
+ val questionsCaseInSensitive =
+ listOf(MdnsPointerRecord(arrayOf("_TESTSERVICE", "_TCP", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ // TTL and data is empty for a question
+ 0L /* ttlMillis */,
+ null /* pointer */))
+ val queryCaseInsensitive = MdnsPacket(0 /* flags */, questionsCaseInSensitive,
+ listOf() /* answers */, listOf() /* authorityRecords */,
+ listOf() /* additionalRecords */)
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val replyCaseInsensitive = repository.getReply(queryCaseInsensitive, src)
+ assertNotNull(replyCaseInsensitive)
+ assertEquals(1, replyCaseInsensitive.answers.size)
+ assertEquals(7, replyCaseInsensitive.additionalAnswers.size)
+ }
+
+ @Test
+ fun testGetReply() {
+ doGetReplyTest(subtype = null)
+ }
+
+ @Test
+ fun testGetReply_WithSubtype() {
+ doGetReplyTest(TEST_SUBTYPE)
+ }
+
+ private fun doGetReplyTest(subtype: String?) {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, subtype)
+ val queriedName = if (subtype == null) arrayOf("_testservice", "_tcp", "local")
+ else arrayOf(subtype, "_sub", "_testservice", "_tcp", "local")
+
+ val questions = listOf(MdnsPointerRecord(queriedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
// TTL and data is empty for a question
@@ -344,7 +434,7 @@
assertEquals(listOf(
MdnsPointerRecord(
- arrayOf("_testservice", "_tcp", "local"),
+ queriedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
longTtl,
@@ -405,8 +495,8 @@
@Test
fun testGetConflictingServices() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
val packet = MdnsPacket(
0 /* flags */,
@@ -431,10 +521,38 @@
}
@Test
+ fun testGetConflictingServicesCaseInsensitive() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+
+ val packet = MdnsPacket(
+ 0 /* flags */,
+ emptyList() /* questions */,
+ listOf(
+ MdnsServiceRecord(
+ arrayOf("MYTESTSERVICE", "_TESTSERVICE", "_tcp", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */, 0L /* ttlMillis */,
+ 0 /* servicePriority */, 0 /* serviceWeight */,
+ TEST_SERVICE_1.port + 1,
+ TEST_HOSTNAME),
+ MdnsTextRecord(
+ arrayOf("MYOTHERTESTSERVICE", "_TESTSERVICE", "_tcp", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */, 0L /* ttlMillis */,
+ listOf(TextEntry.fromString("somedifferent=entry"))),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(setOf(TEST_SERVICE_ID_1, TEST_SERVICE_ID_2),
+ repository.getConflictingServices(packet))
+ }
+
+ @Test
fun testGetConflictingServices_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@@ -446,7 +564,7 @@
0L /* receiptTimeMillis */, true /* cacheFlush */,
otherTtlMillis, 0 /* servicePriority */, 0 /* serviceWeight */,
TEST_SERVICE_1.port,
- TEST_HOSTNAME),
+ arrayOf("ANDROID_000102030405060708090A0B0C0D0E0F", "local")),
MdnsTextRecord(
arrayOf("MyOtherTestService", "_testservice", "_tcp", "local"),
0L /* receiptTimeMillis */, true /* cacheFlush */,
@@ -458,12 +576,44 @@
// Above records are identical to the actual registrations: no conflict
assertEquals(emptySet(), repository.getConflictingServices(packet))
}
+
+ @Test
+ fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+
+ val otherTtlMillis = 1234L
+ val packet = MdnsPacket(
+ 0 /* flags */,
+ emptyList() /* questions */,
+ listOf(
+ MdnsServiceRecord(
+ arrayOf("MYTESTSERVICE", "_TESTSERVICE", "_tcp", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ otherTtlMillis, 0 /* servicePriority */, 0 /* serviceWeight */,
+ TEST_SERVICE_1.port,
+ TEST_HOSTNAME),
+ MdnsTextRecord(
+ arrayOf("MyOtherTestService", "_TESTSERVICE", "_tcp", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ otherTtlMillis, emptyList()),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ // Above records are identical to the actual registrations: no conflict
+ assertEquals(emptySet(), repository.getConflictingServices(packet))
+ }
}
-private fun MdnsRecordRepository.initWithService(serviceId: Int, serviceInfo: NsdServiceInfo):
- AnnouncementInfo {
+private fun MdnsRecordRepository.initWithService(
+ serviceId: Int,
+ serviceInfo: NsdServiceInfo,
+ subtype: String? = null
+): AnnouncementInfo {
updateAddresses(TEST_ADDRESSES)
- addService(serviceId, serviceInfo)
+ addService(serviceId, serviceInfo, subtype)
val probingInfo = setServiceProbing(serviceId)
assertNotNull(probingInfo)
return onProbingSucceeded(probingInfo)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTest.kt
new file mode 100644
index 0000000..8f819b7
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class MdnsRecordTest {
+
+ @Test
+ fun testPointerRecordHasSubType() {
+ val ptrRecord1 = MdnsPointerRecord(
+ arrayOf("_testtype", "_sub", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 4500000 /* ttlMillis */,
+ arrayOf("testservice", "_testtype", "_tcp", "local")
+ )
+ val ptrRecord2 = MdnsPointerRecord(
+ arrayOf("_testtype", "_SUB", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 4500000 /* ttlMillis */,
+ arrayOf("testservice", "_testtype", "_tcp", "local")
+ )
+ assertTrue(ptrRecord1.hasSubtype())
+ assertTrue(ptrRecord2.hasSubtype())
+ }
+
+ @Test
+ fun testEqualsCaseInsensitive() {
+ val ptrRecord1 = MdnsPointerRecord(
+ arrayOf("_testtype", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 4500000 /* ttlMillis */,
+ arrayOf("testservice", "_testtype", "_tcp", "local")
+ )
+ val ptrRecord2 = MdnsPointerRecord(
+ arrayOf("_testType", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 4500000 /* ttlMillis */,
+ arrayOf("testsErvice", "_testtype", "_Tcp", "local")
+ )
+ assertEquals(ptrRecord1, ptrRecord2)
+ assertEquals(ptrRecord1.hashCode(), ptrRecord2.hashCode())
+
+ val srvRecord1 = MdnsServiceRecord(
+ arrayOf("testservice", "_testtype", "_tcp", "local"),
+ 123 /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 2000 /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ 80 /* port */,
+ arrayOf("hostname")
+ )
+ val srvRecord2 = MdnsServiceRecord(
+ arrayOf("Testservice", "_testtype", "_tcp", "local"),
+ 123 /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 2000 /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ 80 /* port */,
+ arrayOf("Hostname")
+ )
+ assertEquals(srvRecord1, srvRecord2)
+ assertEquals(srvRecord1.hashCode(), srvRecord2.hashCode())
+
+ val nsecRecord1 = MdnsNsecRecord(
+ arrayOf("hostname"),
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 2000L, /* ttlMillis */
+ arrayOf("hostname"),
+ intArrayOf(1, 2, 3) /* types */
+ )
+ val nsecRecord2 = MdnsNsecRecord(
+ arrayOf("HOSTNAME"),
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 2000L, /* ttlMillis */
+ arrayOf("HOSTNAME"),
+ intArrayOf(1, 2, 3) /* types */
+ )
+ assertEquals(nsecRecord1, nsecRecord2)
+ assertEquals(nsecRecord1.hashCode(), nsecRecord2.hashCode())
+ }
+
+ @Test
+ fun testLabelsAreSuffix() {
+ val labels1 = arrayOf("a", "b", "c")
+ val labels2 = arrayOf("B", "C")
+ val labels3 = arrayOf("b", "c")
+ val labels4 = arrayOf("b", "d")
+ assertTrue(MdnsRecord.labelsAreSuffix(labels2, labels1))
+ assertTrue(MdnsRecord.labelsAreSuffix(labels3, labels1))
+ assertFalse(MdnsRecord.labelsAreSuffix(labels4, labels1))
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
index b0a1f18..e16c448 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
@@ -269,14 +269,9 @@
assertEquals("st=0", textStrings.get(6));
}
- @Test
- public void testDecodeIPv6AnswerPacket() throws IOException {
- MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, CAST_SERVICE_TYPE);
- assertNotNull(data6);
-
- responses = decode(decoder, data6);
- assertEquals(1, responses.size());
- MdnsResponse response = responses.valueAt(0);
+ private void verifyResponse(ArraySet<MdnsResponse> responseArraySet) {
+ assertEquals(1, responseArraySet.size());
+ MdnsResponse response = responseArraySet.valueAt(0);
assertTrue(response.isComplete());
MdnsInetAddressRecord inet6AddressRecord = response.getInet6AddressRecord();
@@ -290,6 +285,22 @@
}
@Test
+ public void testDecodeIPv6AnswerPacket() throws IOException {
+ MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, CAST_SERVICE_TYPE);
+ assertNotNull(data6);
+ verifyResponse(decode(decoder, data6));
+ }
+
+ @Test
+ public void testDecodeCaseInsensitiveMatch() throws IOException {
+ final String[] castServiceTypeUpperCase =
+ new String[] {"_GOOGLECAST", "_TCP", "LOCAL"};
+ MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, castServiceTypeUpperCase);
+ assertNotNull(data6);
+ verifyResponse(decode(decoder, data6));
+ }
+
+ @Test
public void testIsComplete() {
MdnsResponse response = new MdnsResponse(responses.valueAt(0));
assertTrue(response.isComplete());
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
index 3f5e7a1..dc0e646 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
@@ -228,6 +228,12 @@
final MdnsResponse response = makeCompleteResponse(TEST_TTL_MS);
assertFalse(response.addPointerRecord(response.getPointerRecords().get(0)));
+ final String[] serviceName = new String[] { "MYSERVICE", "_TYPE", "_tcp", "local" };
+ final String[] serviceType = new String[] { "_TYPE", "_tcp", "local" };
+ MdnsPointerRecord pointerRecordCaseInsensitive =
+ new MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */,
+ false /* cacheFlush */, TEST_TTL_MS, serviceName);
+ assertFalse(response.addPointerRecord(pointerRecordCaseInsensitive));
assertFalse(response.addInet6AddressRecord(response.getInet6AddressRecord()));
assertFalse(response.addInet4AddressRecord(response.getInet4AddressRecord()));
assertFalse(response.setServiceRecord(response.getServiceRecord()));
@@ -270,4 +276,30 @@
// All records were replaced, not added
assertEquals(ttlZeroResponse.getRecords().size(), response.getRecords().size());
}
+
+ @Test
+ public void dropUnmatchedAddressRecords_caseInsensitive() {
+
+ final String[] hostname = new String[] { "MyHostname" };
+ final String[] upperCaseHostName = new String[] { "MYHOSTNAME" };
+ final String[] serviceName = new String[] { "MyService", "_type", "_tcp", "local" };
+ final String[] serviceType = new String[] { "_type", "_tcp", "local" };
+ final MdnsResponse response = new MdnsResponse(/* now= */ 0, serviceName, INTERFACE_INDEX,
+ mNetwork);
+ response.addPointerRecord(new MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */,
+ false /* cacheFlush */, TEST_TTL_MS, serviceName));
+ response.setServiceRecord(new MdnsServiceRecord(serviceName, 0L /* receiptTimeMillis */,
+ true /* cacheFlush */, TEST_TTL_MS, 0 /* servicePriority */,
+ 0 /* serviceWeight */, 0 /* servicePort */, hostname));
+ response.setTextRecord(new MdnsTextRecord(serviceName, 0L /* receiptTimeMillis */,
+ true /* cacheFlush */, TEST_TTL_MS, emptyList() /* entries */));
+ response.addInet4AddressRecord(new MdnsInetAddressRecord(
+ upperCaseHostName , 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ TEST_TTL_MS, parseNumericAddress("192.0.2.123")));
+ response.addInet6AddressRecord(new MdnsInetAddressRecord(
+ upperCaseHostName, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ TEST_TTL_MS, parseNumericAddress("2001:db8::123")));
+
+ assertFalse(response.dropUnmatchedAddressRecords());
+ }
}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 2fcdff2..42d296b 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -41,6 +42,7 @@
import android.net.Network;
import android.text.TextUtils;
+import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
import com.android.server.connectivity.mdns.MdnsServiceTypeClient.QueryTaskConfig;
@@ -52,6 +54,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
@@ -87,6 +90,7 @@
private static final long TEST_TTL = 120000L;
private static final long TEST_ELAPSED_REALTIME = 123L;
+ private static final long TEST_TIMEOUT_MS = 10_000L;
@Mock
private MdnsServiceBrowserListener mockListenerOne;
@@ -422,6 +426,34 @@
assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
}
+ @Test
+ public void testQueryScheduledWhenAnsweredFromCache() {
+ final MdnsSearchOptions searchOptions = MdnsSearchOptions.getDefaultOptions();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+ assertNotNull(currentThreadExecutor.getAndClearSubmittedRunnable());
+
+ client.processResponse(createResponse(
+ "service-instance-1", "192.0.2.123", 5353,
+ SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), TEST_TTL), /* interfaceIndex= */ 20, mockNetwork);
+
+ verify(mockListenerOne).onServiceNameDiscovered(any());
+ verify(mockListenerOne).onServiceFound(any());
+
+ // File another identical query
+ client.startSendAndReceive(mockListenerTwo, searchOptions);
+
+ verify(mockListenerTwo).onServiceNameDiscovered(any());
+ verify(mockListenerTwo).onServiceFound(any());
+
+ // This time no query is submitted, only scheduled
+ assertNull(currentThreadExecutor.getAndClearSubmittedRunnable());
+ assertNotNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
+ // This just skips the first query of the first burst
+ assertEquals(MdnsConfigs.timeBetweenQueriesInBurstMs(),
+ currentThreadExecutor.getAndClearLastScheduledDelayInMs());
+ }
+
private static void verifyServiceInfo(MdnsServiceInfo serviceInfo, String serviceName,
String[] serviceType, List<String> ipv4Addresses, List<String> ipv6Addresses, int port,
List<String> subTypes, Map<String, String> attributes, int interfaceIndex,
@@ -1004,10 +1036,11 @@
final String otherInstance = "instance2";
final String ipV4Address = "192.0.2.0";
final String ipV6Address = "2001:db8::";
+ final String capitalizedRequestInstance = "Instance1";
final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
// Use different case in the options
- .setResolveInstanceName("Instance1").build();
+ .setResolveInstanceName(capitalizedRequestInstance).build();
client.startSendAndReceive(mockListenerOne, resolveOptions);
client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
@@ -1035,8 +1068,9 @@
Collections.emptyMap(), 0L /* ttl */), INTERFACE_INDEX, mockNetwork);
// mockListenerOne gets notified for the requested instance
- verify(mockListenerOne).onServiceNameDiscovered(matchServiceName(requestedInstance));
- verify(mockListenerOne).onServiceFound(matchServiceName(requestedInstance));
+ verify(mockListenerOne).onServiceNameDiscovered(
+ matchServiceName(capitalizedRequestInstance));
+ verify(mockListenerOne).onServiceFound(matchServiceName(capitalizedRequestInstance));
// ...but does not get any callback for the other instance
verify(mockListenerOne, never()).onServiceFound(matchServiceName(otherInstance));
@@ -1047,8 +1081,9 @@
// mockListenerTwo gets notified for both though
final InOrder inOrder = inOrder(mockListenerTwo);
inOrder.verify(mockListenerTwo).onServiceNameDiscovered(
- matchServiceName(requestedInstance));
- inOrder.verify(mockListenerTwo).onServiceFound(matchServiceName(requestedInstance));
+ matchServiceName(capitalizedRequestInstance));
+ inOrder.verify(mockListenerTwo).onServiceFound(
+ matchServiceName(capitalizedRequestInstance));
inOrder.verify(mockListenerTwo).onServiceNameDiscovered(matchServiceName(otherInstance));
inOrder.verify(mockListenerTwo).onServiceFound(matchServiceName(otherInstance));
@@ -1057,7 +1092,88 @@
}
@Test
- public void testNotifyAllServicesRemoved() {
+ public void testProcessResponse_SubtypeDiscoveryLimitedToSubtype() {
+ client = new MdnsServiceTypeClient(
+ SERVICE_TYPE, mockSocketClient, currentThreadExecutor, mockNetwork, mockSharedLog);
+
+ final String matchingInstance = "instance1";
+ final String subtype = "_subtype";
+ final String otherInstance = "instance2";
+ final String ipV4Address = "192.0.2.0";
+ final String ipV6Address = "2001:db8::";
+
+ final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
+ // Search with different case. Note MdnsSearchOptions subtype doesn't start with "_"
+ .addSubtype("Subtype").build();
+
+ client.startSendAndReceive(mockListenerOne, options);
+ client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+ // Complete response from instanceName
+ final MdnsPacket packetWithoutSubtype = createResponse(
+ matchingInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap() /* textAttributes */, TEST_TTL);
+ final MdnsPointerRecord originalPtr = (MdnsPointerRecord) CollectionUtils.findFirst(
+ packetWithoutSubtype.answers, r -> r instanceof MdnsPointerRecord);
+
+ // Add a subtype PTR record
+ final ArrayList<MdnsRecord> newAnswers = new ArrayList<>(packetWithoutSubtype.answers);
+ newAnswers.add(new MdnsPointerRecord(
+ // PTR should be _subtype._sub._type._tcp.local -> instance1._type._tcp.local
+ Stream.concat(Stream.of(subtype, "_sub"), Arrays.stream(SERVICE_TYPE_LABELS))
+ .toArray(String[]::new),
+ originalPtr.getReceiptTime(), originalPtr.getCacheFlush(), originalPtr.getTtl(),
+ originalPtr.getPointer()));
+ final MdnsPacket packetWithSubtype = new MdnsPacket(
+ packetWithoutSubtype.flags,
+ packetWithoutSubtype.questions,
+ newAnswers,
+ packetWithoutSubtype.authorityRecords,
+ packetWithoutSubtype.additionalRecords);
+ client.processResponse(packetWithSubtype, INTERFACE_INDEX, mockNetwork);
+
+ // Complete response from otherInstanceName, without subtype
+ client.processResponse(createResponse(
+ otherInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap() /* textAttributes */, TEST_TTL),
+ INTERFACE_INDEX, mockNetwork);
+
+ // Address update from otherInstanceName
+ client.processResponse(createResponse(
+ otherInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+
+ // Goodbye from otherInstanceName
+ client.processResponse(createResponse(
+ otherInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), 0L /* ttl */), INTERFACE_INDEX, mockNetwork);
+
+ // mockListenerOne gets notified for the requested instance
+ final ArgumentMatcher<MdnsServiceInfo> subtypeInstanceMatcher = info ->
+ info.getServiceInstanceName().equals(matchingInstance)
+ && info.getSubtypes().equals(Collections.singletonList(subtype));
+ verify(mockListenerOne).onServiceNameDiscovered(argThat(subtypeInstanceMatcher));
+ verify(mockListenerOne).onServiceFound(argThat(subtypeInstanceMatcher));
+
+ // ...but does not get any callback for the other instance
+ verify(mockListenerOne, never()).onServiceFound(matchServiceName(otherInstance));
+ verify(mockListenerOne, never()).onServiceNameDiscovered(matchServiceName(otherInstance));
+ verify(mockListenerOne, never()).onServiceUpdated(matchServiceName(otherInstance));
+ verify(mockListenerOne, never()).onServiceRemoved(matchServiceName(otherInstance));
+
+ // mockListenerTwo gets notified for both though
+ final InOrder inOrder = inOrder(mockListenerTwo);
+ inOrder.verify(mockListenerTwo).onServiceNameDiscovered(argThat(subtypeInstanceMatcher));
+ inOrder.verify(mockListenerTwo).onServiceFound(argThat(subtypeInstanceMatcher));
+
+ inOrder.verify(mockListenerTwo).onServiceNameDiscovered(matchServiceName(otherInstance));
+ inOrder.verify(mockListenerTwo).onServiceFound(matchServiceName(otherInstance));
+ inOrder.verify(mockListenerTwo).onServiceUpdated(matchServiceName(otherInstance));
+ inOrder.verify(mockListenerTwo).onServiceRemoved(matchServiceName(otherInstance));
+ }
+
+ @Test
+ public void testNotifySocketDestroyed() throws Exception {
client = new MdnsServiceTypeClient(
SERVICE_TYPE, mockSocketClient, currentThreadExecutor, mockNetwork, mockSharedLog);
@@ -1066,12 +1182,21 @@
final String ipV4Address = "192.0.2.0";
final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
- // Use different case in the options
- .setResolveInstanceName("Instance1").build();
+ .setResolveInstanceName("instance1").build();
client.startSendAndReceive(mockListenerOne, resolveOptions);
+ // Ensure the first task is executed so it schedules a future task
+ currentThreadExecutor.getAndClearSubmittedFuture().get(
+ TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+ // Filing the second request cancels the first future
+ verify(expectedSendFutures[0]).cancel(true);
+
+ // Ensure it gets executed too
+ currentThreadExecutor.getAndClearSubmittedFuture().get(
+ TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
// Complete response from instanceName
client.processResponse(createResponse(
requestedInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
@@ -1084,7 +1209,9 @@
Collections.emptyMap() /* textAttributes */, TEST_TTL),
INTERFACE_INDEX, mockNetwork);
- client.notifyAllServicesRemoved();
+ verify(expectedSendFutures[1], never()).cancel(true);
+ client.notifySocketDestroyed();
+ verify(expectedSendFutures[1]).cancel(true);
// mockListenerOne gets notified for the requested instance
final InOrder inOrder1 = inOrder(mockListenerOne);
@@ -1149,6 +1276,7 @@
private long lastScheduledDelayInMs;
private Runnable lastScheduledRunnable;
private Runnable lastSubmittedRunnable;
+ private Future<?> lastSubmittedFuture;
private int futureIndex;
FakeExecutor() {
@@ -1160,6 +1288,7 @@
public Future<?> submit(Runnable command) {
Future<?> future = super.submit(command);
lastSubmittedRunnable = command;
+ lastSubmittedFuture = future;
return future;
}
@@ -1191,6 +1320,12 @@
lastSubmittedRunnable = null;
return val;
}
+
+ Future<?> getAndClearSubmittedFuture() {
+ Future<?> val = lastSubmittedFuture;
+ lastSubmittedFuture = null;
+ return val;
+ }
}
private MdnsPacket createResponse(
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
index 744ec84..4b87556 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
@@ -349,8 +349,8 @@
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
testCallback1.expectedNoCallback();
testCallback2.expectedNoCallback();
- // Expect the socket destroy for tethered interface.
- testCallback3.expectedInterfaceDestroyedForNetwork(null /* network */);
+ // There was still a tethered interface, but no callback should be sent once unregistered
+ testCallback3.expectedNoCallback();
}
private RtNetlinkAddressMessage createNetworkAddressUpdateNetLink(
@@ -528,7 +528,8 @@
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
mHandler.post(()-> mSocketProvider.unrequestSocket(testCallback));
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
- testCallback.expectedInterfaceDestroyedForNetwork(TEST_NETWORK);
+ // No callback sent when unregistered
+ testCallback.expectedNoCallback();
verify(mCm, times(1)).unregisterNetworkCallback(any(NetworkCallback.class));
verify(mTm, times(1)).unregisterTetheringEventCallback(any());
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt
new file mode 100644
index 0000000..c62a081
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt
@@ -0,0 +1,90 @@
+package com.android.server.connectivity.mdns.internal
+
+import android.net.LinkAddress
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.system.OsConstants
+import com.android.net.module.util.SharedLog
+import com.android.net.module.util.netlink.NetlinkConstants
+import com.android.net.module.util.netlink.RtNetlinkAddressMessage
+import com.android.net.module.util.netlink.StructIfaddrMsg
+import com.android.net.module.util.netlink.StructNlMsgHdr
+import com.android.server.connectivity.mdns.MdnsAdvertiserTest
+import com.android.server.connectivity.mdns.MdnsSocketProvider
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito
+import org.mockito.Mockito.argThat
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+private val LINKADDRV4 = LinkAddress("192.0.2.0/24")
+private val IFACE_IDX = 32
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+internal class SocketNetlinkMonitorTest {
+ private val thread = HandlerThread(MdnsAdvertiserTest::class.simpleName)
+ private val sharedlog = Mockito.mock(SharedLog::class.java)
+ private val netlinkMonitorCallBack =
+ Mockito.mock(MdnsSocketProvider.NetLinkMonitorCallBack::class.java)
+
+ @Before
+ fun setUp() {
+ thread.start()
+ }
+
+ @After
+ fun tearDown() {
+ thread.quitSafely()
+ }
+
+ @Test
+ fun testHandleDeprecatedNetlinkMessage() {
+ val socketNetlinkMonitor = SocketNetlinkMonitor(Handler(thread.looper), sharedlog,
+ netlinkMonitorCallBack)
+ val nlmsghdr = StructNlMsgHdr().apply {
+ nlmsg_type = NetlinkConstants.RTM_NEWADDR
+ nlmsg_flags = (StructNlMsgHdr.NLM_F_REQUEST.toInt()
+ or StructNlMsgHdr.NLM_F_ACK.toInt()).toShort()
+ nlmsg_seq = 1
+ }
+ val structIfaddrMsg = StructIfaddrMsg(OsConstants.AF_INET.toShort(),
+ LINKADDRV4.prefixLength.toShort(),
+ LINKADDRV4.flags.toShort(),
+ LINKADDRV4.scope.toShort(), IFACE_IDX)
+ // If the LinkAddress is not preferred, RTM_NEWADDR will not trigger
+ // addOrUpdateInterfaceAddress() callback.
+ val deprecatedAddNetLinkMessage = RtNetlinkAddressMessage(nlmsghdr, structIfaddrMsg,
+ LINKADDRV4.address, null /* structIfacacheInfo */,
+ LINKADDRV4.flags or OsConstants.IFA_F_DEPRECATED)
+ socketNetlinkMonitor.processNetlinkMessage(deprecatedAddNetLinkMessage, 0L /* whenMs */)
+ verify(netlinkMonitorCallBack, never()).addOrUpdateInterfaceAddress(eq(IFACE_IDX),
+ argThat { it.address == LINKADDRV4.address })
+
+ // If the LinkAddress is preferred, RTM_NEWADDR will trigger addOrUpdateInterfaceAddress()
+ // callback.
+ val preferredAddNetLinkMessage = RtNetlinkAddressMessage(nlmsghdr, structIfaddrMsg,
+ LINKADDRV4.address, null /* structIfacacheInfo */,
+ LINKADDRV4.flags or OsConstants.IFA_F_OPTIMISTIC)
+ socketNetlinkMonitor.processNetlinkMessage(preferredAddNetLinkMessage, 0L /* whenMs */)
+ verify(netlinkMonitorCallBack).addOrUpdateInterfaceAddress(eq(IFACE_IDX),
+ argThat { it.address == LINKADDRV4.address })
+
+ // Even if the LinkAddress is not preferred, RTM_DELADDR will trigger
+ // deleteInterfaceAddress() callback.
+ nlmsghdr.nlmsg_type = NetlinkConstants.RTM_DELADDR
+ val deprecatedDelNetLinkMessage = RtNetlinkAddressMessage(nlmsghdr, structIfaddrMsg,
+ LINKADDRV4.address, null /* structIfacacheInfo */,
+ LINKADDRV4.flags or OsConstants.IFA_F_DEPRECATED)
+ socketNetlinkMonitor.processNetlinkMessage(deprecatedDelNetLinkMessage, 0L /* whenMs */)
+ verify(netlinkMonitorCallBack).deleteInterfaceAddress(eq(IFACE_IDX),
+ argThat { it.address == LINKADDRV4.address })
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
index 61c3a70..f705bcb 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/util/MdnsUtilsTest.kt
@@ -17,11 +17,14 @@
package com.android.server.connectivity.mdns.util
import android.os.Build
+import com.android.server.connectivity.mdns.util.MdnsUtils.equalsDnsLabelIgnoreDnsCase
import com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase
+import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLabelsLowerCase
import com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase
import com.android.server.connectivity.mdns.util.MdnsUtils.truncateServiceName
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -50,6 +53,12 @@
}
@Test
+ fun testToDnsLabelsLowerCase() {
+ assertArrayEquals(arrayOf("test", "tÉst", "ţést"),
+ toDnsLabelsLowerCase(arrayOf("TeSt", "TÉST", "ţést")))
+ }
+
+ @Test
fun testEqualsIgnoreDnsCase() {
assertTrue(equalsIgnoreDnsCase("TEST", "Test"))
assertTrue(equalsIgnoreDnsCase("TEST", "test"))
@@ -72,4 +81,25 @@
assertEquals(truncateServiceName("测试abcde", 7), "测试a")
assertEquals(truncateServiceName("测试abcde", 100), "测试abcde")
}
+
+ @Test
+ fun testEqualsLabelIgnoreDnsCase() {
+ assertTrue(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "test")))
+ assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test")))
+ assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("Test"), arrayOf("test", "test")))
+ assertFalse(equalsDnsLabelIgnoreDnsCase(arrayOf("TEST", "Test"), arrayOf("test", "tést")))
+ }
+
+ @Test
+ fun testTypeEqualsOrIsSubtype() {
+ assertTrue(MdnsUtils.typeEqualsOrIsSubtype(arrayOf("_type", "_tcp", "local"),
+ arrayOf("_type", "_TCP", "local")))
+ assertTrue(MdnsUtils.typeEqualsOrIsSubtype(arrayOf("_type", "_tcp", "local"),
+ arrayOf("a", "_SUB", "_type", "_TCP", "local")))
+ assertFalse(MdnsUtils.typeEqualsOrIsSubtype(arrayOf("_sub", "_type", "_tcp", "local"),
+ arrayOf("_type", "_TCP", "local")))
+ assertFalse(MdnsUtils.typeEqualsOrIsSubtype(
+ arrayOf("a", "_other", "_type", "_tcp", "local"),
+ arrayOf("a", "_SUB", "_type", "_TCP", "local")))
+ }
}
diff --git a/tests/unit/vpn-jarjar-rules.txt b/tests/unit/vpn-jarjar-rules.txt
index 16661b9..1a6bddc 100644
--- a/tests/unit/vpn-jarjar-rules.txt
+++ b/tests/unit/vpn-jarjar-rules.txt
@@ -1,4 +1,4 @@
# Only keep classes imported by ConnectivityServiceTest
-keep com.android.server.VpnManagerService
keep com.android.server.connectivity.Vpn
-keep com.android.server.connectivity.VpnProfileStore
\ No newline at end of file
+keep com.android.server.connectivity.VpnProfileStore
+keep com.android.server.net.LockdownVpnTracker