Merge "Revert "Add onSupportedTetheringType callback""
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 95f854b..4774866 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -22,6 +22,30 @@
}
]
},
+ // Also run CtsNetTestCasesLatestSdk to ensure tests using older shims pass.
+ {
+ "name": "CtsNetTestCasesLatestSdk",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
+ },
+ // CTS tests that target older SDKs.
+ {
+ "name": "CtsNetTestCasesMaxTargetSdk31",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
+ },
{
"name": "bpf_existence_test"
},
@@ -29,6 +53,9 @@
"name": "connectivity_native_test"
},
{
+ "name": "libclat_test"
+ },
+ {
"name": "netd_updatable_unit_test"
},
{
@@ -56,16 +83,10 @@
"keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
},
{
- "name": "libclat_test"
- },
- {
"name": "traffic_controller_unit_test",
"keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
},
{
- "name": "libnetworkstats_test"
- },
- {
"name": "FrameworksNetDeflakeTest"
}
],
@@ -82,6 +103,17 @@
]
},
{
+ "name": "CtsNetTestCasesMaxTargetSdk31[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
+ },
+ {
"name": "bpf_existence_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
},
{
@@ -139,23 +171,6 @@
]
}
],
- "auto-postsubmit": [
- // Test tag for automotive targets. These are only running in postsubmit so as to harden the
- // automotive targets to avoid introducing additional test flake and build time. The plan for
- // presubmit testing for auto is to augment the existing tests to cover auto use cases as well.
- // Additionally, this tag is used in targeted test suites to limit resource usage on the test
- // infra during the hardening phase.
- // TODO: this tag to be removed once the above is no longer an issue.
- {
- "name": "FrameworksNetTests"
- },
- {
- "name": "FrameworksNetIntegrationTests"
- },
- {
- "name": "FrameworksNetDeflakeTest"
- }
- ],
"imports": [
{
"path": "frameworks/base/core/java/android/net"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 29f6e12..bfba5cd 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -21,7 +21,7 @@
java_defaults {
name: "TetheringApiLevel",
sdk_version: "module_current",
- target_sdk_version: "31",
+ target_sdk_version: "33",
min_sdk_version: "30",
}
@@ -33,9 +33,11 @@
":framework-connectivity-shared-srcs",
":tethering-module-utils-srcs",
":services-tethering-shared-srcs",
+ ":statslog-tethering-java-gen",
],
static_libs: [
"androidx.annotation_annotation",
+ "connectivity-net-module-utils-bpf",
"modules-utils-build",
"modules-utils-statemachine",
"networkstack-client",
@@ -133,7 +135,7 @@
"-Wthread-safety",
],
- ldflags: ["-Wl,--exclude-libs=ALL,-error-limit=0"],
+ ldflags: ["-Wl,--exclude-libs=ALL,--error-limit=0"],
}
// Common defaults for compiling the actual APK.
@@ -178,9 +180,9 @@
certificate: "networkstack",
manifest: "AndroidManifest.xml",
use_embedded_native_libs: true,
- // The permission configuration *must* be included to ensure security of the device
+ // The network stack *must* be included to ensure security of the device
required: [
- "NetworkPermissionConfig",
+ "NetworkStack",
"privapp_allowlist_com.android.tethering",
],
apex_available: ["com.android.tethering"],
@@ -198,9 +200,9 @@
certificate: "networkstack",
manifest: "AndroidManifest.xml",
use_embedded_native_libs: true,
- // The permission configuration *must* be included to ensure security of the device
+ // The network stack *must* be included to ensure security of the device
required: [
- "NetworkPermissionConfig",
+ "NetworkStackNext",
"privapp_allowlist_com.android.tethering",
],
apex_available: ["com.android.tethering"],
@@ -223,3 +225,11 @@
apex_available: ["com.android.tethering"],
min_sdk_version: "30",
}
+
+genrule {
+ name: "statslog-tethering-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module network_tethering" +
+ " --javaPackage com.android.networkstack.tethering.metrics --javaClass TetheringStatsLog",
+ out: ["com/android/networkstack/tethering/metrics/TetheringStatsLog.java"],
+}
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index bd8fe7c..b3cae7c 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -70,9 +70,9 @@
canned_fs_config: "canned_fs_config",
bpfs: [
"block.o",
- "clatd.o_mainline",
+ "clatd.o",
"dscp_policy.o",
- "netd.o_mainline",
+ "netd.o",
"offload.o",
"test.o",
],
@@ -134,11 +134,11 @@
hidden_api: {
max_target_r_low_priority: [
"hiddenapi/hiddenapi-max-target-r-loprio.txt",
- ],
+ ],
max_target_o_low_priority: [
"hiddenapi/hiddenapi-max-target-o-low-priority.txt",
"hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt",
- ],
+ ],
unsupported: [
"hiddenapi/hiddenapi-unsupported.txt",
"hiddenapi/hiddenapi-unsupported-tiramisu.txt",
diff --git a/Tethering/apex/manifest.json b/Tethering/apex/manifest.json
index 88f13b2..5d5ede6 100644
--- a/Tethering/apex/manifest.json
+++ b/Tethering/apex/manifest.json
@@ -1,4 +1,7 @@
{
"name": "com.android.tethering",
- "version": 319999900
+
+ // Placeholder module version to be replaced during build.
+ // Do not change!
+ "version": 0
}
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index 22d2c5d..18ef631 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -19,7 +19,6 @@
import android.net.INetd;
import android.net.MacAddress;
import android.net.TetherStatsParcel;
-import android.net.util.SharedLog;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.util.SparseArray;
@@ -28,11 +27,12 @@
import androidx.annotation.Nullable;
import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
+import com.android.net.module.util.SharedLog;
import com.android.net.module.util.bpf.Tether4Key;
import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsValue;
import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
-import com.android.networkstack.tethering.TetherStatsValue;
/**
* Bpf coordinator class for API shims.
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 5afb862..fd9dab5 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -19,7 +19,6 @@
import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
import android.net.MacAddress;
-import android.net.util.SharedLog;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
@@ -31,8 +30,11 @@
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
+import com.android.net.module.util.SharedLog;
import com.android.net.module.util.bpf.Tether4Key;
import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
import com.android.networkstack.tethering.BpfUtils;
@@ -42,8 +44,6 @@
import com.android.networkstack.tethering.TetherDownstream6Key;
import com.android.networkstack.tethering.TetherLimitKey;
import com.android.networkstack.tethering.TetherLimitValue;
-import com.android.networkstack.tethering.TetherStatsKey;
-import com.android.networkstack.tethering.TetherStatsValue;
import com.android.networkstack.tethering.TetherUpstream6Key;
import java.io.FileDescriptor;
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index 915e210..69cbab5 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -25,9 +25,9 @@
import com.android.net.module.util.IBpfMap.ThrowingBiConsumer;
import com.android.net.module.util.bpf.Tether4Key;
import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsValue;
import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
-import com.android.networkstack.tethering.TetherStatsValue;
/**
* Bpf coordinator class for API shims.
diff --git a/Tethering/jarjar-rules.txt b/Tethering/jarjar-rules.txt
index 40eed3f..904e491 100644
--- a/Tethering/jarjar-rules.txt
+++ b/Tethering/jarjar-rules.txt
@@ -4,6 +4,7 @@
# module will be overwritten by the ones in the framework.
rule com.android.internal.util.** com.android.networkstack.tethering.util.@1
rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1
+rule android.util.IndentingPrintWriter* com.android.networkstack.tethering.util.AndroidUtilIndentingPrintWriter@1
rule android.net.shared.Inet4AddressUtils* com.android.networkstack.tethering.shared.Inet4AddressUtils@1
@@ -13,4 +14,7 @@
# Classes from net-utils-device-common
rule com.android.net.module.util.Struct* com.android.networkstack.tethering.util.Struct@1
-rule com.google.protobuf.** com.android.networkstack.tethering.protobuf@1
\ No newline at end of file
+rule com.google.protobuf.** com.android.networkstack.tethering.protobuf@1
+
+# Classes for hardware offload hidl interface
+rule android.hidl.base.V1_0.DebugInfo* com.android.networkstack.tethering.hidl.base.V1_0.DebugInfo@1
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 7b5ae0d..2905e28 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -12,6 +12,11 @@
native <methods>;
}
+# Ensure runtime-visible field annotations are kept when using R8 full mode.
+-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
+-keep interface com.android.networkstack.tethering.util.Struct$Field {
+ *;
+}
-keepclassmembers public class * extends com.android.networkstack.tethering.util.Struct {
*;
}
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index c718f4c..da7ca56 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -46,7 +46,6 @@
import android.net.dhcp.IDhcpServer;
import android.net.ip.IpNeighborMonitor.NeighborEvent;
import android.net.ip.RouterAdvertisementDaemon.RaParams;
-import android.net.util.SharedLog;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -64,11 +63,13 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.SharedLog;
import com.android.networkstack.tethering.BpfCoordinator;
import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
import com.android.networkstack.tethering.PrivateAddressCoordinator;
import com.android.networkstack.tethering.TetheringConfiguration;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.networkstack.tethering.util.PrefixUtils;
@@ -282,13 +283,15 @@
private LinkAddress mIpv4Address;
+ private final TetheringMetrics mTetheringMetrics;
+
// TODO: Add a dependency object to pass the data members or variables from the tethering
// object. It helps to reduce the arguments of the constructor.
public IpServer(
String ifaceName, Looper looper, int interfaceType, SharedLog log,
INetd netd, @NonNull BpfCoordinator coordinator, Callback callback,
TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
- Dependencies deps) {
+ TetheringMetrics tetheringMetrics, Dependencies deps) {
super(ifaceName, looper);
mLog = log.forSubComponent(ifaceName);
mNetd = netd;
@@ -303,6 +306,7 @@
mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
mPrivateAddressCoordinator = addressCoordinator;
mDeps = deps;
+ mTetheringMetrics = tetheringMetrics;
resetLinkProperties();
mLastError = TetheringManager.TETHER_ERROR_NO_ERROR;
mServingMode = STATE_AVAILABLE;
@@ -1201,6 +1205,9 @@
stopConntrackMonitoring();
resetLinkProperties();
+
+ mTetheringMetrics.updateErrorCode(mInterfaceType, mLastError);
+ mTetheringMetrics.sendReport(mInterfaceType);
}
@Override
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index f8a1094..133ae01 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -44,14 +44,12 @@
import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
import android.net.ip.IpServer;
import android.net.netstats.provider.NetworkStatsProvider;
-import android.net.util.SharedLog;
import android.os.Handler;
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.OsConstants;
import android.text.TextUtils;
import android.util.ArraySet;
-import android.util.Base64;
import android.util.Log;
import android.util.SparseArray;
@@ -61,19 +59,25 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfDump;
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.U32;
import com.android.net.module.util.bpf.Tether4Key;
import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
import com.android.net.module.util.netlink.ConntrackMessage;
import com.android.net.module.util.netlink.NetlinkConstants;
import com.android.net.module.util.netlink.NetlinkSocket;
import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
import com.android.networkstack.tethering.util.TetheringUtils.ForwardedStats;
+import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
@@ -118,9 +122,8 @@
private static final String TETHER_LIMIT_MAP_PATH = makeMapPath("limit");
private static final String TETHER_ERROR_MAP_PATH = makeMapPath("error");
private static final String TETHER_DEV_MAP_PATH = makeMapPath("dev");
-
- // Using "," as a separator is safe because base64 characters are [0-9a-zA-Z/=+].
- private static final String DUMP_BASE64_DELIMITER = ",";
+ private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
+ private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
/** The names of all the BPF counters defined in bpf_tethering.h. */
public static final String[] sBpfCounterNames = getBpfCounterNames();
@@ -938,6 +941,8 @@
* be allowed to be accessed on the handler thread.
*/
public void dump(@NonNull IndentingPrintWriter pw) {
+ // Note that EthernetTetheringTest#isTetherConfigBpfOffloadEnabled relies on
+ // "mIsBpfEnabled" to check tethering config via dumpsys. Beware of the change if any.
pw.println("mIsBpfEnabled: " + mIsBpfEnabled);
pw.println("Polling " + (mPollingStarted ? "started" : "not started"));
pw.println("Stats provider " + (mStatsProvider != null
@@ -1019,7 +1024,7 @@
map.forEach((k, v) -> {
pw.println(String.format("%s: %s", k, v));
});
- } catch (ErrnoException e) {
+ } catch (ErrnoException | IOException e) {
pw.println("Error dumping BPF stats map: " + e);
}
}
@@ -1067,44 +1072,56 @@
return;
}
map.forEach((k, v) -> pw.println(ipv6UpstreamRuletoString(k, v)));
- } catch (ErrnoException e) {
+ } catch (ErrnoException | IOException e) {
pw.println("Error dumping IPv6 upstream map: " + e);
}
}
- private String ipv4RuleToBase64String(Tether4Key key, Tether4Value value) {
- final byte[] keyBytes = key.writeToBytes();
- final String keyBase64Str = Base64.encodeToString(keyBytes, Base64.DEFAULT)
- .replace("\n", "");
- final byte[] valueBytes = value.writeToBytes();
- final String valueBase64Str = Base64.encodeToString(valueBytes, Base64.DEFAULT)
- .replace("\n", "");
-
- return keyBase64Str + DUMP_BASE64_DELIMITER + valueBase64Str;
- }
-
- private void dumpRawIpv4ForwardingRuleMap(
- BpfMap<Tether4Key, Tether4Value> map, IndentingPrintWriter pw) throws ErrnoException {
+ private <K extends Struct, V extends Struct> void dumpRawMap(BpfMap<K, V> map,
+ IndentingPrintWriter pw) throws ErrnoException {
if (map == null) {
- pw.println("No IPv4 support");
+ pw.println("No BPF support");
return;
}
if (map.isEmpty()) {
- pw.println("No rules");
+ pw.println("No entries");
return;
}
- map.forEach((k, v) -> pw.println(ipv4RuleToBase64String(k, v)));
+ map.forEach((k, v) -> pw.println(BpfDump.toBase64EncodedString(k, v)));
}
/**
- * Dump raw BPF map in base64 encoded strings. For test only.
+ * Dump raw BPF map into the base64 encoded strings "<base64 key>,<base64 value>".
+ * Allow to dump only one map path once. For test only.
+ *
+ * Usage:
+ * $ dumpsys tethering bpfRawMap --<map name>
+ *
+ * Output:
+ * <base64 encoded key #1>,<base64 encoded value #1>
+ * <base64 encoded key #2>,<base64 encoded value #2>
+ * ..
*/
- public void dumpRawMap(@NonNull IndentingPrintWriter pw) {
- try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
- // TODO: dump downstream map.
- dumpRawIpv4ForwardingRuleMap(upstreamMap, pw);
- } catch (ErrnoException e) {
- pw.println("Error dumping IPv4 map: " + e);
+ public void dumpRawMap(@NonNull IndentingPrintWriter pw, @Nullable String[] args) {
+ // TODO: consider checking the arg order that <map name> is after "bpfRawMap". Probably
+ // it is okay for now because this is used by test only and test is supposed to use
+ // expected argument order.
+ // TODO: dump downstream4 map.
+ if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_STATS)) {
+ try (BpfMap<TetherStatsKey, TetherStatsValue> statsMap = mDeps.getBpfStatsMap()) {
+ dumpRawMap(statsMap, pw);
+ } catch (ErrnoException | IOException e) {
+ pw.println("Error dumping stats map: " + e);
+ }
+ return;
+ }
+ if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_UPSTREAM4)) {
+ try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
+ dumpRawMap(upstreamMap, pw);
+ } catch (ErrnoException | IOException e) {
+ pw.println("Error dumping IPv4 map: " + e);
+ }
+ return;
}
}
@@ -1172,7 +1189,7 @@
pw.increaseIndent();
dumpIpv4ForwardingRuleMap(now, DOWNSTREAM, downstreamMap, pw);
pw.decreaseIndent();
- } catch (ErrnoException e) {
+ } catch (ErrnoException | IOException e) {
pw.println("Error dumping IPv4 map: " + e);
}
}
@@ -1197,7 +1214,7 @@
}
if (v.val > 0) pw.println(String.format("%s: %d", counterName, v.val));
});
- } catch (ErrnoException e) {
+ } catch (ErrnoException | IOException e) {
pw.println("Error dumping counter map: " + e);
}
}
@@ -1221,7 +1238,7 @@
pw.println(String.format("%d (%s) -> %d (%s)", k.ifIndex, getIfName(k.ifIndex),
v.ifIndex, getIfName(v.ifIndex)));
});
- } catch (ErrnoException e) {
+ } catch (ErrnoException | IOException e) {
pw.println("Error dumping dev map: " + e);
}
pw.decreaseIndent();
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index 844efde..741af5c 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -34,7 +34,6 @@
import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
import static com.android.networkstack.apishim.ConstantsShim.ACTION_TETHER_UNSUPPORTED_CARRIER_UI;
-import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
import android.app.AlarmManager;
import android.app.PendingIntent;
@@ -44,20 +43,18 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
-import android.net.util.SharedLog;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
-import android.os.PersistableBundle;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.provider.Settings;
-import android.telephony.CarrierConfigManager;
import android.util.SparseIntArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.SharedLog;
import java.io.PrintWriter;
import java.util.BitSet;
@@ -307,13 +304,13 @@
if (SystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)) {
return TETHERING_PROVISIONING_NOT_REQUIRED;
}
- // TODO: Find a way to avoid get carrier config twice.
- if (carrierConfigAffirmsCarrierNotSupport(config)) {
+
+ if (!config.isCarrierSupportTethering) {
// To block tethering, behave as if running provisioning check and failed.
return TETHERING_PROVISIONING_CARRIER_UNSUPPORT;
}
- if (carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
+ if (!config.isCarrierConfigAffirmsEntitlementCheckRequired) {
return TETHERING_PROVISIONING_NOT_REQUIRED;
}
return (config.provisioningApp.length == 2)
@@ -380,57 +377,6 @@
}
/**
- * Get carrier configuration bundle.
- * @param config an object that encapsulates the various tethering configuration elements.
- * */
- public PersistableBundle getCarrierConfig(final TetheringConfiguration config) {
- final CarrierConfigManager configManager = mContext
- .getSystemService(CarrierConfigManager.class);
- if (configManager == null) return null;
-
- final PersistableBundle carrierConfig = configManager.getConfigForSubId(
- config.activeDataSubId);
-
- if (CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) {
- return carrierConfig;
- }
-
- return null;
- }
-
- // The logic here is aimed solely at confirming that a CarrierConfig exists
- // and affirms that entitlement checks are not required.
- //
- // TODO: find a better way to express this, or alter the checking process
- // entirely so that this is more intuitive.
- // TODO: Find a way to avoid using getCarrierConfig everytime.
- private boolean carrierConfigAffirmsEntitlementCheckNotRequired(
- final TetheringConfiguration config) {
- // Check carrier config for entitlement checks
- final PersistableBundle carrierConfig = getCarrierConfig(config);
- if (carrierConfig == null) return false;
-
- // A CarrierConfigManager was found and it has a config.
- final boolean isEntitlementCheckRequired = carrierConfig.getBoolean(
- CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL);
- return !isEntitlementCheckRequired;
- }
-
- private boolean carrierConfigAffirmsCarrierNotSupport(final TetheringConfiguration config) {
- if (!SdkLevel.isAtLeastT()) {
- return false;
- }
- // Check carrier config for entitlement checks
- final PersistableBundle carrierConfig = getCarrierConfig(config);
- if (carrierConfig == null) return false;
-
- // A CarrierConfigManager was found and it has a config.
- final boolean mIsCarrierSupport = carrierConfig.getBoolean(
- KEY_CARRIER_SUPPORTS_TETHERING_BOOL, true);
- return !mIsCarrierSupport;
- }
-
- /**
* Run no UI tethering provisioning check.
* @param type tethering type from TetheringManager.TETHERING_{@code *}
* @param subId default data subscription ID.
@@ -479,7 +425,7 @@
private void runTetheringProvisioning(
boolean showProvisioningUi, int downstreamType, final TetheringConfiguration config) {
- if (carrierConfigAffirmsCarrierNotSupport(config)) {
+ if (!config.isCarrierSupportTethering) {
mListener.onTetherProvisioningFailed(downstreamType, "Carrier does not support.");
if (showProvisioningUi) {
showCarrierUnsupportedDialog();
@@ -497,7 +443,7 @@
}
private void showCarrierUnsupportedDialog() {
- // This is only used when carrierConfigAffirmsCarrierNotSupport() is true.
+ // This is only used when TetheringConfiguration.isCarrierSupportTethering is false.
if (!SdkLevel.isAtLeastT()) {
return;
}
diff --git a/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java b/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java
index f3dcaa2..ab3929d 100644
--- a/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java
@@ -24,9 +24,10 @@
import android.net.RouteInfo;
import android.net.ip.IpServer;
import android.net.util.NetworkConstants;
-import android.net.util.SharedLog;
import android.util.Log;
+import com.android.net.module.util.SharedLog;
+
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadController.java b/Tethering/src/com/android/networkstack/tethering/OffloadController.java
index d60c21d..94684af 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadController.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadController.java
@@ -43,7 +43,6 @@
import android.net.NetworkStats.Entry;
import android.net.RouteInfo;
import android.net.netstats.provider.NetworkStatsProvider;
-import android.net.util.SharedLog;
import android.os.Handler;
import android.provider.Settings;
import android.system.ErrnoException;
@@ -53,6 +52,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.SharedLog;
import com.android.net.module.util.netlink.ConntrackMessage;
import com.android.net.module.util.netlink.NetlinkConstants;
import com.android.net.module.util.netlink.NetlinkSocket;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index 9da66d8..fbb342d 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -28,7 +28,6 @@
import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent;
import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback;
-import android.net.util.SharedLog;
import android.net.util.SocketUtils;
import android.os.Handler;
import android.os.NativeHandle;
@@ -40,6 +39,7 @@
import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
import com.android.net.module.util.netlink.NetlinkSocket;
import com.android.net.module.util.netlink.StructNfGenMsg;
import com.android.net.module.util.netlink.StructNlMsgHdr;
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index cc2422f..41a10ae 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -172,6 +172,9 @@
return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS);
}
+ // This ensures that tethering isn't started on 2 different interfaces with the same type.
+ // Once tethering could support multiple interface with the same type,
+ // TetheringSoftApCallback would need to handle it among others.
final LinkAddress cachedAddress = mCachedAddresses.get(ipServer.interfaceType());
if (useLastAddress && cachedAddress != null
&& !isConflictWithUpstream(asIpPrefix(cachedAddress))) {
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 0b607bd..3f328db 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -99,7 +99,6 @@
import android.net.TetheringRequestParcel;
import android.net.ip.IpServer;
import android.net.shared.NetdUtils;
-import android.net.util.SharedLog;
import android.net.wifi.WifiClient;
import android.net.wifi.WifiManager;
import android.net.wifi.p2p.WifiP2pGroup;
@@ -135,10 +134,13 @@
import com.android.internal.util.StateMachine;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.common.BluetoothPanShim;
import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.networkstack.tethering.util.PrefixUtils;
import com.android.networkstack.tethering.util.TetheringUtils;
@@ -253,6 +255,7 @@
private final UserManager mUserManager;
private final BpfCoordinator mBpfCoordinator;
private final PrivateAddressCoordinator mPrivateAddressCoordinator;
+ private final TetheringMetrics mTetheringMetrics;
private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
private volatile TetheringConfiguration mConfig;
@@ -286,6 +289,7 @@
mNetd = mDeps.getINetd(mContext);
mLooper = mDeps.getTetheringLooper();
mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
+ mTetheringMetrics = mDeps.getTetheringMetrics();
// This is intended to ensrure that if something calls startTethering(bluetooth) just after
// bluetooth is enabled. Before onServiceConnected is called, store the calls into this
@@ -439,8 +443,22 @@
mStateReceiver, noUpstreamFilter, PERMISSION_MAINLINE_NETWORK_STACK, mHandler);
final WifiManager wifiManager = getWifiManager();
+ TetheringSoftApCallback softApCallback = new TetheringSoftApCallback();
if (wifiManager != null) {
- wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback());
+ wifiManager.registerSoftApCallback(mExecutor, softApCallback);
+ }
+ if (SdkLevel.isAtLeastT() && wifiManager != null) {
+ try {
+ // Although WifiManager#registerLocalOnlyHotspotSoftApCallback document that it need
+ // NEARBY_WIFI_DEVICES permission, but actually a caller who have NETWORK_STACK
+ // or MAINLINE_NETWORK_STACK permission would also able to use this API.
+ wifiManager.registerLocalOnlyHotspotSoftApCallback(mExecutor, softApCallback);
+ } catch (UnsupportedOperationException e) {
+ // Since wifi module development in internal branch,
+ // #registerLocalOnlyHotspotSoftApCallback currently doesn't supported in AOSP
+ // before AOSP switch to Android T + 1.
+ Log.wtf(TAG, "registerLocalOnlyHotspotSoftApCallback API is not supported");
+ }
}
startTrackDefaultNetwork();
@@ -476,7 +494,7 @@
// To avoid launching unexpected provisioning checks, ignore re-provisioning
// when no CarrierConfig loaded yet. Assume reevaluateSimCardProvisioning()
// will be triggered again when CarrierConfig is loaded.
- if (mEntitlementMgr.getCarrierConfig(mConfig) != null) {
+ if (TetheringConfiguration.getCarrierConfig(mContext, subId) != null) {
mEntitlementMgr.reevaluateSimCardProvisioning(mConfig);
} else {
mLog.log("IGNORED reevaluate provisioning, no carrier config loaded");
@@ -534,6 +552,13 @@
}
// Called by wifi when the number of soft AP clients changed.
+ // Currently multiple softAp would not behave well in PrivateAddressCoordinator
+ // (where it gets the address from cache), it ensure tethering only support one ipServer for
+ // TETHERING_WIFI. Once tethering support multiple softAp enabled simultaneously,
+ // onConnectedClientsChanged should also be updated to support tracking different softAp's
+ // clients individually.
+ // TODO: Add wtf log and have check to reject request duplicated type with different
+ // interface.
@Override
public void onConnectedClientsChanged(final List<WifiClient> clients) {
updateConnectedClients(clients);
@@ -608,7 +633,8 @@
processInterfaceStateChange(iface, false /* enabled */);
}
- void startTethering(final TetheringRequestParcel request, final IIntResultListener listener) {
+ void startTethering(final TetheringRequestParcel request, final String callerPkg,
+ final IIntResultListener listener) {
mHandler.post(() -> {
final TetheringRequestParcel unfinishedRequest = mActiveTetheringRequests.get(
request.tetheringType);
@@ -628,6 +654,7 @@
request.showProvisioningUi);
}
enableTetheringInternal(request.tetheringType, true /* enabled */, listener);
+ mTetheringMetrics.createBuilder(request.tetheringType, callerPkg);
});
}
@@ -687,7 +714,11 @@
// If changing tethering fail, remove corresponding request
// no matter who trigger the start/stop.
- if (result != TETHER_ERROR_NO_ERROR) mActiveTetheringRequests.remove(type);
+ if (result != TETHER_ERROR_NO_ERROR) {
+ mActiveTetheringRequests.remove(type);
+ mTetheringMetrics.updateErrorCode(type, result);
+ mTetheringMetrics.sendReport(type);
+ }
}
private int setWifiTethering(final boolean enable) {
@@ -1280,7 +1311,7 @@
// Finally bring up serving on the new interface
mWifiP2pTetherInterface = group.getInterface();
- enableWifiIpServing(mWifiP2pTetherInterface, IFACE_IP_MODE_LOCAL_ONLY);
+ enableWifiP2pIpServing(mWifiP2pTetherInterface);
}
private void handleUserRestrictionAction() {
@@ -1368,23 +1399,27 @@
private void enableIpServing(int tetheringType, String ifname, int ipServingMode,
boolean isNcm) {
ensureIpServerStarted(ifname, tetheringType, isNcm);
- changeInterfaceState(ifname, ipServingMode);
+ if (tether(ifname, ipServingMode) != TETHER_ERROR_NO_ERROR) {
+ Log.e(TAG, "unable start tethering on iface " + ifname);
+ }
}
- private void disableWifiIpServingCommon(int tetheringType, String ifname, int apState) {
- mLog.log("Canceling WiFi tethering request -"
- + " type=" + tetheringType
- + " interface=" + ifname
- + " state=" + apState);
-
- if (!TextUtils.isEmpty(ifname)) {
- final TetherState ts = mTetherStates.get(ifname);
- if (ts != null) {
- ts.ipServer.unwanted();
- return;
- }
+ private void disableWifiIpServingCommon(int tetheringType, String ifname) {
+ if (!TextUtils.isEmpty(ifname) && mTetherStates.containsKey(ifname)) {
+ mTetherStates.get(ifname).ipServer.unwanted();
+ return;
}
+ if (SdkLevel.isAtLeastT()) {
+ mLog.e("Tethering no longer handle untracked interface after T: " + ifname);
+ return;
+ }
+
+ // Attempt to guess the interface name before T. Pure AOSP code should never enter here
+ // because WIFI_AP_STATE_CHANGED intent always include ifname and it should be tracked
+ // by mTetherStates. In case OEMs have some modification in wifi side which pass null
+ // or empty ifname. Before T, tethering allow to disable the first wifi ipServer if
+ // given ifname don't match any tracking ipServer.
for (int i = 0; i < mTetherStates.size(); i++) {
final IpServer ipServer = mTetherStates.valueAt(i).ipServer;
if (ipServer.interfaceType() == tetheringType) {
@@ -1392,7 +1427,6 @@
return;
}
}
-
mLog.log("Error disabling Wi-Fi IP serving; "
+ (TextUtils.isEmpty(ifname) ? "no interface name specified"
: "specified interface: " + ifname));
@@ -1401,20 +1435,39 @@
private void disableWifiIpServing(String ifname, int apState) {
// Regardless of whether we requested this transition, the AP has gone
// down. Don't try to tether again unless we're requested to do so.
- // TODO: Remove this altogether, once Wi-Fi reliably gives us an
- // interface name with every broadcast.
mWifiTetherRequested = false;
- disableWifiIpServingCommon(TETHERING_WIFI, ifname, apState);
+ mLog.log("Canceling WiFi tethering request - interface=" + ifname + " state=" + apState);
+
+ disableWifiIpServingCommon(TETHERING_WIFI, ifname);
+ }
+
+ private void enableWifiP2pIpServing(String ifname) {
+ if (TextUtils.isEmpty(ifname)) {
+ mLog.e("Cannot enable P2P IP serving with invalid interface");
+ return;
+ }
+
+ // After T, tethering always trust the iface pass by state change intent. This allow
+ // tethering to deprecate tetherable p2p regexs after T.
+ final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI_P2P : ifaceNameToType(ifname);
+ if (!checkTetherableType(type)) {
+ mLog.e(ifname + " is not a tetherable iface, ignoring");
+ return;
+ }
+ enableIpServing(type, ifname, IpServer.STATE_LOCAL_ONLY);
}
private void disableWifiP2pIpServingIfNeeded(String ifname) {
if (TextUtils.isEmpty(ifname)) return;
- disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname, /* fake */ 0);
+ mLog.log("Canceling P2P tethering request - interface=" + ifname);
+ disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname);
}
private void enableWifiIpServing(String ifname, int wifiIpMode) {
+ mLog.log("request WiFi tethering - interface=" + ifname + " state=" + wifiIpMode);
+
// Map wifiIpMode values to IpServer.Callback serving states, inferring
// from mWifiTetherRequested as a final "best guess".
final int ipServingMode;
@@ -1430,13 +1483,18 @@
return;
}
+ // After T, tethering always trust the iface pass by state change intent. This allow
+ // tethering to deprecate tetherable wifi regexs after T.
+ final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
+ if (!checkTetherableType(type)) {
+ mLog.e(ifname + " is not a tetherable iface, ignoring");
+ return;
+ }
+
if (!TextUtils.isEmpty(ifname)) {
- ensureIpServerStarted(ifname);
- changeInterfaceState(ifname, ipServingMode);
+ enableIpServing(type, ifname, ipServingMode);
} else {
- mLog.e(String.format(
- "Cannot enable IP serving in mode %s on missing interface name",
- ipServingMode));
+ mLog.e("Cannot enable IP serving on missing interface name");
}
}
@@ -1488,27 +1546,6 @@
}
}
- private void changeInterfaceState(String ifname, int requestedState) {
- final int result;
- switch (requestedState) {
- case IpServer.STATE_UNAVAILABLE:
- case IpServer.STATE_AVAILABLE:
- result = untether(ifname);
- break;
- case IpServer.STATE_TETHERED:
- case IpServer.STATE_LOCAL_ONLY:
- result = tether(ifname, requestedState);
- break;
- default:
- Log.wtf(TAG, "Unknown interface state: " + requestedState);
- return;
- }
- if (result != TETHER_ERROR_NO_ERROR) {
- Log.e(TAG, "unable start or stop tethering on iface " + ifname);
- return;
- }
- }
-
TetheringConfiguration getTetheringConfiguration() {
return mConfig;
}
@@ -2472,13 +2509,12 @@
writer, " ");
// Used for testing instead of human debug.
- // TODO: add options to choose which map to dump.
- if (argsContain(args, "bpfRawMap")) {
- mBpfCoordinator.dumpRawMap(pw);
+ if (CollectionUtils.contains(args, "bpfRawMap")) {
+ mBpfCoordinator.dumpRawMap(pw, args);
return;
}
- if (argsContain(args, "bpf")) {
+ if (CollectionUtils.contains(args, "bpf")) {
dumpBpf(pw);
return;
}
@@ -2544,7 +2580,7 @@
pw.println("Log:");
pw.increaseIndent();
- if (argsContain(args, "--short")) {
+ if (CollectionUtils.contains(args, "--short")) {
pw.println("<log removed for brevity>");
} else {
mLog.dump(fd, pw, args);
@@ -2588,13 +2624,6 @@
if (e != null) throw e;
}
- private static boolean argsContain(String[] args, String target) {
- for (String arg : args) {
- if (target.equals(arg)) return true;
- }
- return false;
- }
-
private void updateConnectedClients(final List<WifiClient> wifiClients) {
if (mConnectedClientsTracker.updateConnectedClients(mTetherMainSM.getAllDownstreams(),
wifiClients)) {
@@ -2681,23 +2710,28 @@
mTetherMainSM.sendMessage(which, state, 0, newLp);
}
+ private boolean hasSystemFeature(final String feature) {
+ return mContext.getPackageManager().hasSystemFeature(feature);
+ }
+
+ private boolean checkTetherableType(int type) {
+ if ((type == TETHERING_WIFI || type == TETHERING_WIGIG)
+ && !hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+ return false;
+ }
+
+ if (type == TETHERING_WIFI_P2P && !hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
+ return false;
+ }
+
+ return type != TETHERING_INVALID;
+ }
+
private void ensureIpServerStarted(final String iface) {
// If we don't care about this type of interface, ignore.
final int interfaceType = ifaceNameToType(iface);
- if (interfaceType == TETHERING_INVALID) {
- mLog.log(iface + " is not a tetherable iface, ignoring");
- return;
- }
-
- final PackageManager pm = mContext.getPackageManager();
- if ((interfaceType == TETHERING_WIFI || interfaceType == TETHERING_WIGIG)
- && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
- mLog.log(iface + " is not tetherable, because WiFi feature is disabled");
- return;
- }
- if (interfaceType == TETHERING_WIFI_P2P
- && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
- mLog.log(iface + " is not tetherable, because WiFi Direct feature is disabled");
+ if (!checkTetherableType(interfaceType)) {
+ mLog.log(iface + " is used for " + interfaceType + " which is not tetherable");
return;
}
@@ -2715,7 +2749,7 @@
final TetherState tetherState = new TetherState(
new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
makeControlCallback(), mConfig, mPrivateAddressCoordinator,
- mDeps.getIpServerDependencies()), isNcm);
+ mTetheringMetrics, mDeps.getIpServerDependencies()), isNcm);
mTetherStates.put(iface, tetherState);
tetherState.ipServer.start();
}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index f9f3ed9..696a970 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -24,14 +24,16 @@
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.net.TetheringConfigurationParcel;
-import android.net.util.SharedLog;
+import android.os.PersistableBundle;
import android.provider.DeviceConfig;
import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -39,6 +41,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.SharedLog;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -142,6 +145,9 @@
public final int provisioningCheckPeriod;
public final String provisioningResponse;
+ public final boolean isCarrierSupportTethering;
+ public final boolean isCarrierConfigAffirmsEntitlementCheckRequired;
+
public final int activeDataSubId;
private final boolean mEnableLegacyDhcpServer;
@@ -207,6 +213,11 @@
provisioningResponse = getResourceString(res,
R.string.config_mobile_hotspot_provision_response);
+ PersistableBundle carrierConfigs = getCarrierConfig(ctx, activeDataSubId);
+ isCarrierSupportTethering = carrierConfigAffirmsCarrierSupport(carrierConfigs);
+ isCarrierConfigAffirmsEntitlementCheckRequired =
+ carrierConfigAffirmsEntitlementCheckRequired(carrierConfigs);
+
mOffloadPollInterval = getResourceInteger(res,
R.integer.config_tether_offload_poll_interval,
DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
@@ -329,6 +340,10 @@
pw.print("provisioningAppNoUi: ");
pw.println(provisioningAppNoUi);
+ pw.println("isCarrierSupportTethering: " + isCarrierSupportTethering);
+ pw.println("isCarrierConfigAffirmsEntitlementCheckRequired: "
+ + isCarrierConfigAffirmsEntitlementCheckRequired);
+
pw.print("enableBpfOffload: ");
pw.println(mEnableBpfOffload);
@@ -361,6 +376,9 @@
toIntArray(preferredUpstreamIfaceTypes)));
sj.add(String.format("provisioningApp:%s", makeString(provisioningApp)));
sj.add(String.format("provisioningAppNoUi:%s", provisioningAppNoUi));
+ sj.add(String.format("isCarrierSupportTethering:%s", isCarrierSupportTethering));
+ sj.add(String.format("isCarrierConfigAffirmsEntitlementCheckRequired:%s",
+ isCarrierConfigAffirmsEntitlementCheckRequired));
sj.add(String.format("enableBpfOffload:%s", mEnableBpfOffload));
sj.add(String.format("enableLegacyDhcpServer:%s", mEnableLegacyDhcpServer));
return String.format("TetheringConfiguration{%s}", sj.toString());
@@ -596,6 +614,39 @@
return result;
}
+ private static boolean carrierConfigAffirmsEntitlementCheckRequired(
+ PersistableBundle carrierConfig) {
+ if (carrierConfig == null) {
+ return true;
+ }
+ return carrierConfig.getBoolean(
+ CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true);
+ }
+
+ private static boolean carrierConfigAffirmsCarrierSupport(PersistableBundle carrierConfig) {
+ if (!SdkLevel.isAtLeastT() || carrierConfig == null) {
+ return true;
+ }
+ return carrierConfig.getBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, true);
+ }
+
+ /**
+ * Get carrier configuration bundle.
+ */
+ public static PersistableBundle getCarrierConfig(Context context, int activeDataSubId) {
+ final CarrierConfigManager configManager =
+ context.getSystemService(CarrierConfigManager.class);
+ if (configManager == null) {
+ return null;
+ }
+
+ final PersistableBundle carrierConfig = configManager.getConfigForSubId(activeDataSubId);
+ if (CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) {
+ return carrierConfig;
+ }
+ return null;
+ }
+
/**
* Convert this TetheringConfiguration to a TetheringConfigurationParcel.
*/
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 9224213..611d1cf 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -22,7 +22,6 @@
import android.content.Context;
import android.net.INetd;
import android.net.ip.IpServer;
-import android.net.util.SharedLog;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -32,8 +31,10 @@
import androidx.annotation.NonNull;
import com.android.internal.util.StateMachine;
+import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.BluetoothPanShimImpl;
import com.android.networkstack.apishim.common.BluetoothPanShim;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
import java.util.ArrayList;
@@ -163,4 +164,11 @@
public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) {
return BluetoothPanShimImpl.newInstance(pan);
}
+
+ /**
+ * Get a reference to the TetheringMetrics to be used by tethering.
+ */
+ public TetheringMetrics getTetheringMetrics() {
+ return new TetheringMetrics();
+ }
}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 9fb61fe..f147e10 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -137,7 +137,7 @@
return;
}
- mTethering.startTethering(request, listener);
+ mTethering.startTethering(request, callerPkg, listener);
}
@Override
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index f8dd673..16c031b 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -36,7 +36,6 @@
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
-import android.net.util.SharedLog;
import android.os.Handler;
import android.util.Log;
import android.util.SparseIntArray;
@@ -46,6 +45,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.StateMachine;
+import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
import com.android.networkstack.apishim.common.ConnectivityManagerShim;
import com.android.networkstack.tethering.util.PrefixUtils;
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
new file mode 100644
index 0000000..d8e631e
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering.metrics;
+
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_DISABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
+import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
+import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+
+import android.stats.connectivity.DownstreamType;
+import android.stats.connectivity.ErrorCode;
+import android.stats.connectivity.UpstreamType;
+import android.stats.connectivity.UserType;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Collection of utilities for tethering metrics.
+ *
+ * To see if the logs are properly sent to statsd, execute following commands
+ *
+ * $ adb shell cmd stats print-logs
+ * $ adb logcat | grep statsd OR $ adb logcat -b stats
+ *
+ * @hide
+ */
+public class TetheringMetrics {
+ private static final String TAG = TetheringMetrics.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final String SETTINGS_PKG_NAME = "com.android.settings";
+ private static final String SYSTEMUI_PKG_NAME = "com.android.systemui";
+ private static final String GMS_PKG_NAME = "com.google.android.gms";
+ private final SparseArray<NetworkTetheringReported.Builder> mBuilderMap = new SparseArray<>();
+
+ /** Update Tethering stats about caller's package name and downstream type. */
+ public void createBuilder(final int downstreamType, final String callerPkg) {
+ NetworkTetheringReported.Builder statsBuilder =
+ NetworkTetheringReported.newBuilder();
+ statsBuilder.setDownstreamType(downstreamTypeToEnum(downstreamType))
+ .setUserType(userTypeToEnum(callerPkg))
+ .setUpstreamType(UpstreamType.UT_UNKNOWN)
+ .setErrorCode(ErrorCode.EC_NO_ERROR)
+ .build();
+ mBuilderMap.put(downstreamType, statsBuilder);
+ }
+
+ /** Update error code of given downstreamType. */
+ public void updateErrorCode(final int downstreamType, final int errCode) {
+ NetworkTetheringReported.Builder statsBuilder = mBuilderMap.get(downstreamType);
+ if (statsBuilder == null) {
+ Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
+ return;
+ }
+ statsBuilder.setErrorCode(errorCodeToEnum(errCode));
+ }
+
+ /** Remove Tethering stats.
+ * If Tethering stats is ready to write then write it before removing.
+ */
+ public void sendReport(final int downstreamType) {
+ final NetworkTetheringReported.Builder statsBuilder =
+ mBuilderMap.get(downstreamType);
+ if (statsBuilder == null) {
+ Log.e(TAG, "Given downstreamType does not exist, this is a bug!");
+ return;
+ }
+ write(statsBuilder.build());
+ mBuilderMap.remove(downstreamType);
+ }
+
+ /** Collect Tethering stats and write metrics data to statsd pipeline. */
+ @VisibleForTesting
+ public void write(@NonNull final NetworkTetheringReported reported) {
+ TetheringStatsLog.write(TetheringStatsLog.NETWORK_TETHERING_REPORTED,
+ reported.getErrorCode().getNumber(),
+ reported.getDownstreamType().getNumber(),
+ reported.getUpstreamType().getNumber(),
+ reported.getUserType().getNumber());
+ if (DBG) {
+ Log.d(TAG, "Write errorCode: " + reported.getErrorCode().getNumber()
+ + ", downstreamType: " + reported.getDownstreamType().getNumber()
+ + ", upstreamType: " + reported.getUpstreamType().getNumber()
+ + ", userType: " + reported.getUserType().getNumber());
+ }
+ }
+
+ /** Map {@link TetheringType} to {@link DownstreamType} */
+ private DownstreamType downstreamTypeToEnum(final int ifaceType) {
+ switch(ifaceType) {
+ case TETHERING_WIFI:
+ return DownstreamType.DS_TETHERING_WIFI;
+ case TETHERING_WIFI_P2P:
+ return DownstreamType.DS_TETHERING_WIFI_P2P;
+ case TETHERING_USB:
+ return DownstreamType.DS_TETHERING_USB;
+ case TETHERING_BLUETOOTH:
+ return DownstreamType.DS_TETHERING_BLUETOOTH;
+ case TETHERING_NCM:
+ return DownstreamType.DS_TETHERING_NCM;
+ case TETHERING_ETHERNET:
+ return DownstreamType.DS_TETHERING_ETHERNET;
+ default:
+ return DownstreamType.DS_UNSPECIFIED;
+ }
+ }
+
+ /** Map {@link StartTetheringError} to {@link ErrorCode} */
+ private ErrorCode errorCodeToEnum(final int lastError) {
+ switch(lastError) {
+ case TETHER_ERROR_NO_ERROR:
+ return ErrorCode.EC_NO_ERROR;
+ case TETHER_ERROR_UNKNOWN_IFACE:
+ return ErrorCode.EC_UNKNOWN_IFACE;
+ case TETHER_ERROR_SERVICE_UNAVAIL:
+ return ErrorCode.EC_SERVICE_UNAVAIL;
+ case TETHER_ERROR_UNSUPPORTED:
+ return ErrorCode.EC_UNSUPPORTED;
+ case TETHER_ERROR_UNAVAIL_IFACE:
+ return ErrorCode.EC_UNAVAIL_IFACE;
+ case TETHER_ERROR_INTERNAL_ERROR:
+ return ErrorCode.EC_INTERNAL_ERROR;
+ case TETHER_ERROR_TETHER_IFACE_ERROR:
+ return ErrorCode.EC_TETHER_IFACE_ERROR;
+ case TETHER_ERROR_UNTETHER_IFACE_ERROR:
+ return ErrorCode.EC_UNTETHER_IFACE_ERROR;
+ case TETHER_ERROR_ENABLE_FORWARDING_ERROR:
+ return ErrorCode.EC_ENABLE_FORWARDING_ERROR;
+ case TETHER_ERROR_DISABLE_FORWARDING_ERROR:
+ return ErrorCode.EC_DISABLE_FORWARDING_ERROR;
+ case TETHER_ERROR_IFACE_CFG_ERROR:
+ return ErrorCode.EC_IFACE_CFG_ERROR;
+ case TETHER_ERROR_PROVISIONING_FAILED:
+ return ErrorCode.EC_PROVISIONING_FAILED;
+ case TETHER_ERROR_DHCPSERVER_ERROR:
+ return ErrorCode.EC_DHCPSERVER_ERROR;
+ case TETHER_ERROR_ENTITLEMENT_UNKNOWN:
+ return ErrorCode.EC_ENTITLEMENT_UNKNOWN;
+ case TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION:
+ return ErrorCode.EC_NO_CHANGE_TETHERING_PERMISSION;
+ case TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION:
+ return ErrorCode.EC_NO_ACCESS_TETHERING_PERMISSION;
+ default:
+ return ErrorCode.EC_UNKNOWN_TYPE;
+ }
+ }
+
+ /** Map callerPkg to {@link UserType} */
+ private UserType userTypeToEnum(final String callerPkg) {
+ if (callerPkg.equals(SETTINGS_PKG_NAME)) {
+ return UserType.USER_SETTINGS;
+ } else if (callerPkg.equals(SYSTEMUI_PKG_NAME)) {
+ return UserType.USER_SYSTEMUI;
+ } else if (callerPkg.equals(GMS_PKG_NAME)) {
+ return UserType.USER_GMS;
+ } else {
+ return UserType.USER_UNKNOWN;
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
index 66d67a1..e6236df 100644
--- a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java
@@ -22,7 +22,7 @@
import androidx.annotation.NonNull;
import com.android.net.module.util.JniUtil;
-import com.android.networkstack.tethering.TetherStatsValue;
+import com.android.net.module.util.bpf.TetherStatsValue;
import java.io.FileDescriptor;
import java.net.Inet6Address;
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index a4d0448..ca8d3de 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -32,6 +32,7 @@
"net-tests-utils",
"net-utils-device-common-bpf",
"testables",
+ "connectivity-net-module-utils-bpf",
],
libs: [
"android.test.runner",
@@ -49,7 +50,7 @@
// Use with NetworkStackJarJarRules.
android_library {
name: "TetheringIntegrationTestsLatestSdkLib",
- target_sdk_version: "31",
+ target_sdk_version: "33",
platform_apis: true,
defaults: ["TetheringIntegrationTestsDefaults"],
visibility: [
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index de81a38..86dca1c 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -26,19 +26,25 @@
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
import static android.net.TetheringManager.TETHERING_ETHERNET;
-import static android.net.TetheringTester.RemoteResponder;
-import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.net.TetheringTester.isExpectedIcmpv6Packet;
+import static android.net.TetheringTester.isExpectedUdpPacket;
import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_IPV6;
import static android.system.OsConstants.IPPROTO_UDP;
+import static com.android.net.module.util.BpfDump.BASE64_DELIMITER;
import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
import static com.android.net.module.util.HexDump.dumpHexString;
import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.testutils.DeviceInfoUtils.KVersion;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
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;
@@ -59,6 +65,7 @@
import android.os.HandlerThread;
import android.os.SystemClock;
import android.os.SystemProperties;
+import android.os.VintfRuntimeInfo;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
@@ -70,18 +77,18 @@
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.Ipv6Utils;
import com.android.net.module.util.PacketBuilder;
import com.android.net.module.util.Struct;
import com.android.net.module.util.bpf.Tether4Key;
import com.android.net.module.util.bpf.Tether4Value;
-import com.android.net.module.util.structs.EthernetHeader;
-import com.android.net.module.util.structs.Icmpv6Header;
-import com.android.net.module.util.structs.Ipv4Header;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
import com.android.net.module.util.structs.Ipv6Header;
-import com.android.net.module.util.structs.UdpHeader;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DeviceInfoUtils;
import com.android.testutils.DumpTestUtils;
import com.android.testutils.HandlerUtils;
import com.android.testutils.TapPacketReader;
@@ -95,6 +102,7 @@
import java.io.FileDescriptor;
import java.net.Inet4Address;
+import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
@@ -128,17 +136,33 @@
// Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
// See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
private static final int UDP_STREAM_TS_MS = 2000;
+ // Per RX UDP packet size: iphdr (20) + udphdr (8) + payload (2) = 30 bytes.
+ private static final int RX_UDP_PACKET_SIZE = 30;
+ private static final int RX_UDP_PACKET_COUNT = 456;
+ // Per TX UDP packet size: ethhdr (14) + iphdr (20) + udphdr (8) + payload (2) = 44 bytes.
+ private static final int TX_UDP_PACKET_SIZE = 44;
+ private static final int TX_UDP_PACKET_COUNT = 123;
+ private static final long WAIT_RA_TIMEOUT_MS = 2000;
+
private static final LinkAddress TEST_IP4_ADDR = new LinkAddress("10.0.0.1/8");
private static final LinkAddress TEST_IP6_ADDR = new LinkAddress("2001:db8:1::101/64");
private static final InetAddress TEST_IP4_DNS = parseNumericAddress("8.8.8.8");
private static final InetAddress TEST_IP6_DNS = parseNumericAddress("2001:db8:1::888");
+ private static final IpPrefix TEST_NAT64PREFIX = new IpPrefix("64:ff9b::/96");
+ private static final Inet6Address REMOTE_NAT64_ADDR =
+ (Inet6Address) parseNumericAddress("64:ff9b::808:808");
private static final ByteBuffer TEST_REACHABILITY_PAYLOAD =
ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa });
private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
- private static final String BASE64_DELIMITER = ",";
+ private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
+ private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
private static final String LINE_DELIMITER = "\\n";
+ // version=6, traffic class=0x0, flowlabel=0x0;
+ private static final int VERSION_TRAFFICCLASS_FLOWLABEL = 0x60000000;
+ private static final short HOP_LIMIT = 0x40;
+
private final Context mContext = InstrumentationRegistry.getContext();
private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
@@ -300,27 +324,13 @@
}
- private static boolean isRouterAdvertisement(byte[] pkt) {
- if (pkt == null) return false;
-
- ByteBuffer buf = ByteBuffer.wrap(pkt);
-
- final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
- if (ethHdr.etherType != ETHER_TYPE_IPV6) return false;
-
- final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
- if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return false;
-
- final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
- return icmpv6Hdr.type == (short) ICMPV6_ROUTER_ADVERTISEMENT;
- }
-
- private static void expectRouterAdvertisement(TapPacketReader reader, String iface,
+ private static void waitForRouterAdvertisement(TapPacketReader reader, String iface,
long timeoutMs) {
final long deadline = SystemClock.uptimeMillis() + timeoutMs;
do {
byte[] pkt = reader.popPacket(timeoutMs);
- if (isRouterAdvertisement(pkt)) return;
+ if (isExpectedIcmpv6Packet(pkt, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) return;
+
timeoutMs = deadline - SystemClock.uptimeMillis();
} while (timeoutMs > 0);
fail("Did not receive router advertisement on " + iface + " after "
@@ -372,7 +382,7 @@
// before the reader is started.
mDownstreamReader = makePacketReader(mDownstreamIface);
- expectRouterAdvertisement(mDownstreamReader, iface, 2000 /* timeoutMs */);
+ waitForRouterAdvertisement(mDownstreamReader, iface, WAIT_RA_TIMEOUT_MS);
expectLocalOnlyAddresses(iface);
}
@@ -748,36 +758,42 @@
}
}
- private TestNetworkTracker createTestUpstream(final List<LinkAddress> addresses)
- throws Exception {
+ private TestNetworkTracker createTestUpstream(final List<LinkAddress> addresses,
+ final List<InetAddress> dnses) throws Exception {
mTm.setPreferTestNetworks(true);
- return initTestNetwork(mContext, addresses, TIMEOUT_MS);
+ final LinkProperties lp = new LinkProperties();
+ lp.setLinkAddresses(addresses);
+ lp.setDnsServers(dnses);
+ lp.setNat64Prefix(TEST_NAT64PREFIX);
+
+ return initTestNetwork(mContext, lp, TIMEOUT_MS);
}
@Test
- public void testTestNetworkUpstream() throws Exception {
- assumeFalse(mEm.isAvailable());
+ public void testIcmpv6Echo() throws Exception {
+ runPing6Test(initTetheringTester(toList(TEST_IP4_ADDR, TEST_IP6_ADDR),
+ toList(TEST_IP4_DNS, TEST_IP6_DNS)));
+ }
- // MyTetheringEventCallback currently only support await first available upstream. Tethering
- // may select internet network as upstream if test network is not available and not be
- // preferred yet. Create test upstream network before enable tethering.
- mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR, TEST_IP6_ADDR));
+ private void runPing6Test(TetheringTester tester) throws Exception {
+ TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString("1:2:3:4:5:6"),
+ true /* hasIpv6 */);
+ Inet6Address remoteIp6Addr = (Inet6Address) parseNumericAddress("2400:222:222::222");
+ ByteBuffer request = Ipv6Utils.buildEchoRequestPacket(tethered.macAddr,
+ tethered.routerMacAddr, tethered.ipv6Addr, remoteIp6Addr);
+ tester.verifyUpload(request, p -> {
+ Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
- mDownstreamIface = createTestInterface();
- mEm.setIncludeTestInterfaces(true);
+ return isExpectedIcmpv6Packet(p, false /* hasEth */, ICMPV6_ECHO_REQUEST_TYPE);
+ });
- final String iface = mTetheredInterfaceRequester.getInterface();
- assertEquals("TetheredInterfaceCallback for unexpected interface",
- mDownstreamIface.getInterfaceName(), iface);
+ ByteBuffer reply = Ipv6Utils.buildEchoReplyPacket(remoteIp6Addr, tethered.ipv6Addr);
+ tester.verifyDownload(reply, p -> {
+ Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
- mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName(),
- mUpstreamTracker.getNetwork());
- assertEquals("onUpstreamChanged for unexpected network", mUpstreamTracker.getNetwork(),
- mTetheringEventCallback.awaitUpstreamChanged());
-
- mDownstreamReader = makePacketReader(mDownstreamIface);
- // TODO: do basic forwarding test here.
+ return isExpectedIcmpv6Packet(p, true /* hasEth */, ICMPV6_ECHO_REPLY_TYPE);
+ });
}
// Test network topology:
@@ -795,12 +811,11 @@
// Used by public port and private port. Assume port 9876 has not been used yet before the
// testing that public port and private port are the same in the testing. Note that NAT port
// forwarding could be different between private port and public port.
+ // TODO: move to the start of test class.
private static final short LOCAL_PORT = 9876;
private static final short REMOTE_PORT = 433;
private static final byte TYPE_OF_SERVICE = 0;
private static final short ID = 27149;
- private static final short ID2 = 27150;
- private static final short ID3 = 27151;
private static final short FLAGS_AND_FRAGMENT_OFFSET = (short) 0x4000; // flags=DF, offset=0
private static final byte TIME_TO_LIVE = (byte) 0x40;
private static final ByteBuffer PAYLOAD =
@@ -810,43 +825,48 @@
private static final ByteBuffer PAYLOAD3 =
ByteBuffer.wrap(new byte[] { (byte) 0x9a, (byte) 0xbc });
- private boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEther,
- @NonNull final ByteBuffer payload) {
- final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
-
- if (hasEther) {
- final EthernetHeader etherHeader = Struct.parse(EthernetHeader.class, buf);
- if (etherHeader == null) return false;
- }
-
- final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, buf);
- if (ipv4Header == null) return false;
-
- final UdpHeader udpHeader = Struct.parse(UdpHeader.class, buf);
- if (udpHeader == null) return false;
-
- if (buf.remaining() != payload.limit()) return false;
-
- return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
- payload.array());
- }
-
@NonNull
- private ByteBuffer buildUdpv4Packet(@Nullable final MacAddress srcMac,
- @Nullable final MacAddress dstMac, short id,
- @NonNull final Inet4Address srcIp, @NonNull final Inet4Address dstIp,
+ private ByteBuffer buildUdpPacket(
+ @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
+ @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
short srcPort, short dstPort, @Nullable final ByteBuffer payload)
throws Exception {
+ int ipProto;
+ short ethType;
+ if (srcIp instanceof Inet4Address && dstIp instanceof Inet4Address) {
+ ipProto = IPPROTO_IP;
+ ethType = (short) ETHER_TYPE_IPV4;
+ } else if (srcIp instanceof Inet6Address && dstIp instanceof Inet6Address) {
+ ipProto = IPPROTO_IPV6;
+ ethType = (short) ETHER_TYPE_IPV6;
+ } else {
+ fail("Unsupported conditions: srcIp " + srcIp + ", dstIp " + dstIp);
+ // Make compiler happy to the uninitialized ipProto and ethType.
+ return null; // unreachable, the annotation @NonNull of function return value is true.
+ }
+
final boolean hasEther = (srcMac != null && dstMac != null);
final int payloadLen = (payload == null) ? 0 : payload.limit();
- final ByteBuffer buffer = PacketBuilder.allocate(hasEther, IPPROTO_IP, IPPROTO_UDP,
+ final ByteBuffer buffer = PacketBuilder.allocate(hasEther, ipProto, IPPROTO_UDP,
payloadLen);
final PacketBuilder packetBuilder = new PacketBuilder(buffer);
+ // [1] Ethernet header
if (hasEther) packetBuilder.writeL2Header(srcMac, dstMac, (short) ETHER_TYPE_IPV4);
- packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
- TIME_TO_LIVE, (byte) IPPROTO_UDP, srcIp, dstIp);
+
+ // [2] IP header
+ if (ipProto == IPPROTO_IP) {
+ packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
+ TIME_TO_LIVE, (byte) IPPROTO_UDP, (Inet4Address) srcIp, (Inet4Address) dstIp);
+ } else {
+ packetBuilder.writeIpv6Header(VERSION_TRAFFICCLASS_FLOWLABEL, (byte) IPPROTO_UDP,
+ HOP_LIMIT, (Inet6Address) srcIp, (Inet6Address) dstIp);
+ }
+
+ // [3] UDP header
packetBuilder.writeUdpHeader(srcPort, dstPort);
+
+ // [4] Payload
if (payload != null) {
buffer.put(payload);
// in case data might be reused by caller, restore the position and
@@ -858,38 +878,47 @@
}
@NonNull
- private ByteBuffer buildUdpv4Packet(short id, @NonNull final Inet4Address srcIp,
- @NonNull final Inet4Address dstIp, short srcPort, short dstPort,
+ private ByteBuffer buildUdpPacket(@NonNull final InetAddress srcIp,
+ @NonNull final InetAddress dstIp, short srcPort, short dstPort,
@Nullable final ByteBuffer payload) throws Exception {
- return buildUdpv4Packet(null /* srcMac */, null /* dstMac */, id, srcIp, dstIp, srcPort,
+ return buildUdpPacket(null /* srcMac */, null /* dstMac */, srcIp, dstIp, srcPort,
dstPort, payload);
}
- // TODO: remove this verification once upstream connected notification race is fixed.
- // See #runUdp4Test.
- private boolean isIpv4TetherConnectivityVerified(TetheringTester tester,
- RemoteResponder remote, TetheredDevice tethered) throws Exception {
- final ByteBuffer probePacket = buildUdpv4Packet(tethered.macAddr,
- tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
- REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /*dstPort */,
+ // TODO: remove ipv4 verification (is4To6 = false) once upstream connected notification race is
+ // fixed. See #runUdp4Test.
+ //
+ // This function sends a probe packet to downstream interface and exam the result from upstream
+ // interface to make sure ipv4 tethering is ready. Return the entire packet which received from
+ // upstream interface.
+ @NonNull
+ private byte[] probeV4TetheringConnectivity(TetheringTester tester, TetheredDevice tethered,
+ boolean is4To6) throws Exception {
+ final ByteBuffer probePacket = buildUdpPacket(tethered.macAddr,
+ tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
+ REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
TEST_REACHABILITY_PAYLOAD);
// Send a UDP packet from client and check the packet can be found on upstream interface.
for (int i = 0; i < TETHER_REACHABILITY_ATTEMPTS; i++) {
- tester.sendPacket(probePacket);
- byte[] expectedPacket = remote.getNextMatchedPacket(p -> {
+ byte[] expectedPacket = tester.testUpload(probePacket, p -> {
Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
- return isExpectedUdpPacket(p, false /* hasEther */, TEST_REACHABILITY_PAYLOAD);
+ // If is4To6 is true, the ipv4 probe packet would be translated to ipv6 by Clat and
+ // would see this translated ipv6 packet in upstream interface.
+ return isExpectedUdpPacket(p, false /* hasEther */, !is4To6 /* isIpv4 */,
+ TEST_REACHABILITY_PAYLOAD);
});
- if (expectedPacket != null) return true;
+ if (expectedPacket != null) return expectedPacket;
}
- return false;
+
+ fail("Can't verify " + (is4To6 ? "ipv4 to ipv6" : "ipv4") + " tethering connectivity after "
+ + TETHER_REACHABILITY_ATTEMPTS + " attempts");
+ return null;
}
- private void runUdp4Test(TetheringTester tester, RemoteResponder remote, boolean usingBpf)
- throws Exception {
+ private void runUdp4Test(TetheringTester tester, boolean usingBpf) throws Exception {
final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
- "1:2:3:4:5:6"));
+ "1:2:3:4:5:6"), false /* hasIpv6 */);
// TODO: remove the connectivity verification for upstream connected notification race.
// Because async upstream connected notification can't guarantee the tethering routing is
@@ -897,26 +926,26 @@
// For short term plan, consider using IPv6 RA to get MAC address because the prefix comes
// from upstream. That can guarantee that the routing is ready. Long term plan is that
// refactors upstream connected notification from async to sync.
- assertTrue(isIpv4TetherConnectivityVerified(tester, remote, tethered));
+ probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
// Send a UDP packet in original direction.
- final ByteBuffer originalPacket = buildUdpv4Packet(tethered.macAddr,
- tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
- REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /*dstPort */,
+ final ByteBuffer originalPacket = buildUdpPacket(tethered.macAddr,
+ tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
+ REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
PAYLOAD /* payload */);
- tester.verifyUpload(remote, originalPacket, p -> {
+ tester.verifyUpload(originalPacket, p -> {
Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
- return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD);
+ return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD);
});
// Send a UDP packet in reply direction.
final Inet4Address publicIp4Addr = (Inet4Address) TEST_IP4_ADDR.getAddress();
- final ByteBuffer replyPacket = buildUdpv4Packet(ID2, REMOTE_IP4_ADDR /* srcIp */,
- publicIp4Addr /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /*dstPort */,
+ final ByteBuffer replyPacket = buildUdpPacket(REMOTE_IP4_ADDR /* srcIp */,
+ publicIp4Addr /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /* dstPort */,
PAYLOAD2 /* payload */);
- remote.verifyDownload(tester, replyPacket, p -> {
+ tester.verifyDownload(replyPacket, p -> {
Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
- return isExpectedUdpPacket(p, true/* hasEther */, PAYLOAD2);
+ return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
});
if (usingBpf) {
@@ -929,53 +958,96 @@
// See kernel upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5 and
// nf_conntrack_udp_packet in net/netfilter/nf_conntrack_proto_udp.c
Thread.sleep(UDP_STREAM_TS_MS);
- final ByteBuffer originalPacket2 = buildUdpv4Packet(tethered.macAddr,
- tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
+ final ByteBuffer originalPacket2 = buildUdpPacket(tethered.macAddr,
+ tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */,
- REMOTE_PORT /*dstPort */, PAYLOAD3 /* payload */);
- tester.verifyUpload(remote, originalPacket2, p -> {
+ REMOTE_PORT /* dstPort */, PAYLOAD3 /* payload */);
+ tester.verifyUpload(originalPacket2, p -> {
Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
- return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD3);
+ return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD3);
});
- final HashMap<Tether4Key, Tether4Value> upstreamMap = pollIpv4UpstreamMapFromDump();
+ // [1] Verify IPv4 upstream rule map.
+ final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
+ Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
assertNotNull(upstreamMap);
assertEquals(1, upstreamMap.size());
final Map.Entry<Tether4Key, Tether4Value> rule =
upstreamMap.entrySet().iterator().next();
- final Tether4Key key = rule.getKey();
- assertEquals(IPPROTO_UDP, key.l4proto);
- assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), key.src4));
- assertEquals(LOCAL_PORT, key.srcPort);
- assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), key.dst4));
- assertEquals(REMOTE_PORT, key.dstPort);
+ final Tether4Key upstream4Key = rule.getKey();
+ assertEquals(IPPROTO_UDP, upstream4Key.l4proto);
+ assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), upstream4Key.src4));
+ assertEquals(LOCAL_PORT, upstream4Key.srcPort);
+ assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), upstream4Key.dst4));
+ assertEquals(REMOTE_PORT, upstream4Key.dstPort);
- final Tether4Value value = rule.getValue();
+ final Tether4Value upstream4Value = rule.getValue();
assertTrue(Arrays.equals(publicIp4Addr.getAddress(),
- InetAddress.getByAddress(value.src46).getAddress()));
- assertEquals(LOCAL_PORT, value.srcPort);
+ InetAddress.getByAddress(upstream4Value.src46).getAddress()));
+ assertEquals(LOCAL_PORT, upstream4Value.srcPort);
assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
- InetAddress.getByAddress(value.dst46).getAddress()));
- assertEquals(REMOTE_PORT, value.dstPort);
+ InetAddress.getByAddress(upstream4Value.dst46).getAddress()));
+ assertEquals(REMOTE_PORT, upstream4Value.dstPort);
+
+ // [2] Verify stats map.
+ // Transmit packets on both direction for verifying stats. Because we only care the
+ // packet count in stats test, we just reuse the existing packets to increaes
+ // the packet count on both direction.
+
+ // Send packets on original direction.
+ for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
+ tester.verifyUpload(originalPacket, p -> {
+ Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+ return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD);
+ });
+ }
+
+ // Send packets on reply direction.
+ for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
+ tester.verifyDownload(replyPacket, p -> {
+ Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+ return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
+ });
+ }
+
+ // Dump stats map to verify.
+ final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
+ TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
+ assertNotNull(statsMap);
+ assertEquals(1, statsMap.size());
+
+ final Map.Entry<TetherStatsKey, TetherStatsValue> stats =
+ statsMap.entrySet().iterator().next();
+
+ // TODO: verify the upstream index in TetherStatsKey.
+
+ final TetherStatsValue statsValue = stats.getValue();
+ assertEquals(RX_UDP_PACKET_COUNT, statsValue.rxPackets);
+ assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE, statsValue.rxBytes);
+ assertEquals(0, statsValue.rxErrors);
+ assertEquals(TX_UDP_PACKET_COUNT, statsValue.txPackets);
+ assertEquals(TX_UDP_PACKET_COUNT * TX_UDP_PACKET_SIZE, statsValue.txBytes);
+ assertEquals(0, statsValue.txErrors);
}
}
- void initializeTethering() throws Exception {
+ private TetheringTester initTetheringTester(List<LinkAddress> upstreamAddresses,
+ List<InetAddress> upstreamDnses) throws Exception {
assumeFalse(mEm.isAvailable());
// MyTetheringEventCallback currently only support await first available upstream. Tethering
// may select internet network as upstream if test network is not available and not be
// preferred yet. Create test upstream network before enable tethering.
- mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR));
+ mUpstreamTracker = createTestUpstream(upstreamAddresses, upstreamDnses);
mDownstreamIface = createTestInterface();
mEm.setIncludeTestInterfaces(true);
- final String iface = mTetheredInterfaceRequester.getInterface();
+ // Make sure EtherentTracker use "mDownstreamIface" as server mode interface.
assertEquals("TetheredInterfaceCallback for unexpected interface",
- mDownstreamIface.getInterfaceName(), iface);
+ mDownstreamIface.getInterfaceName(), mTetheredInterfaceRequester.getInterface());
mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName(),
mUpstreamTracker.getNetwork());
@@ -984,26 +1056,74 @@
mDownstreamReader = makePacketReader(mDownstreamIface);
mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
+
+ final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+ // Currently tethering don't have API to tell when ipv6 tethering is available. Thus, make
+ // sure tethering already have ipv6 connectivity before testing.
+ if (cm.getLinkProperties(mUpstreamTracker.getNetwork()).hasGlobalIpv6Address()) {
+ waitForRouterAdvertisement(mDownstreamReader, mDownstreamIface.getInterfaceName(),
+ WAIT_RA_TIMEOUT_MS);
+ }
+
+ return new TetheringTester(mDownstreamReader, mUpstreamReader);
}
@Test
- @IgnoreAfter(Build.VERSION_CODES.Q)
- public void testTetherUdpV4WithoutBpf() throws Exception {
- initializeTethering();
- runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
+ @IgnoreAfter(Build.VERSION_CODES.R)
+ public void testTetherUdpV4UpToR() throws Exception {
+ runUdp4Test(initTetheringTester(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS)),
false /* usingBpf */);
}
+ private static boolean isUdpOffloadSupportedByKernel(final String kernelVersion) {
+ final KVersion current = DeviceInfoUtils.getMajorMinorSubminorVersion(kernelVersion);
+ return current.isInRange(new KVersion(4, 14, 222), new KVersion(4, 19, 0))
+ || current.isInRange(new KVersion(4, 19, 176), new KVersion(5, 4, 0))
+ || current.isAtLeast(new KVersion(5, 4, 98));
+ }
+
+ @Test
+ public void testIsUdpOffloadSupportedByKernel() throws Exception {
+ assertFalse(isUdpOffloadSupportedByKernel("4.14.221"));
+ assertTrue(isUdpOffloadSupportedByKernel("4.14.222"));
+ assertTrue(isUdpOffloadSupportedByKernel("4.16.0"));
+ assertTrue(isUdpOffloadSupportedByKernel("4.18.0"));
+ assertFalse(isUdpOffloadSupportedByKernel("4.19.0"));
+
+ assertFalse(isUdpOffloadSupportedByKernel("4.19.175"));
+ assertTrue(isUdpOffloadSupportedByKernel("4.19.176"));
+ assertTrue(isUdpOffloadSupportedByKernel("5.2.0"));
+ assertTrue(isUdpOffloadSupportedByKernel("5.3.0"));
+ assertFalse(isUdpOffloadSupportedByKernel("5.4.0"));
+
+ assertFalse(isUdpOffloadSupportedByKernel("5.4.97"));
+ assertTrue(isUdpOffloadSupportedByKernel("5.4.98"));
+ assertTrue(isUdpOffloadSupportedByKernel("5.10.0"));
+ }
+
+ // TODO: refactor test testTetherUdpV4* into IPv4 UDP non-offload and offload tests.
+ // That can be easier to know which feature is verified from test results.
@Test
@IgnoreUpTo(Build.VERSION_CODES.R)
- public void testTetherUdpV4WithBpf() throws Exception {
- initializeTethering();
- runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
- true /* usingBpf */);
+ public void testTetherUdpV4AfterR() throws Exception {
+ final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
+ final boolean isUdpOffloadSupported = isUdpOffloadSupportedByKernel(kernelVersion);
+ if (!isUdpOffloadSupported) {
+ Log.i(TAG, "testTetherUdpV4AfterR will skip BPF offload test for kernel "
+ + kernelVersion);
+ }
+ final boolean isTetherConfigBpfOffloadEnabled = isTetherConfigBpfOffloadEnabled();
+ if (!isTetherConfigBpfOffloadEnabled) {
+ Log.i(TAG, "testTetherUdpV4AfterR will skip BPF offload test "
+ + "because tethering config doesn't enable BPF offload.");
+ }
+ runUdp4Test(initTetheringTester(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS)),
+ isUdpOffloadSupported && isTetherConfigBpfOffloadEnabled);
}
@Nullable
- private Pair<Tether4Key, Tether4Value> parseTether4KeyValue(@NonNull String dumpStr) {
+ private <K extends Struct, V extends Struct> Pair<K, V> parseMapKeyValue(
+ Class<K> keyClass, Class<V> valueClass, @NonNull String dumpStr) {
Log.w(TAG, "Parsing string: " + dumpStr);
String[] keyValueStrs = dumpStr.split(BASE64_DELIMITER);
@@ -1016,36 +1136,38 @@
Log.d(TAG, "keyBytes: " + dumpHexString(keyBytes));
final ByteBuffer keyByteBuffer = ByteBuffer.wrap(keyBytes);
keyByteBuffer.order(ByteOrder.nativeOrder());
- final Tether4Key tether4Key = Struct.parse(Tether4Key.class, keyByteBuffer);
- Log.w(TAG, "tether4Key: " + tether4Key);
+ final K k = Struct.parse(keyClass, keyByteBuffer);
final byte[] valueBytes = Base64.decode(keyValueStrs[1], Base64.DEFAULT);
Log.d(TAG, "valueBytes: " + dumpHexString(valueBytes));
final ByteBuffer valueByteBuffer = ByteBuffer.wrap(valueBytes);
valueByteBuffer.order(ByteOrder.nativeOrder());
- final Tether4Value tether4Value = Struct.parse(Tether4Value.class, valueByteBuffer);
- Log.w(TAG, "tether4Value: " + tether4Value);
+ final V v = Struct.parse(valueClass, valueByteBuffer);
- return new Pair<>(tether4Key, tether4Value);
+ return new Pair<>(k, v);
}
@NonNull
- private HashMap<Tether4Key, Tether4Value> dumpIpv4UpstreamMap() throws Exception {
- final String rawMapStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE,
- DUMPSYS_TETHERING_RAWMAP_ARG);
- final HashMap<Tether4Key, Tether4Value> map = new HashMap<>();
+ private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
+ Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+ throws Exception {
+ final String[] args = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, mapArg};
+ final String rawMapStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE, args);
+ final HashMap<K, V> map = new HashMap<>();
for (final String line : rawMapStr.split(LINE_DELIMITER)) {
- final Pair<Tether4Key, Tether4Value> rule = parseTether4KeyValue(line.trim());
+ final Pair<K, V> rule = parseMapKeyValue(keyClass, valueClass, line.trim());
map.put(rule.first, rule.second);
}
return map;
}
@Nullable
- private HashMap<Tether4Key, Tether4Value> pollIpv4UpstreamMapFromDump() throws Exception {
+ private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
+ Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+ throws Exception {
for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
- final HashMap<Tether4Key, Tether4Value> map = dumpIpv4UpstreamMap();
+ final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, mapArg);
if (!map.isEmpty()) return map;
Thread.sleep(DUMP_POLLING_INTERVAL_MS);
@@ -1055,6 +1177,85 @@
return null;
}
+ private boolean isTetherConfigBpfOffloadEnabled() throws Exception {
+ final String dumpStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE, "--short");
+
+ // BPF offload tether config can be overridden by "config_tether_enable_bpf_offload" in
+ // packages/modules/Connectivity/Tethering/res/values/config.xml. OEM may disable config by
+ // RRO to override the enabled default value. Get the tethering config via dumpsys.
+ // $ dumpsys tethering
+ // mIsBpfEnabled: true
+ boolean enabled = dumpStr.contains("mIsBpfEnabled: true");
+ if (!enabled) {
+ Log.d(TAG, "BPF offload tether config not enabled: " + dumpStr);
+ }
+ return enabled;
+ }
+
+ @NonNull
+ private Inet6Address getClatIpv6Address(TetheringTester tester, TetheredDevice tethered)
+ throws Exception {
+ // Send an IPv4 UDP packet from client and check that a CLAT translated IPv6 UDP packet can
+ // be found on upstream interface. Get CLAT IPv6 address from the CLAT translated IPv6 UDP
+ // packet.
+ byte[] expectedPacket = probeV4TetheringConnectivity(tester, tethered, true /* is4To6 */);
+
+ // Above has guaranteed that the found packet is an IPv6 packet without ether header.
+ return Struct.parse(Ipv6Header.class, ByteBuffer.wrap(expectedPacket)).srcIp;
+ }
+
+ // Test network topology:
+ //
+ // public network (rawip) private network
+ // | UE (CLAT support) |
+ // +---------------+ V +------------+------------+ V +------------+
+ // | NAT64 Gateway +---------+ Upstream | Downstream +---------+ Client |
+ // +---------------+ +------------+------------+ +------------+
+ // remote ip public ip private ip
+ // [64:ff9b::808:808]:443 [clat ipv6]:9876 [TetheredDevice ipv4]:9876
+ //
+ // Note that CLAT IPv6 address is generated by ClatCoordinator. Get the CLAT IPv6 address by
+ // sending out an IPv4 packet and extracting the source address from CLAT translated IPv6
+ // packet.
+ //
+ private void runClatUdpTest(TetheringTester tester) throws Exception {
+ final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
+ "1:2:3:4:5:6"), true /* hasIpv6 */);
+
+ // Get CLAT IPv6 address.
+ final Inet6Address clatAddr6 = getClatIpv6Address(tester, tethered);
+
+ // Send an IPv4 UDP packet in original direction.
+ // IPv4 packet -- CLAT translation --> IPv6 packet
+ final ByteBuffer originalPacket = buildUdpPacket(tethered.macAddr,
+ tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
+ REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
+ PAYLOAD /* payload */);
+ tester.verifyUpload(originalPacket, p -> {
+ Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
+ return isExpectedUdpPacket(p, false /* hasEther */, false /* isIpv4 */, PAYLOAD);
+ });
+
+ // Send an IPv6 UDP packet in reply direction.
+ // IPv6 packet -- CLAT translation --> IPv4 packet
+ final ByteBuffer replyPacket = buildUdpPacket(REMOTE_NAT64_ADDR /* srcIp */,
+ clatAddr6 /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /* dstPort */,
+ PAYLOAD2 /* payload */);
+ tester.verifyDownload(replyPacket, p -> {
+ Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+ return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
+ });
+
+ // TODO: test CLAT bpf maps.
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testTetherClatUdp() throws Exception {
+ // CLAT only starts on IPv6 only network.
+ runClatUdpTest(initTetheringTester(toList(TEST_IP6_ADDR), toList(TEST_IP6_DNS)));
+ }
+
private <T> List<T> toList(T... array) {
return Arrays.asList(array);
}
diff --git a/Tethering/tests/integration/src/android/net/TetheringTester.java b/Tethering/tests/integration/src/android/net/TetheringTester.java
index d24661a..4d90d39 100644
--- a/Tethering/tests/integration/src/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/src/android/net/TetheringTester.java
@@ -16,10 +16,24 @@
package android.net;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_UDP;
+
import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
@@ -30,13 +44,30 @@
import android.util.ArrayMap;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv4Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.NsHeader;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.net.module.util.structs.UdpHeader;
import com.android.networkstack.arp.ArpPacket;
import com.android.testutils.TapPacketReader;
import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
@@ -50,31 +81,40 @@
private static final String TAG = TetheringTester.class.getSimpleName();
private static final int PACKET_READ_TIMEOUT_MS = 100;
private static final int DHCP_DISCOVER_ATTEMPTS = 10;
+ private static final int READ_RA_ATTEMPTS = 10;
private static final byte[] DHCP_REQUESTED_PARAMS = new byte[] {
DhcpPacket.DHCP_SUBNET_MASK,
DhcpPacket.DHCP_ROUTER,
DhcpPacket.DHCP_DNS_SERVER,
DhcpPacket.DHCP_LEASE_TIME,
};
+ private static final InetAddress LINK_LOCAL = parseNumericAddress("fe80::1");
public static final String DHCP_HOSTNAME = "testhostname";
private final ArrayMap<MacAddress, TetheredDevice> mTetheredDevices;
private final TapPacketReader mDownstreamReader;
+ private final TapPacketReader mUpstreamReader;
public TetheringTester(TapPacketReader downstream) {
+ this(downstream, null);
+ }
+
+ public TetheringTester(TapPacketReader downstream, TapPacketReader upstream) {
if (downstream == null) fail("Downstream reader could not be NULL");
mDownstreamReader = downstream;
+ mUpstreamReader = upstream;
mTetheredDevices = new ArrayMap<>();
}
- public TetheredDevice createTetheredDevice(MacAddress macAddr) throws Exception {
+ public TetheredDevice createTetheredDevice(MacAddress macAddr, boolean hasIpv6)
+ throws Exception {
if (mTetheredDevices.get(macAddr) != null) {
fail("Tethered device already created");
}
- TetheredDevice tethered = new TetheredDevice(macAddr);
+ TetheredDevice tethered = new TetheredDevice(macAddr, hasIpv6);
mTetheredDevices.put(macAddr, tethered);
return tethered;
@@ -84,14 +124,15 @@
public final MacAddress macAddr;
public final MacAddress routerMacAddr;
public final Inet4Address ipv4Addr;
+ public final Inet6Address ipv6Addr;
- private TetheredDevice(MacAddress mac) throws Exception {
+ private TetheredDevice(MacAddress mac, boolean hasIpv6) throws Exception {
macAddr = mac;
-
DhcpResults dhcpResults = runDhcp(macAddr.toByteArray());
ipv4Addr = (Inet4Address) dhcpResults.ipAddress.getAddress();
routerMacAddr = getRouterMacAddressFromArp(ipv4Addr, macAddr,
dhcpResults.serverAddress);
+ ipv6Addr = hasIpv6 ? runSlaac(macAddr, routerMacAddr) : null;
}
}
@@ -141,7 +182,7 @@
}
private DhcpPacket getNextDhcpPacket() throws Exception {
- final byte[] packet = getNextMatchedPacket((p) -> {
+ final byte[] packet = getDownloadPacket((p) -> {
// Test whether this is DHCP packet.
try {
DhcpPacket.decodeFullPacket(p, p.length, DhcpPacket.ENCAP_L2);
@@ -184,7 +225,7 @@
tethered.ipv4Addr.getAddress() /* sender IP */,
(short) ARP_REPLY);
try {
- sendPacket(arpReply);
+ sendUploadPacket(arpReply);
} catch (Exception e) {
fail("Failed to reply ARP for " + tethered.ipv4Addr);
}
@@ -198,9 +239,9 @@
tetherMac.toByteArray() /* srcMac */, routerIp.getAddress() /* target IP */,
new byte[ETHER_ADDR_LEN] /* target HW address */,
tetherIp.getAddress() /* sender IP */, (short) ARP_REQUEST);
- sendPacket(arpProbe);
+ sendUploadPacket(arpProbe);
- final byte[] packet = getNextMatchedPacket((p) -> {
+ final byte[] packet = getDownloadPacket((p) -> {
final ArpPacket arpPacket = parseArpPacket(p);
if (arpPacket == null || arpPacket.opCode != ARP_REPLY) return false;
return arpPacket.targetIp.equals(tetherIp);
@@ -216,45 +257,205 @@
return null;
}
- public void sendPacket(ByteBuffer packet) throws Exception {
+ private List<PrefixInformationOption> getRaPrefixOptions(byte[] packet) {
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ if (!isExpectedIcmpv6Packet(buf, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) {
+ fail("Parsing RA packet fail");
+ }
+
+ Struct.parse(RaHeader.class, buf);
+ final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
+ while (buf.position() < packet.length) {
+ final int currentPos = buf.position();
+ final int type = Byte.toUnsignedInt(buf.get());
+ final int length = Byte.toUnsignedInt(buf.get());
+ if (type == ICMPV6_ND_OPTION_PIO) {
+ final ByteBuffer pioBuf = ByteBuffer.wrap(buf.array(), currentPos,
+ Struct.getSize(PrefixInformationOption.class));
+ final PrefixInformationOption pio =
+ Struct.parse(PrefixInformationOption.class, pioBuf);
+ pioList.add(pio);
+
+ // Move ByteBuffer position to the next option.
+ buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
+ } else {
+ buf.position(currentPos + (length * 8));
+ }
+ }
+ return pioList;
+ }
+
+ private Inet6Address runSlaac(MacAddress srcMac, MacAddress dstMac) throws Exception {
+ sendRsPacket(srcMac, dstMac);
+
+ final byte[] raPacket = verifyPacketNotNull("Receive RA fail", getDownloadPacket(p -> {
+ return isExpectedIcmpv6Packet(p, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT);
+ }));
+
+ final List<PrefixInformationOption> options = getRaPrefixOptions(raPacket);
+
+ for (PrefixInformationOption pio : options) {
+ if (pio.validLifetime > 0) {
+ final byte[] addressBytes = pio.prefix;
+ // Random the last two bytes as suffix.
+ // TODO: Currently do not implmement DAD in the test. Rely the gateway ipv6 address
+ // genetrated by tethering module always has random the last byte.
+ addressBytes[addressBytes.length - 1] = (byte) (new Random()).nextInt();
+ addressBytes[addressBytes.length - 2] = (byte) (new Random()).nextInt();
+
+ return (Inet6Address) InetAddress.getByAddress(addressBytes);
+ }
+ }
+
+ fail("No available ipv6 prefix");
+ return null;
+ }
+
+ private void sendRsPacket(MacAddress srcMac, MacAddress dstMac) throws Exception {
+ Log.d(TAG, "Sending RS");
+ ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac);
+ ByteBuffer rs = Ipv6Utils.buildRsPacket(srcMac, dstMac, (Inet6Address) LINK_LOCAL,
+ IPV6_ADDR_ALL_NODES_MULTICAST, slla);
+
+ sendUploadPacket(rs);
+ }
+
+ private void maybeReplyNa(byte[] packet) {
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
+ if (ethHdr.etherType != ETHER_TYPE_IPV6) return;
+
+ final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
+ if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return;
+
+ final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
+ if (icmpv6Hdr.type != (short) ICMPV6_NEIGHBOR_SOLICITATION) return;
+
+ final NsHeader nsHdr = Struct.parse(NsHeader.class, buf);
+ for (int i = 0; i < mTetheredDevices.size(); i++) {
+ TetheredDevice tethered = mTetheredDevices.valueAt(i);
+ if (!nsHdr.target.equals(tethered.ipv6Addr)) continue;
+
+ final ByteBuffer tlla = LlaOption.build((byte) ICMPV6_ND_OPTION_TLLA, tethered.macAddr);
+ int flags = NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED
+ | NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+ ByteBuffer ns = Ipv6Utils.buildNaPacket(tethered.macAddr, tethered.routerMacAddr,
+ nsHdr.target, ipv6Hdr.srcIp, flags, nsHdr.target, tlla);
+ try {
+ sendUploadPacket(ns);
+ } catch (Exception e) {
+ fail("Failed to reply NA for " + tethered.ipv6Addr);
+ }
+
+ return;
+ }
+ }
+
+ public static boolean isExpectedIcmpv6Packet(byte[] packet, boolean hasEth, int type) {
+ final ByteBuffer buf = ByteBuffer.wrap(packet);
+ return isExpectedIcmpv6Packet(buf, hasEth, type);
+ }
+
+ private static boolean isExpectedIcmpv6Packet(ByteBuffer buf, boolean hasEth, int type) {
+ try {
+ if (hasEth && !hasExpectedEtherHeader(buf, false /* isIpv4 */)) return false;
+
+ if (!hasExpectedIpHeader(buf, false /* isIpv4 */, IPPROTO_ICMPV6)) return false;
+
+ return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
+ } catch (Exception e) {
+ // Parsing packet fail means it is not icmpv6 packet.
+ }
+
+ return false;
+ }
+
+ private static boolean hasExpectedEtherHeader(@NonNull final ByteBuffer buf, boolean isIpv4)
+ throws Exception {
+ final int expected = isIpv4 ? ETHER_TYPE_IPV4 : ETHER_TYPE_IPV6;
+
+ return Struct.parse(EthernetHeader.class, buf).etherType == expected;
+ }
+
+ private static boolean hasExpectedIpHeader(@NonNull final ByteBuffer buf, boolean isIpv4,
+ int ipProto) throws Exception {
+ if (isIpv4) {
+ return Struct.parse(Ipv4Header.class, buf).protocol == (byte) ipProto;
+ } else {
+ return Struct.parse(Ipv6Header.class, buf).nextHeader == (byte) ipProto;
+ }
+ }
+
+ public static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
+ boolean isIpv4, @NonNull final ByteBuffer payload) {
+ final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
+ try {
+ if (hasEth && !hasExpectedEtherHeader(buf, isIpv4)) return false;
+
+ if (!hasExpectedIpHeader(buf, isIpv4, IPPROTO_UDP)) return false;
+
+ if (Struct.parse(UdpHeader.class, buf) == null) return false;
+ } catch (Exception e) {
+ // Parsing packet fail means it is not udp packet.
+ return false;
+ }
+
+ if (buf.remaining() != payload.limit()) return false;
+
+ return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
+ payload.array());
+ }
+
+ private void sendUploadPacket(ByteBuffer packet) throws Exception {
mDownstreamReader.sendResponse(packet);
}
- public byte[] getNextMatchedPacket(Predicate<byte[]> filter) {
+ private void sendDownloadPacket(ByteBuffer packet) throws Exception {
+ assertNotNull("Can't deal with upstream interface in local only mode", mUpstreamReader);
+
+ mUpstreamReader.sendResponse(packet);
+ }
+
+ private byte[] getDownloadPacket(Predicate<byte[]> filter) {
byte[] packet;
while ((packet = mDownstreamReader.poll(PACKET_READ_TIMEOUT_MS)) != null) {
if (filter.test(packet)) return packet;
maybeReplyArp(packet);
+ maybeReplyNa(packet);
}
return null;
}
- public void verifyUpload(final RemoteResponder dst, final ByteBuffer packet,
- final Predicate<byte[]> filter) throws Exception {
- sendPacket(packet);
- assertNotNull("Upload fail", dst.getNextMatchedPacket(filter));
+ private byte[] getUploadPacket(Predicate<byte[]> filter) {
+ assertNotNull("Can't deal with upstream interface in local only mode", mUpstreamReader);
+
+ return mUpstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
}
- public static class RemoteResponder {
- final TapPacketReader mUpstreamReader;
- public RemoteResponder(TapPacketReader reader) {
- mUpstreamReader = reader;
- }
+ private @NonNull byte[] verifyPacketNotNull(String message, @Nullable byte[] packet) {
+ assertNotNull(message, packet);
- public void sendPacket(ByteBuffer packet) throws Exception {
- mUpstreamReader.sendResponse(packet);
- }
+ return packet;
+ }
- public byte[] getNextMatchedPacket(Predicate<byte[]> filter) throws Exception {
- return mUpstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
- }
+ public byte[] testUpload(final ByteBuffer packet, final Predicate<byte[]> filter)
+ throws Exception {
+ sendUploadPacket(packet);
- public void verifyDownload(final TetheringTester dst, final ByteBuffer packet,
- final Predicate<byte[]> filter) throws Exception {
- sendPacket(packet);
- assertNotNull("Download fail", dst.getNextMatchedPacket(filter));
- }
+ return getUploadPacket(filter);
+ }
+
+ public byte[] verifyUpload(final ByteBuffer packet, final Predicate<byte[]> filter)
+ throws Exception {
+ return verifyPacketNotNull("Upload fail", testUpload(packet, filter));
+ }
+
+ public byte[] verifyDownload(final ByteBuffer packet, final Predicate<byte[]> filter)
+ throws Exception {
+ sendDownloadPacket(packet);
+
+ return verifyPacketNotNull("Download fail", getDownloadPacket(filter));
}
}
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index 18fd63b..a84fdd2 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -22,7 +22,7 @@
name: "MtsTetheringTestLatestSdk",
min_sdk_version: "30",
- target_sdk_version: "31",
+ target_sdk_version: "33",
libs: [
"android.test.base",
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
index ad2faa0..68c1c57 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -352,15 +352,6 @@
assertFalse(mTestMap.isEmpty());
mTestMap.clear();
assertTrue(mTestMap.isEmpty());
-
- // Clearing an already-closed map throws.
- mTestMap.close();
- try {
- mTestMap.clear();
- fail("clearing already-closed map should throw");
- } catch (ErrnoException expected) {
- assertEquals(OsConstants.EBADF, expected.errno);
- }
}
@Test
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
index 7ee69b2..d38a7c3 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
@@ -28,7 +28,6 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
-import android.net.util.SharedLog;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
@@ -38,6 +37,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.SharedLog;
import com.android.net.module.util.netlink.StructNlMsgHdr;
import org.junit.Before;
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index d1b8380..0ee12ad 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -89,7 +89,7 @@
static_libs: [
"TetheringApiStableLib",
],
- target_sdk_version: "31",
+ target_sdk_version: "33",
visibility: [
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 43f1eaa..ef143de 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -86,7 +86,6 @@
import android.net.ip.IpNeighborMonitor.NeighborEvent;
import android.net.ip.IpNeighborMonitor.NeighborEventConsumer;
import android.net.ip.RouterAdvertisementDaemon.RaParams;
-import android.net.util.SharedLog;
import android.os.Build;
import android.os.Handler;
import android.os.RemoteException;
@@ -101,8 +100,11 @@
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SharedLog;
import com.android.net.module.util.bpf.Tether4Key;
import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
import com.android.networkstack.tethering.BpfCoordinator;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
import com.android.networkstack.tethering.PrivateAddressCoordinator;
@@ -112,10 +114,9 @@
import com.android.networkstack.tethering.TetherDownstream6Key;
import com.android.networkstack.tethering.TetherLimitKey;
import com.android.networkstack.tethering.TetherLimitValue;
-import com.android.networkstack.tethering.TetherStatsKey;
-import com.android.networkstack.tethering.TetherStatsValue;
import com.android.networkstack.tethering.TetherUpstream6Key;
import com.android.networkstack.tethering.TetheringConfiguration;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.networkstack.tethering.util.PrefixUtils;
import com.android.testutils.DevSdkIgnoreRule;
@@ -186,6 +187,7 @@
@Mock private NetworkStatsManager mStatsManager;
@Mock private TetheringConfiguration mTetherConfig;
@Mock private ConntrackMonitor mConntrackMonitor;
+ @Mock private TetheringMetrics mTetheringMetrics;
@Mock private BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
@Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
@Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
@@ -235,7 +237,7 @@
when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
mIpServer = new IpServer(
IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator,
- mCallback, mTetherConfig, mAddressCoordinator, mDependencies);
+ mCallback, mTetherConfig, mAddressCoordinator, mTetheringMetrics, mDependencies);
mIpServer.start();
mNeighborEventConsumer = neighborCaptor.getValue();
@@ -367,7 +369,7 @@
.thenReturn(mIpNeighborMonitor);
mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog,
mNetd, mBpfCoordinator, mCallback, mTetherConfig, mAddressCoordinator,
- mDependencies);
+ mTetheringMetrics, mDependencies);
mIpServer.start();
mLooper.dispatchAll();
verify(mCallback).updateInterfaceState(
@@ -451,6 +453,9 @@
mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
inOrder.verify(mCallback).updateLinkProperties(
eq(mIpServer), any(LinkProperties.class));
+ verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_BLUETOOTH),
+ eq(TETHER_ERROR_NO_ERROR));
+ verify(mTetheringMetrics).sendReport(eq(TETHERING_BLUETOOTH));
verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
}
@@ -658,6 +663,9 @@
usbTeardownOrder.verify(mCallback).updateLinkProperties(
eq(mIpServer), mLinkPropertiesCaptor.capture());
assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+ verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_USB),
+ eq(TETHER_ERROR_TETHER_IFACE_ERROR));
+ verify(mTetheringMetrics).sendReport(eq(TETHERING_USB));
}
@Test
@@ -676,6 +684,9 @@
usbTeardownOrder.verify(mCallback).updateLinkProperties(
eq(mIpServer), mLinkPropertiesCaptor.capture());
assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+ verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_USB),
+ eq(TETHER_ERROR_ENABLE_FORWARDING_ERROR));
+ verify(mTetheringMetrics).sendReport(eq(TETHERING_USB));
}
@Test
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 4967d27..20a222d 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -85,7 +85,6 @@
import android.net.ip.ConntrackMonitor;
import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
import android.net.ip.IpServer;
-import android.net.util.SharedLog;
import android.os.Build;
import android.os.Handler;
import android.os.test.TestLooper;
@@ -100,8 +99,11 @@
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SharedLog;
import com.android.net.module.util.bpf.Tether4Key;
import com.android.net.module.util.bpf.Tether4Value;
+import com.android.net.module.util.bpf.TetherStatsKey;
+import com.android.net.module.util.bpf.TetherStatsValue;
import com.android.net.module.util.netlink.ConntrackMessage;
import com.android.net.module.util.netlink.NetlinkConstants;
import com.android.net.module.util.netlink.NetlinkSocket;
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 690ff71..e4263db 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -65,7 +65,6 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
-import android.net.util.SharedLog;
import android.os.Bundle;
import android.os.Handler;
import android.os.PersistableBundle;
@@ -82,6 +81,7 @@
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.SharedLog;
import com.android.testutils.DevSdkIgnoreRule;
import org.junit.After;
@@ -304,33 +304,6 @@
}
@Test
- public void toleratesCarrierConfigManagerMissing() {
- setupForRequiredProvisioning();
- mockService(Context.CARRIER_CONFIG_SERVICE, CarrierConfigManager.class, null);
- mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
- // Couldn't get the CarrierConfigManager, but still had a declared provisioning app.
- // Therefore provisioning still be required.
- assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
- }
-
- @Test
- public void toleratesCarrierConfigMissing() {
- setupForRequiredProvisioning();
- when(mCarrierConfigManager.getConfig()).thenReturn(null);
- mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
- // We still have a provisioning app configured, so still require provisioning.
- assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
- }
-
- @Test
- public void toleratesCarrierConfigNotLoaded() {
- setupForRequiredProvisioning();
- mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, false);
- // We still have a provisioning app configured, so still require provisioning.
- assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
- }
-
- @Test
public void provisioningNotRequiredWhenAppNotFound() {
setupForRequiredProvisioning();
when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
@@ -706,8 +679,8 @@
@IgnoreUpTo(SC_V2)
public void requestLatestTetheringEntitlementResult_carrierDoesNotSupport_noProvisionCount()
throws Exception {
- setupForRequiredProvisioning();
setupCarrierConfig(false);
+ setupForRequiredProvisioning();
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
ResultReceiver receiver = new ResultReceiver(null) {
@Override
@@ -735,6 +708,7 @@
mEnMgr.notifyUpstream(false);
mLooper.dispatchAll();
setupCarrierConfig(false);
+ mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
mEnMgr.reevaluateSimCardProvisioning(mConfig);
// Turn on upstream.
@@ -749,8 +723,8 @@
@IgnoreUpTo(SC_V2)
public void startProvisioningIfNeeded_carrierUnsupport()
throws Exception {
- setupForRequiredProvisioning();
setupCarrierConfig(false);
+ setupForRequiredProvisioning();
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
verify(mTetherProvisioningFailedListener, never())
.onTetherProvisioningFailed(TETHERING_WIFI, "Carrier does not support.");
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
index ac5c59d..95ec38f 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
@@ -18,7 +18,8 @@
import android.content.Context;
import android.content.res.Resources;
-import android.net.util.SharedLog;
+
+import com.android.net.module.util.SharedLog;
/** FakeTetheringConfiguration is used to override static method for testing. */
public class FakeTetheringConfiguration extends TetheringConfiguration {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java
index f2b5314..865228a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java
@@ -41,11 +41,12 @@
import android.net.NetworkCapabilities;
import android.net.RouteInfo;
import android.net.ip.IpServer;
-import android.net.util.SharedLog;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.SharedLog;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
index e9716b3..faca1c8 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
@@ -26,6 +26,7 @@
import static android.net.RouteInfo.RTN_UNICAST;
import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_IFACE;
import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_UID;
@@ -66,7 +67,6 @@
import android.net.NetworkStats.Entry;
import android.net.RouteInfo;
import android.net.netstats.provider.NetworkStatsProvider;
-import android.net.util.SharedLog;
import android.os.Build;
import android.os.Handler;
import android.os.test.TestLooper;
@@ -78,6 +78,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.net.module.util.SharedLog;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.TestableNetworkStatsProviderCbBinder;
@@ -668,7 +669,7 @@
if (isAtLeastT()) {
mTetherStatsProviderCb.expectNotifyLimitReached();
- } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) {
+ } else if (isAtLeastS()) {
mTetherStatsProviderCb.expectNotifyWarningOrLimitReached();
} else {
mTetherStatsProviderCb.expectNotifyLimitReached();
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
index d1891ed..36b439b 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java
@@ -43,7 +43,6 @@
import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback;
import android.hardware.tetheroffload.control.V1_1.OffloadCallbackEvent;
-import android.net.util.SharedLog;
import android.os.Handler;
import android.os.NativeHandle;
import android.os.test.TestLooper;
@@ -55,6 +54,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.SharedLog;
import com.android.net.module.util.netlink.StructNfGenMsg;
import com.android.net.module.util.netlink.StructNlMsgHdr;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index 7fcf2b2..1a12125 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -22,10 +22,13 @@
import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.telephony.CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL;
+import static android.telephony.CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_FORCE_USB_FUNCTIONS;
import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_NCM_FUNCTION;
import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_RNDIS_FUNCTION;
@@ -44,10 +47,11 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
-import android.net.util.SharedLog;
import android.os.Build;
+import android.os.PersistableBundle;
import android.provider.DeviceConfig;
import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
import android.telephony.TelephonyManager;
import android.test.mock.MockContentResolver;
@@ -56,7 +60,9 @@
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.SharedLog;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -88,6 +94,7 @@
private static final long TEST_PACKAGE_VERSION = 1234L;
@Mock private ApplicationInfo mApplicationInfo;
@Mock private Context mContext;
+ @Mock private CarrierConfigManager mCarrierConfigManager;
@Mock private TelephonyManager mTelephonyManager;
@Mock private Resources mResources;
@Mock private Resources mResourcesForSubId;
@@ -97,6 +104,7 @@
private boolean mHasTelephonyManager;
private MockitoSession mMockingSession;
private MockContentResolver mContentResolver;
+ private final PersistableBundle mCarrierConfig = new PersistableBundle();
private class MockTetheringConfiguration extends TetheringConfiguration {
MockTetheringConfiguration(Context ctx, SharedLog log, int id) {
@@ -474,6 +482,56 @@
PROVISIONING_APP_RESPONSE);
}
+ private <T> void mockService(String serviceName, Class<T> serviceClass, T service) {
+ when(mMockContext.getSystemServiceName(serviceClass)).thenReturn(serviceName);
+ when(mMockContext.getSystemService(serviceName)).thenReturn(service);
+ }
+
+ @Test
+ public void testGetCarrierConfigBySubId_noCarrierConfigManager_configsAreDefault() {
+ // Act like the CarrierConfigManager is present and ready unless told otherwise.
+ mockService(Context.CARRIER_CONFIG_SERVICE,
+ CarrierConfigManager.class, null);
+ final TetheringConfiguration cfg = new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+
+ assertTrue(cfg.isCarrierSupportTethering);
+ assertTrue(cfg.isCarrierConfigAffirmsEntitlementCheckRequired);
+ }
+
+ @Test
+ public void testGetCarrierConfigBySubId_carrierConfigMissing_configsAreDefault() {
+ // Act like the CarrierConfigManager is present and ready unless told otherwise.
+ mockService(Context.CARRIER_CONFIG_SERVICE,
+ CarrierConfigManager.class, mCarrierConfigManager);
+ when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(null);
+ final TetheringConfiguration cfg = new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+
+ assertTrue(cfg.isCarrierSupportTethering);
+ assertTrue(cfg.isCarrierConfigAffirmsEntitlementCheckRequired);
+ }
+
+ @Test
+ public void testGetCarrierConfigBySubId_hasConfigs_carrierUnsupportAndCheckNotRequired() {
+ mockService(Context.CARRIER_CONFIG_SERVICE,
+ CarrierConfigManager.class, mCarrierConfigManager);
+ mCarrierConfig.putBoolean(KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
+ mCarrierConfig.putBoolean(KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, false);
+ mCarrierConfig.putBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, false);
+ when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig);
+ final TetheringConfiguration cfg = new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+
+ if (SdkLevel.isAtLeastT()) {
+ assertFalse(cfg.isCarrierSupportTethering);
+ } else {
+ assertTrue(cfg.isCarrierSupportTethering);
+ }
+ assertFalse(cfg.isCarrierConfigAffirmsEntitlementCheckRequired);
+
+ }
+
@Test
public void testEnableLegacyWifiP2PAddress() throws Exception {
final TetheringConfiguration defaultCfg = new TetheringConfiguration(
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
index f664d5d..9db8f16 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java
@@ -275,7 +275,7 @@
mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG,
result);
verify(mTethering).isTetheringSupported();
- verify(mTethering).startTethering(eq(request), eq(result));
+ verify(mTethering).startTethering(eq(request), eq(TEST_CALLER_PKG), eq(result));
}
@Test
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 0388758..e90d27e 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -45,6 +45,7 @@
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
@@ -57,6 +58,7 @@
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@@ -153,7 +155,6 @@
import android.net.ip.IpServer;
import android.net.ip.RouterAdvertisementDaemon;
import android.net.util.NetworkConstants;
-import android.net.util.SharedLog;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiClient;
import android.net.wifi.WifiManager;
@@ -185,16 +186,21 @@
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.common.BluetoothPanShim;
import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.MiscAsserts;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -216,6 +222,8 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class TetheringTest {
+ @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
private static final int IFINDEX_OFFSET = 100;
private static final String TEST_MOBILE_IFNAME = "test_rmnet_data0";
@@ -236,6 +244,7 @@
private static final String TEST_WIFI_REGEX = "test_wlan\\d";
private static final String TEST_P2P_REGEX = "test_p2p-p2p\\d-.*";
private static final String TEST_BT_REGEX = "test_pan\\d";
+ private static final String TEST_CALLER_PKG = "com.test.tethering";
private static final int CELLULAR_NETID = 100;
private static final int WIFI_NETID = 101;
@@ -270,6 +279,7 @@
@Mock private BluetoothPan mBluetoothPan;
@Mock private BluetoothPanShim mBluetoothPanShim;
@Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim;
+ @Mock private TetheringMetrics mTetheringMetrics;
private final MockIpServerDependencies mIpServerDependencies =
spy(new MockIpServerDependencies());
@@ -293,6 +303,7 @@
private OffloadController mOffloadCtrl;
private PrivateAddressCoordinator mPrivateAddressCoordinator;
private SoftApCallback mSoftApCallback;
+ private SoftApCallback mLocalOnlyHotspotCallback;
private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim;
@@ -494,6 +505,11 @@
}
@Override
+ public TetheringMetrics getTetheringMetrics() {
+ return mTetheringMetrics;
+ }
+
+ @Override
public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx,
TetheringConfiguration cfg) {
mPrivateAddressCoordinator = super.getPrivateAddressCoordinator(ctx, cfg);
@@ -658,6 +674,14 @@
verify(mWifiManager).registerSoftApCallback(any(), softApCallbackCaptor.capture());
mSoftApCallback = softApCallbackCaptor.getValue();
+ if (isAtLeastT()) {
+ final ArgumentCaptor<SoftApCallback> localOnlyCallbackCaptor =
+ ArgumentCaptor.forClass(SoftApCallback.class);
+ verify(mWifiManager).registerLocalOnlyHotspotSoftApCallback(any(),
+ localOnlyCallbackCaptor.capture());
+ mLocalOnlyHotspotCallback = localOnlyCallbackCaptor.getValue();
+ }
+
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
}
@@ -852,7 +876,8 @@
private void prepareNcmTethering() {
// Emulate startTethering(TETHERING_NCM) called
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), null);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), TEST_CALLER_PKG,
+ null);
mLooper.dispatchAll();
verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
}
@@ -860,7 +885,7 @@
private void prepareUsbTethering() {
// Emulate pressing the USB tethering button in Settings UI.
final TetheringRequestParcel request = createTetheringRequestParcel(TETHERING_USB);
- mTethering.startTethering(request, null);
+ mTethering.startTethering(request, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
assertEquals(1, mTethering.getActiveTetheringRequests().size());
@@ -933,7 +958,7 @@
// Emulate externally-visible WifiManager effects, when hotspot mode
// is being torn down.
- sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
mLooper.dispatchAll();
@@ -1430,7 +1455,8 @@
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
// Emulate pressing the WiFi tethering button.
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+ null);
mLooper.dispatchAll();
verify(mWifiManager, times(1)).startTetheredHotspot(null);
verifyNoMoreInteractions(mWifiManager);
@@ -1457,7 +1483,8 @@
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
// Emulate pressing the WiFi tethering button.
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+ null);
mLooper.dispatchAll();
verify(mWifiManager, times(1)).startTetheredHotspot(null);
verifyNoMoreInteractions(mWifiManager);
@@ -1506,7 +1533,7 @@
// Emulate externally-visible WifiManager effects, when tethering mode
// is being torn down.
- sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
mLooper.dispatchAll();
@@ -1533,11 +1560,13 @@
doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
// Emulate pressing the WiFi tethering button.
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+ null);
mLooper.dispatchAll();
verify(mWifiManager, times(1)).startTetheredHotspot(null);
verifyNoMoreInteractions(mWifiManager);
verifyNoMoreInteractions(mNetd);
+ verify(mTetheringMetrics).createBuilder(eq(TETHERING_WIFI), anyString());
// Emulate externally-visible WifiManager effects, causing the
// per-interface state machine to start up, and telling us that
@@ -1576,6 +1605,10 @@
verify(mWifiManager).updateInterfaceIpState(
TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR);
+ verify(mTetheringMetrics, times(2)).updateErrorCode(eq(TETHERING_WIFI),
+ eq(TETHER_ERROR_INTERNAL_ERROR));
+ verify(mTetheringMetrics, times(2)).sendReport(eq(TETHERING_WIFI));
+
verifyNoMoreInteractions(mWifiManager);
verifyNoMoreInteractions(mNetd);
}
@@ -1867,7 +1900,8 @@
tetherState = callback.pollTetherStatesChanged();
assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), TEST_CALLER_PKG,
+ null);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
tetherState = callback.pollTetherStatesChanged();
assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface});
@@ -1889,7 +1923,13 @@
mTethering.unregisterTetheringEventCallback(callback);
mLooper.dispatchAll();
mTethering.stopTethering(TETHERING_WIFI);
- sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED);
+ if (isAtLeastT()) {
+ // After T, tethering doesn't support WIFI_AP_STATE_DISABLED with null interface name.
+ callback2.assertNoStateChangeCallback();
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME,
+ IFACE_IP_MODE_TETHERED);
+ }
tetherState = callback2.pollTetherStatesChanged();
assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
mLooper.dispatchAll();
@@ -1964,10 +2004,12 @@
public void testNoDuplicatedEthernetRequest() throws Exception {
final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class);
when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
+ null);
mLooper.dispatchAll();
verify(mEm, times(1)).requestTetheredInterface(any(), any());
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
+ null);
mLooper.dispatchAll();
verifyNoMoreInteractions(mEm);
mTethering.stopTethering(TETHERING_ETHERNET);
@@ -2171,14 +2213,16 @@
final ResultListener thirdResult = new ResultListener(TETHER_ERROR_NO_ERROR);
// Enable USB tethering and check that Tethering starts USB.
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), firstResult);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), TEST_CALLER_PKG,
+ firstResult);
mLooper.dispatchAll();
firstResult.assertHasResult();
verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
verifyNoMoreInteractions(mUsbManager);
// Enable USB tethering again with the same request and expect no change to USB.
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), secondResult);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), TEST_CALLER_PKG,
+ secondResult);
mLooper.dispatchAll();
secondResult.assertHasResult();
verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE);
@@ -2187,7 +2231,8 @@
// Enable USB tethering with a different request and expect that USB is stopped and
// started.
mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
- serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), thirdResult);
+ serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+ TEST_CALLER_PKG, thirdResult);
mLooper.dispatchAll();
thirdResult.assertHasResult();
verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
@@ -2216,7 +2261,8 @@
final ArgumentCaptor<DhcpServingParamsParcel> dhcpParamsCaptor =
ArgumentCaptor.forClass(DhcpServingParamsParcel.class);
mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB,
- serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), null);
+ serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL),
+ TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM);
mTethering.interfaceStatusChanged(TEST_NCM_IFNAME, true);
@@ -2284,7 +2330,7 @@
final TetheringRequestParcel wifiNotExemptRequest =
createTetheringRequestParcel(TETHERING_WIFI, null, null, false,
CONNECTIVITY_SCOPE_GLOBAL);
- mTethering.startTethering(wifiNotExemptRequest, null);
+ mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
@@ -2298,7 +2344,7 @@
final TetheringRequestParcel wifiExemptRequest =
createTetheringRequestParcel(TETHERING_WIFI, null, null, true,
CONNECTIVITY_SCOPE_GLOBAL);
- mTethering.startTethering(wifiExemptRequest, null);
+ mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
@@ -2311,14 +2357,14 @@
// If one app enables tethering without provisioning check first, then another app enables
// tethering of the same type but does not disable the provisioning check.
setupForRequiredProvisioning();
- mTethering.startTethering(wifiExemptRequest, null);
+ mTethering.startTethering(wifiExemptRequest, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false);
verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI);
assertTrue(mEntitleMgr.isCellularUpstreamPermitted());
reset(mEntitleMgr);
setupForRequiredProvisioning();
- mTethering.startTethering(wifiNotExemptRequest, null);
+ mTethering.startTethering(wifiNotExemptRequest, TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false);
verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI);
@@ -2408,7 +2454,8 @@
when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
final ArgumentCaptor<TetheredInterfaceCallback> callbackCaptor =
ArgumentCaptor.forClass(TetheredInterfaceCallback.class);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET),
+ TEST_CALLER_PKG, null);
mLooper.dispatchAll();
verify(mEm).requestTetheredInterface(any(), callbackCaptor.capture());
TetheredInterfaceCallback ethCallback = callbackCaptor.getValue();
@@ -2490,12 +2537,11 @@
eventCallbacks = dhcpEventCbsCaptor.getValue();
// Update lease for local only tethering.
final MacAddress testMac1 = MacAddress.fromString("11:11:11:11:11:11");
- final ArrayList<DhcpLeaseParcelable> p2pLeases = new ArrayList<>();
- p2pLeases.add(createDhcpLeaseParcelable("clientId1", testMac1, "192.168.50.24", 24,
- Long.MAX_VALUE, "test1"));
- notifyDhcpLeasesChanged(p2pLeases, eventCallbacks);
- final List<TetheredClient> clients = toTetheredClients(p2pLeases, TETHERING_WIFI_P2P);
- callback.expectTetheredClientChanged(clients);
+ final DhcpLeaseParcelable p2pLease = createDhcpLeaseParcelable("clientId1", testMac1,
+ "192.168.50.24", 24, Long.MAX_VALUE, "test1");
+ final List<TetheredClient> p2pClients = notifyDhcpLeasesChanged(TETHERING_WIFI_P2P,
+ eventCallbacks, p2pLease);
+ callback.expectTetheredClientChanged(p2pClients);
reset(mDhcpServer);
// Run wifi tethering.
@@ -2505,25 +2551,20 @@
any(), dhcpEventCbsCaptor.capture());
eventCallbacks = dhcpEventCbsCaptor.getValue();
// Update mac address from softAp callback before getting dhcp lease.
- final ArrayList<WifiClient> wifiClients = new ArrayList<>();
final MacAddress testMac2 = MacAddress.fromString("22:22:22:22:22:22");
- final WifiClient testClient = mock(WifiClient.class);
- when(testClient.getMacAddress()).thenReturn(testMac2);
- wifiClients.add(testClient);
- mSoftApCallback.onConnectedClientsChanged(wifiClients);
- final TetheredClient noAddrClient = new TetheredClient(testMac2,
- Collections.emptyList() /* addresses */, TETHERING_WIFI);
- clients.add(noAddrClient);
- callback.expectTetheredClientChanged(clients);
+ 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.
- clients.remove(noAddrClient);
- final ArrayList<DhcpLeaseParcelable> wifiLeases = new ArrayList<>();
- wifiLeases.add(createDhcpLeaseParcelable("clientId2", testMac2, "192.168.43.24", 24,
- Long.MAX_VALUE, "test2"));
- notifyDhcpLeasesChanged(wifiLeases, eventCallbacks);
- clients.addAll(toTetheredClients(wifiLeases, TETHERING_WIFI));
- callback.expectTetheredClientChanged(clients);
+ 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);
// Test onStarted callback that register second callback when tethering is running.
TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
@@ -2531,18 +2572,74 @@
mTethering.registerTetheringEventCallback(callback2);
mLooper.dispatchAll();
});
- callback2.expectTetheredClientChanged(clients);
+ callback2.expectTetheredClientChanged(p2pAndWifiClients);
}
- private void notifyDhcpLeasesChanged(List<DhcpLeaseParcelable> leaseParcelables,
- IDhcpEventCallbacks callback) throws Exception {
- callback.onLeasesChanged(leaseParcelables);
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testUpdateConnectedClientsForLocalOnlyHotspot() throws Exception {
+ TestTetheringEventCallback callback = new TestTetheringEventCallback();
+ runAsShell(NETWORK_SETTINGS, () -> {
+ mTethering.registerTetheringEventCallback(callback);
+ mLooper.dispatchAll();
+ });
+ callback.expectTetheredClientChanged(Collections.emptyList());
+
+ // Run local only hotspot.
+ mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
+ sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
+
+ final ArgumentCaptor<IDhcpEventCallbacks> dhcpEventCbsCaptor =
+ ArgumentCaptor.forClass(IDhcpEventCallbacks.class);
+ 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 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);
+
+ // Client disconnect from local only hotspot.
+ mLocalOnlyHotspotCallback.onConnectedClientsChanged(Collections.emptyList());
+ callback.expectTetheredClientChanged(Collections.emptyList());
+ }
+
+ private TetheredClient notifyConnectedWifiClientsChanged(final MacAddress mac,
+ boolean isLocalOnly) throws Exception {
+ final ArrayList<WifiClient> wifiClients = new ArrayList<>();
+ final WifiClient testClient = mock(WifiClient.class);
+ when(testClient.getMacAddress()).thenReturn(mac);
+ wifiClients.add(testClient);
+ if (isLocalOnly) {
+ mLocalOnlyHotspotCallback.onConnectedClientsChanged(wifiClients);
+ } else {
+ mSoftApCallback.onConnectedClientsChanged(wifiClients);
+ }
+ return new TetheredClient(mac, Collections.emptyList() /* addresses */, TETHERING_WIFI);
+ }
+
+ private List<TetheredClient> notifyDhcpLeasesChanged(int type, IDhcpEventCallbacks callback,
+ DhcpLeaseParcelable... leases) throws Exception {
+ final List<DhcpLeaseParcelable> dhcpLeases = Arrays.asList(leases);
+ callback.onLeasesChanged(dhcpLeases);
mLooper.dispatchAll();
+
+ return toTetheredClients(dhcpLeases, type);
}
private List<TetheredClient> toTetheredClients(List<DhcpLeaseParcelable> leaseParcelables,
int type) throws Exception {
- final ArrayList<TetheredClient> leases = new ArrayList<>();
+ final ArrayList<TetheredClient> clients = new ArrayList<>();
for (DhcpLeaseParcelable lease : leaseParcelables) {
final LinkAddress address = new LinkAddress(
intToInet4AddressHTH(lease.netAddr), lease.prefixLength,
@@ -2552,13 +2649,13 @@
final MacAddress macAddress = MacAddress.fromBytes(lease.hwAddr);
final AddressInfo addressInfo = new TetheredClient.AddressInfo(address, lease.hostname);
- leases.add(new TetheredClient(
+ clients.add(new TetheredClient(
macAddress,
Collections.singletonList(addressInfo),
type));
}
- return leases;
+ return clients;
}
private DhcpLeaseParcelable createDhcpLeaseParcelable(final String clientId,
@@ -2583,7 +2680,8 @@
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+ TEST_CALLER_PKG, result);
mLooper.dispatchAll();
verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
result.assertHasResult();
@@ -2618,7 +2716,8 @@
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+ TEST_CALLER_PKG, result);
mLooper.dispatchAll();
verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
result.assertHasResult();
@@ -2639,7 +2738,8 @@
// already bound.
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
final ResultListener secondResult = new ResultListener(TETHER_ERROR_NO_ERROR);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), secondResult);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+ TEST_CALLER_PKG, secondResult);
mLooper.dispatchAll();
verifySetBluetoothTethering(true /* enable */, false /* bindToPanService */);
secondResult.assertHasResult();
@@ -2660,7 +2760,8 @@
public void testBluetoothServiceDisconnects() throws Exception {
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
+ TEST_CALLER_PKG, result);
mLooper.dispatchAll();
ServiceListener panListener = verifySetBluetoothTethering(true /* enable */,
true /* bindToPanService */);
@@ -2811,18 +2912,26 @@
runNcmTethering();
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
any(), any());
+ verify(mTetheringMetrics).createBuilder(eq(TETHERING_NCM), anyString());
// Change the USB tethering function to NCM. Because the USB tethering function was set to
// RNDIS (the default), tethering is stopped.
forceUsbTetheringUse(TETHER_USB_NCM_FUNCTION);
verifyUsbTetheringStopDueToSettingChange(TEST_NCM_IFNAME);
+ verify(mTetheringMetrics).updateErrorCode(anyInt(), eq(TETHER_ERROR_NO_ERROR));
+ verify(mTetheringMetrics).sendReport(eq(TETHERING_NCM));
// If TETHERING_USB is forced to use ncm function, TETHERING_NCM would no longer be
// available.
final ResultListener ncmResult = new ResultListener(TETHER_ERROR_SERVICE_UNAVAIL);
- mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), ncmResult);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), TEST_CALLER_PKG,
+ ncmResult);
mLooper.dispatchAll();
ncmResult.assertHasResult();
+ verify(mTetheringMetrics, times(2)).createBuilder(eq(TETHERING_NCM), anyString());
+ verify(mTetheringMetrics).updateErrorCode(eq(TETHERING_NCM),
+ eq(TETHER_ERROR_SERVICE_UNAVAIL));
+ verify(mTetheringMetrics, times(2)).sendReport(eq(TETHERING_NCM));
// Run TETHERING_USB with ncm configuration.
runDualStackUsbTethering(TEST_NCM_IFNAME);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index 97cebd8..9b9507b 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -49,7 +49,6 @@
import android.net.LinkProperties;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
-import android.net.util.SharedLog;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -60,6 +59,7 @@
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
+import com.android.net.module.util.SharedLog;
import com.android.networkstack.tethering.TestConnectivityManager.NetworkRequestInfo;
import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
new file mode 100644
index 0000000..6a85718
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering.metrics;
+
+import static android.net.TetheringManager.TETHERING_BLUETOOTH;
+import static android.net.TetheringManager.TETHERING_ETHERNET;
+import static android.net.TetheringManager.TETHERING_NCM;
+import static android.net.TetheringManager.TETHERING_USB;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.TetheringManager.TETHERING_WIFI_P2P;
+import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_DISABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
+import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
+import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
+import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
+import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE;
+import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE;
+import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
+import static android.net.TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.stats.connectivity.DownstreamType;
+import android.stats.connectivity.ErrorCode;
+import android.stats.connectivity.UpstreamType;
+import android.stats.connectivity.UserType;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class TetheringMetricsTest {
+ private static final String TEST_CALLER_PKG = "com.test.caller.pkg";
+ private static final String SETTINGS_PKG = "com.android.settings";
+ private static final String SYSTEMUI_PKG = "com.android.systemui";
+ private static final String GMS_PKG = "com.google.android.gms";
+ private TetheringMetrics mTetheringMetrics;
+
+ private final NetworkTetheringReported.Builder mStatsBuilder =
+ NetworkTetheringReported.newBuilder();
+
+ private class MockTetheringMetrics extends TetheringMetrics {
+ @Override
+ public void write(final NetworkTetheringReported reported) { }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mTetheringMetrics = spy(new MockTetheringMetrics());
+ }
+
+ private void verifyReport(DownstreamType downstream, ErrorCode error, UserType user)
+ throws Exception {
+ final NetworkTetheringReported expectedReport =
+ mStatsBuilder.setDownstreamType(downstream)
+ .setUserType(user)
+ .setUpstreamType(UpstreamType.UT_UNKNOWN)
+ .setErrorCode(error)
+ .build();
+ verify(mTetheringMetrics).write(expectedReport);
+ }
+
+ private void updateErrorAndSendReport(int downstream, int error) {
+ mTetheringMetrics.updateErrorCode(downstream, error);
+ mTetheringMetrics.sendReport(downstream);
+ }
+
+ private void runDownstreamTypesTest(final Pair<Integer, DownstreamType>... testPairs)
+ throws Exception {
+ for (Pair<Integer, DownstreamType> testPair : testPairs) {
+ final int type = testPair.first;
+ final DownstreamType expectedResult = testPair.second;
+
+ mTetheringMetrics.createBuilder(type, TEST_CALLER_PKG);
+ updateErrorAndSendReport(type, TETHER_ERROR_NO_ERROR);
+ verifyReport(expectedResult, ErrorCode.EC_NO_ERROR, UserType.USER_UNKNOWN);
+ reset(mTetheringMetrics);
+ }
+ }
+
+ @Test
+ public void testDownstreamTypes() throws Exception {
+ runDownstreamTypesTest(new Pair<>(TETHERING_WIFI, DownstreamType.DS_TETHERING_WIFI),
+ new Pair<>(TETHERING_WIFI_P2P, DownstreamType.DS_TETHERING_WIFI_P2P),
+ new Pair<>(TETHERING_BLUETOOTH, DownstreamType.DS_TETHERING_BLUETOOTH),
+ new Pair<>(TETHERING_USB, DownstreamType.DS_TETHERING_USB),
+ new Pair<>(TETHERING_NCM, DownstreamType.DS_TETHERING_NCM),
+ new Pair<>(TETHERING_ETHERNET, DownstreamType.DS_TETHERING_ETHERNET));
+ }
+
+ private void runErrorCodesTest(final Pair<Integer, ErrorCode>... testPairs)
+ throws Exception {
+ for (Pair<Integer, ErrorCode> testPair : testPairs) {
+ final int errorCode = testPair.first;
+ final ErrorCode expectedResult = testPair.second;
+
+ mTetheringMetrics.createBuilder(TETHERING_WIFI, TEST_CALLER_PKG);
+ updateErrorAndSendReport(TETHERING_WIFI, errorCode);
+ verifyReport(DownstreamType.DS_TETHERING_WIFI, expectedResult, UserType.USER_UNKNOWN);
+ reset(mTetheringMetrics);
+ }
+ }
+
+ @Test
+ public void testErrorCodes() throws Exception {
+ runErrorCodesTest(new Pair<>(TETHER_ERROR_NO_ERROR, ErrorCode.EC_NO_ERROR),
+ new Pair<>(TETHER_ERROR_UNKNOWN_IFACE, ErrorCode.EC_UNKNOWN_IFACE),
+ new Pair<>(TETHER_ERROR_SERVICE_UNAVAIL, ErrorCode.EC_SERVICE_UNAVAIL),
+ new Pair<>(TETHER_ERROR_UNSUPPORTED, ErrorCode.EC_UNSUPPORTED),
+ new Pair<>(TETHER_ERROR_UNAVAIL_IFACE, ErrorCode.EC_UNAVAIL_IFACE),
+ new Pair<>(TETHER_ERROR_INTERNAL_ERROR, ErrorCode.EC_INTERNAL_ERROR),
+ new Pair<>(TETHER_ERROR_TETHER_IFACE_ERROR, ErrorCode.EC_TETHER_IFACE_ERROR),
+ new Pair<>(TETHER_ERROR_UNTETHER_IFACE_ERROR, ErrorCode.EC_UNTETHER_IFACE_ERROR),
+ new Pair<>(TETHER_ERROR_ENABLE_FORWARDING_ERROR,
+ ErrorCode.EC_ENABLE_FORWARDING_ERROR),
+ new Pair<>(TETHER_ERROR_DISABLE_FORWARDING_ERROR,
+ ErrorCode.EC_DISABLE_FORWARDING_ERROR),
+ new Pair<>(TETHER_ERROR_IFACE_CFG_ERROR, ErrorCode.EC_IFACE_CFG_ERROR),
+ new Pair<>(TETHER_ERROR_PROVISIONING_FAILED, ErrorCode.EC_PROVISIONING_FAILED),
+ new Pair<>(TETHER_ERROR_DHCPSERVER_ERROR, ErrorCode.EC_DHCPSERVER_ERROR),
+ new Pair<>(TETHER_ERROR_ENTITLEMENT_UNKNOWN, ErrorCode.EC_ENTITLEMENT_UNKNOWN),
+ new Pair<>(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION,
+ ErrorCode.EC_NO_CHANGE_TETHERING_PERMISSION),
+ new Pair<>(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION,
+ ErrorCode.EC_NO_ACCESS_TETHERING_PERMISSION),
+ new Pair<>(TETHER_ERROR_UNKNOWN_TYPE, ErrorCode.EC_UNKNOWN_TYPE));
+ }
+
+ private void runUserTypesTest(final Pair<String, UserType>... testPairs)
+ throws Exception {
+ for (Pair<String, UserType> testPair : testPairs) {
+ final String callerPkg = testPair.first;
+ final UserType expectedResult = testPair.second;
+
+ mTetheringMetrics.createBuilder(TETHERING_WIFI, callerPkg);
+ updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
+ verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR, expectedResult);
+ reset(mTetheringMetrics);
+ }
+ }
+
+ @Test
+ public void testUserTypes() throws Exception {
+ runUserTypesTest(new Pair<>(TEST_CALLER_PKG, UserType.USER_UNKNOWN),
+ new Pair<>(SETTINGS_PKG, UserType.USER_SETTINGS),
+ new Pair<>(SYSTEMUI_PKG, UserType.USER_SYSTEMUI),
+ new Pair<>(GMS_PKG, UserType.USER_GMS));
+ }
+
+ @Test
+ public void testMultiBuildersCreatedBeforeSendReport() throws Exception {
+ mTetheringMetrics.createBuilder(TETHERING_WIFI, SETTINGS_PKG);
+ mTetheringMetrics.createBuilder(TETHERING_USB, SYSTEMUI_PKG);
+ mTetheringMetrics.createBuilder(TETHERING_BLUETOOTH, GMS_PKG);
+
+ updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_DHCPSERVER_ERROR);
+ verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_DHCPSERVER_ERROR,
+ UserType.USER_SETTINGS);
+
+ updateErrorAndSendReport(TETHERING_USB, TETHER_ERROR_ENABLE_FORWARDING_ERROR);
+ verifyReport(DownstreamType.DS_TETHERING_USB, ErrorCode.EC_ENABLE_FORWARDING_ERROR,
+ UserType.USER_SYSTEMUI);
+
+ updateErrorAndSendReport(TETHERING_BLUETOOTH, TETHER_ERROR_TETHER_IFACE_ERROR);
+ verifyReport(DownstreamType.DS_TETHERING_BLUETOOTH, ErrorCode.EC_TETHER_IFACE_ERROR,
+ UserType.USER_GMS);
+ }
+}
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 1fe0e9a..78fca29 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -25,8 +25,14 @@
name: "bpf_connectivity_headers",
vendor_available: false,
host_supported: false,
- header_libs: ["bpf_headers"],
- export_header_lib_headers: ["bpf_headers"],
+ header_libs: [
+ "bpf_headers",
+ "netd_mainline_headers",
+ ],
+ export_header_lib_headers: [
+ "bpf_headers",
+ "netd_mainline_headers",
+ ],
export_include_dirs: ["."],
cflags: [
"-Wall",
@@ -37,11 +43,8 @@
apex_available: [
"//apex_available:platform",
"com.android.tethering",
- ],
+ ],
visibility: [
- // TODO: remove it when NetworkStatsService is moved into the mainline module and no more
- // calls to JNI in libservices.core.
- "//frameworks/base/services/core/jni",
"//packages/modules/Connectivity/netd",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service/native/libs/libclat",
@@ -50,7 +53,6 @@
"//packages/modules/Connectivity/tests/native",
"//packages/modules/Connectivity/service-t/native/libs/libnetworkstats",
"//packages/modules/Connectivity/tests/unit/jni",
- "//system/netd/server",
"//system/netd/tests",
],
}
@@ -61,6 +63,7 @@
bpf {
name: "block.o",
srcs: ["block.c"],
+ btf: true,
cflags: [
"-Wall",
"-Werror",
@@ -71,6 +74,7 @@
bpf {
name: "dscp_policy.o",
srcs: ["dscp_policy.c"],
+ btf: true,
cflags: [
"-Wall",
"-Werror",
@@ -97,27 +101,25 @@
}
bpf {
- name: "clatd.o_mainline",
+ name: "clatd.o",
srcs: ["clatd.c"],
+ btf: true,
cflags: [
"-Wall",
"-Werror",
],
- include_dirs: [
- "frameworks/libs/net/common/netd/libnetdutils/include",
- ],
sub_dir: "net_shared",
}
bpf {
- name: "netd.o_mainline",
+ // WARNING: Android T's non-updatable netd depends on 'netd' string for xt_bpf programs it loads
+ name: "netd.o",
srcs: ["netd.c"],
+ btf: true,
cflags: [
"-Wall",
"-Werror",
],
- include_dirs: [
- "frameworks/libs/net/common/netd/libnetdutils/include",
- ],
- sub_dir: "net_shared",
+ // WARNING: Android T's non-updatable netd depends on 'netd_shared' string for xt_bpf programs
+ sub_dir: "netd_shared",
}
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
index ddd9a1c..f2a3e62 100644
--- a/bpf_progs/block.c
+++ b/bpf_progs/block.c
@@ -19,6 +19,9 @@
#include <netinet/in.h>
#include <stdint.h>
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
#include "bpf_helpers.h"
#define ALLOW 1
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index 2ddc7b8..85b9f86 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -21,6 +21,11 @@
#include <linux/in.h>
#include <linux/in6.h>
+#ifdef __cplusplus
+#include <string_view>
+#include "XtBpfProgLocations.h"
+#endif
+
// This header file is shared by eBPF kernel programs (C) and netd (C++) and
// some of the maps are also accessed directly from Java mainline module code.
//
@@ -98,30 +103,52 @@
static const int CONFIGURATION_MAP_SIZE = 2;
static const int UID_OWNER_MAP_SIZE = 2000;
-#define BPF_PATH "/sys/fs/bpf/"
+#ifdef __cplusplus
-#define BPF_EGRESS_PROG_PATH BPF_PATH "prog_netd_cgroupskb_egress_stats"
-#define BPF_INGRESS_PROG_PATH BPF_PATH "prog_netd_cgroupskb_ingress_stats"
-#define XT_BPF_INGRESS_PROG_PATH BPF_PATH "prog_netd_skfilter_ingress_xtbpf"
-#define XT_BPF_EGRESS_PROG_PATH BPF_PATH "prog_netd_skfilter_egress_xtbpf"
-#define XT_BPF_ALLOWLIST_PROG_PATH BPF_PATH "prog_netd_skfilter_allowlist_xtbpf"
-#define XT_BPF_DENYLIST_PROG_PATH BPF_PATH "prog_netd_skfilter_denylist_xtbpf"
-#define CGROUP_SOCKET_PROG_PATH BPF_PATH "prog_netd_cgroupsock_inet_create"
+#define BPF_NETD_PATH "/sys/fs/bpf/netd_shared/"
+
+#define BPF_EGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupskb_egress_stats"
+#define BPF_INGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupskb_ingress_stats"
+
+#define ASSERT_STRING_EQUAL(s1, s2) \
+ static_assert(std::string_view(s1) == std::string_view(s2), "mismatch vs Android T netd")
+
+/* -=-=-=-=- WARNING -=-=-=-=-
+ *
+ * These 4 xt_bpf program paths are actually defined by:
+ * //system/netd/include/mainline/XtBpfProgLocations.h
+ * which is intentionally a non-automerged location.
+ *
+ * They are *UNCHANGEABLE* due to being hard coded in Android T's netd binary
+ * as such we have compile time asserts that things match.
+ * (which will be validated during build on mainline-prod branch against old system/netd)
+ *
+ * If you break this, netd on T will fail to start with your tethering mainline module.
+ */
+ASSERT_STRING_EQUAL(XT_BPF_INGRESS_PROG_PATH, BPF_NETD_PATH "prog_netd_skfilter_ingress_xtbpf");
+ASSERT_STRING_EQUAL(XT_BPF_EGRESS_PROG_PATH, BPF_NETD_PATH "prog_netd_skfilter_egress_xtbpf");
+ASSERT_STRING_EQUAL(XT_BPF_ALLOWLIST_PROG_PATH, BPF_NETD_PATH "prog_netd_skfilter_allowlist_xtbpf");
+ASSERT_STRING_EQUAL(XT_BPF_DENYLIST_PROG_PATH, BPF_NETD_PATH "prog_netd_skfilter_denylist_xtbpf");
+
+#define CGROUP_SOCKET_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
#define TC_BPF_INGRESS_ACCOUNT_PROG_NAME "prog_netd_schedact_ingress_account"
-#define TC_BPF_INGRESS_ACCOUNT_PROG_PATH BPF_PATH TC_BPF_INGRESS_ACCOUNT_PROG_NAME
+#define TC_BPF_INGRESS_ACCOUNT_PROG_PATH BPF_NETD_PATH TC_BPF_INGRESS_ACCOUNT_PROG_NAME
-#define COOKIE_TAG_MAP_PATH BPF_PATH "map_netd_cookie_tag_map"
-#define UID_COUNTERSET_MAP_PATH BPF_PATH "map_netd_uid_counterset_map"
-#define APP_UID_STATS_MAP_PATH BPF_PATH "map_netd_app_uid_stats_map"
-#define STATS_MAP_A_PATH BPF_PATH "map_netd_stats_map_A"
-#define STATS_MAP_B_PATH BPF_PATH "map_netd_stats_map_B"
-#define IFACE_INDEX_NAME_MAP_PATH BPF_PATH "map_netd_iface_index_name_map"
-#define IFACE_STATS_MAP_PATH BPF_PATH "map_netd_iface_stats_map"
-#define CONFIGURATION_MAP_PATH BPF_PATH "map_netd_configuration_map"
-#define UID_OWNER_MAP_PATH BPF_PATH "map_netd_uid_owner_map"
-#define UID_PERMISSION_MAP_PATH BPF_PATH "map_netd_uid_permission_map"
+#define COOKIE_TAG_MAP_PATH BPF_NETD_PATH "map_netd_cookie_tag_map"
+#define UID_COUNTERSET_MAP_PATH BPF_NETD_PATH "map_netd_uid_counterset_map"
+#define APP_UID_STATS_MAP_PATH BPF_NETD_PATH "map_netd_app_uid_stats_map"
+#define STATS_MAP_A_PATH BPF_NETD_PATH "map_netd_stats_map_A"
+#define STATS_MAP_B_PATH BPF_NETD_PATH "map_netd_stats_map_B"
+#define IFACE_INDEX_NAME_MAP_PATH BPF_NETD_PATH "map_netd_iface_index_name_map"
+#define IFACE_STATS_MAP_PATH BPF_NETD_PATH "map_netd_iface_stats_map"
+#define CONFIGURATION_MAP_PATH BPF_NETD_PATH "map_netd_configuration_map"
+#define UID_OWNER_MAP_PATH BPF_NETD_PATH "map_netd_uid_owner_map"
+#define UID_PERMISSION_MAP_PATH BPF_NETD_PATH "map_netd_uid_permission_map"
+#endif // __cplusplus
+
+// LINT.IfChange(match_type)
enum UidOwnerMatchType {
NO_MATCH = 0,
HAPPY_BOX_MATCH = (1 << 0),
@@ -132,7 +159,12 @@
RESTRICTED_MATCH = (1 << 5),
LOW_POWER_STANDBY_MATCH = (1 << 6),
IIF_MATCH = (1 << 7),
+ LOCKDOWN_VPN_MATCH = (1 << 8),
+ OEM_DENY_1_MATCH = (1 << 9),
+ OEM_DENY_2_MATCH = (1 << 10),
+ OEM_DENY_3_MATCH = (1 << 11),
};
+// LINT.ThenChange(packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java)
enum BpfPermissionMatch {
BPF_PERMISSION_INTERNET = 1 << 2,
@@ -146,9 +178,9 @@
SELECT_MAP_B,
};
-// TODO: change the configuration object from an 8-bit bitmask to an object with clearer
+// TODO: change the configuration object from a bitmask to an object with clearer
// semantics, like a struct.
-typedef uint8_t BpfConfig;
+typedef uint32_t BpfConfig;
static const BpfConfig DEFAULT_CONFIG = 0;
typedef struct {
@@ -159,16 +191,10 @@
} UidOwnerValue;
STRUCT_SIZE(UidOwnerValue, 2 * 4); // 8
-#define UID_RULES_CONFIGURATION_KEY 1
-#define CURRENT_STATS_MAP_CONFIGURATION_KEY 2
-
-#define CLAT_INGRESS6_PROG_RAWIP_NAME "prog_clatd_schedcls_ingress6_clat_rawip"
-#define CLAT_INGRESS6_PROG_ETHER_NAME "prog_clatd_schedcls_ingress6_clat_ether"
-
-#define CLAT_INGRESS6_PROG_RAWIP_PATH BPF_PATH CLAT_INGRESS6_PROG_RAWIP_NAME
-#define CLAT_INGRESS6_PROG_ETHER_PATH BPF_PATH CLAT_INGRESS6_PROG_ETHER_NAME
-
-#define CLAT_INGRESS6_MAP_PATH BPF_PATH "map_clatd_clat_ingress6_map"
+// Entry in the configuration map that stores which UID rules are enabled.
+#define UID_RULES_CONFIGURATION_KEY 0
+// Entry in the configuration map that stores which stats map is currently in use.
+#define CURRENT_STATS_MAP_CONFIGURATION_KEY 1
typedef struct {
uint32_t iif; // The input interface index
@@ -183,14 +209,6 @@
} ClatIngress6Value;
STRUCT_SIZE(ClatIngress6Value, 4 + 4); // 8
-#define CLAT_EGRESS4_PROG_RAWIP_NAME "prog_clatd_schedcls_egress4_clat_rawip"
-#define CLAT_EGRESS4_PROG_ETHER_NAME "prog_clatd_schedcls_egress4_clat_ether"
-
-#define CLAT_EGRESS4_PROG_RAWIP_PATH BPF_PATH CLAT_EGRESS4_PROG_RAWIP_NAME
-#define CLAT_EGRESS4_PROG_ETHER_PATH BPF_PATH CLAT_EGRESS4_PROG_ETHER_NAME
-
-#define CLAT_EGRESS4_MAP_PATH BPF_PATH "map_clatd_clat_egress4_map"
-
typedef struct {
uint32_t iif; // The input interface index
struct in_addr local4; // The source IPv4 address
diff --git a/bpf_progs/bpf_tethering.h b/bpf_progs/bpf_tethering.h
index b0ec8f6..f9ef6ef 100644
--- a/bpf_progs/bpf_tethering.h
+++ b/bpf_progs/bpf_tethering.h
@@ -73,10 +73,6 @@
#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
-#define BPF_PATH_TETHER BPF_PATH "tethering/"
-
-#define TETHER_STATS_MAP_PATH BPF_PATH_TETHER "map_offload_tether_stats_map"
-
typedef uint32_t TetherStatsKey; // upstream ifindex
typedef struct {
@@ -89,19 +85,9 @@
} TetherStatsValue;
STRUCT_SIZE(TetherStatsValue, 6 * 8); // 48
-#define TETHER_LIMIT_MAP_PATH BPF_PATH_TETHER "map_offload_tether_limit_map"
-
typedef uint32_t TetherLimitKey; // upstream ifindex
typedef uint64_t TetherLimitValue; // in bytes
-#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream6_rawip"
-#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream6_ether"
-
-#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME
-#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME
-
-#define TETHER_DOWNSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream6_map"
-
// For now tethering offload only needs to support downstreams that use 6-byte MAC addresses,
// because all downstream types that are currently supported (WiFi, USB, Bluetooth and
// Ethernet) have 6-byte MAC addresses.
@@ -121,8 +107,6 @@
} Tether6Value;
STRUCT_SIZE(Tether6Value, 4 + 14 + 2); // 20
-#define TETHER_DOWNSTREAM64_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream64_map"
-
typedef struct {
uint32_t iif; // The input interface index
uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress)
@@ -146,14 +130,6 @@
} TetherDownstream64Value;
STRUCT_SIZE(TetherDownstream64Value, 4 + 14 + 2 + 4 + 4 + 2 + 2 + 8); // 40
-#define TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream6_rawip"
-#define TETHER_UPSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream6_ether"
-
-#define TETHER_UPSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME
-#define TETHER_UPSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_ETHER_NAME
-
-#define TETHER_UPSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream6_map"
-
typedef struct {
uint32_t iif; // The input interface index
uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress)
@@ -162,23 +138,6 @@
} TetherUpstream6Key;
STRUCT_SIZE(TetherUpstream6Key, 12);
-#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream4_rawip"
-#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream4_ether"
-
-#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME
-#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME
-
-#define TETHER_DOWNSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream4_map"
-
-
-#define TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream4_rawip"
-#define TETHER_UPSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream4_ether"
-
-#define TETHER_UPSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME
-#define TETHER_UPSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_ETHER_NAME
-
-#define TETHER_UPSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream4_map"
-
typedef struct {
uint32_t iif; // The input interface index
uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress)
@@ -202,16 +161,4 @@
} Tether4Value;
STRUCT_SIZE(Tether4Value, 4 + 14 + 2 + 16 + 16 + 2 + 2 + 8); // 64
-#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_downstream_rawip"
-#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_downstream_ether"
-
-#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME
-#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME
-
-#define TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_upstream_rawip"
-#define TETHER_UPSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_upstream_ether"
-
-#define TETHER_UPSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME
-#define TETHER_UPSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_ETHER_NAME
-
#undef STRUCT_SIZE
diff --git a/bpf_progs/clat_mark.h b/bpf_progs/clat_mark.h
new file mode 100644
index 0000000..874d6ae
--- /dev/null
+++ b/bpf_progs/clat_mark.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+/* -=-=-=-=-= WARNING -=-=-=-=-=-
+ *
+ * DO *NOT* *EVER* CHANGE THIS CONSTANT
+ *
+ * This is aidl::android::net::INetd::CLAT_MARK but we can't use that from
+ * pure C code (ie. the eBPF clat program).
+ *
+ * It must match the iptables rules setup by netd on Android T.
+ *
+ * This mark value is used by the eBPF clatd program to mark ingress non-offloaded clat
+ * packets for later dropping in ip6tables bw_raw_PREROUTING.
+ * They need to be dropped *after* the clat daemon (via receive on an AF_PACKET socket)
+ * sees them and thus cannot be dropped from the bpf program itself.
+ */
+static const uint32_t CLAT_MARK = 0xDEADC1A7;
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index 9a9d337..66e9616 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -30,19 +30,17 @@
#define __kernel_udphdr udphdr
#include <linux/udp.h>
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
#include "bpf_helpers.h"
#include "bpf_net_helpers.h"
#include "bpf_shared.h"
+#include "clat_mark.h"
// From kernel:include/net/ip.h
#define IP_DF 0x4000 // Flag: "Don't Fragment"
-// Used for iptables drops ingress clat packet. Beware of clat mark change may break the device
-// which is using the old clat mark in netd platform code. The reason is that the clat mark is a
-// mainline constant since T+ but netd iptable rules (ex: bandwidth control, firewall, and so on)
-// are set in stone.
-#define CLAT_MARK 0xdeadc1a7
-
DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
static inline __always_inline int nat64(struct __sk_buff* skb, bool is_ethernet) {
diff --git a/bpf_progs/dscp_policy.c b/bpf_progs/dscp_policy.c
index 9989e6b..92ea0e2 100644
--- a/bpf_progs/dscp_policy.c
+++ b/bpf_progs/dscp_policy.c
@@ -14,262 +14,301 @@
* limitations under the License.
*/
-#include <linux/types.h>
#include <linux/bpf.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
-#include <linux/if_ether.h>
#include <linux/pkt_cls.h>
#include <linux/tcp.h>
-#include <stdint.h>
+#include <linux/types.h>
#include <netinet/in.h>
#include <netinet/udp.h>
+#include <stdint.h>
#include <string.h>
+// The resulting .o needs to load on the Android T beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
#include "bpf_helpers.h"
+#include "dscp_policy.h"
-#define MAX_POLICIES 16
-#define MAP_A 1
-#define MAP_B 2
+#define ECN_MASK 3
+#define IP4_OFFSET(field, header) (header + offsetof(struct iphdr, field))
+#define UPDATE_TOS(dscp, tos) (dscp << 2) | (tos & ECN_MASK)
+#define UPDATE_PRIORITY(dscp) ((dscp >> 2) + 0x60)
+#define UPDATE_FLOW_LABEL(dscp, flow_lbl) ((dscp & 0xf) << 6) + (flow_lbl >> 6)
-#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
-
-// TODO: these are already defined in /system/netd/bpf_progs/bpf_net_helpers.h
-// should they be moved to common location?
-static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
- (void*)BPF_FUNC_get_socket_cookie;
-static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
- __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
-static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
- __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
-
-typedef struct {
- // Add family here to match __sk_buff ?
- struct in_addr srcIp;
- struct in_addr dstIp;
- __be16 srcPort;
- __be16 dstPort;
- uint8_t proto;
- uint8_t dscpVal;
- uint8_t pad[2];
-} Ipv4RuleEntry;
-STRUCT_SIZE(Ipv4RuleEntry, 2 * 4 + 2 * 2 + 2 * 1 + 2); // 16, 4 for in_addr
-
-#define SRC_IP_MASK 1
-#define DST_IP_MASK 2
-#define SRC_PORT_MASK 4
-#define DST_PORT_MASK 8
-#define PROTO_MASK 16
-
-typedef struct {
- struct in6_addr srcIp;
- struct in6_addr dstIp;
- __be16 srcPort;
- __be16 dstPortStart;
- __be16 dstPortEnd;
- uint8_t proto;
- uint8_t dscpVal;
- uint8_t mask;
- uint8_t pad[3];
-} Ipv4Policy;
-STRUCT_SIZE(Ipv4Policy, 2 * 16 + 3 * 2 + 3 * 1 + 3); // 44
-
-typedef struct {
- struct in6_addr srcIp;
- struct in6_addr dstIp;
- __be16 srcPort;
- __be16 dstPortStart;
- __be16 dstPortEnd;
- uint8_t proto;
- uint8_t dscpVal;
- uint8_t mask;
- // should we override this struct to include the param bitmask for linear search?
- // For mapping socket to policies, all the params should match exactly since we can
- // pull any missing from the sock itself.
-} Ipv6RuleEntry;
-STRUCT_SIZE(Ipv6RuleEntry, 2 * 16 + 3 * 2 + 3 * 1 + 3); // 44
-
-// TODO: move to using 1 map. Map v4 address to 0xffff::v4
-DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES,
- AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_B, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES,
- AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_A, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES,
- AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_B, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES,
- AID_SYSTEM)
DEFINE_BPF_MAP_GRW(switch_comp_map, ARRAY, int, uint64_t, 1, AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, Ipv4Policy, MAX_POLICIES,
- AID_SYSTEM)
-DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, Ipv6RuleEntry, MAX_POLICIES,
- AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+ AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_B, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+ AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_A, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+ AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_B, HASH, uint64_t, RuleEntry, MAX_POLICIES,
+ AID_SYSTEM)
-DEFINE_BPF_PROG_KVER("schedcls/set_dscp", AID_ROOT, AID_SYSTEM,
- schedcls_set_dscp, KVER(5, 4, 0))
-(struct __sk_buff* skb) {
- int one = 0;
- uint64_t* selectedMap = bpf_switch_comp_map_lookup_elem(&one);
+DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, DscpPolicy, MAX_POLICIES, AID_SYSTEM)
+
+static inline __always_inline void match_policy(struct __sk_buff* skb, bool ipv4, bool is_eth) {
+ void* data = (void*)(long)skb->data;
+ const void* data_end = (void*)(long)skb->data_end;
+
+ const int l2_header_size = is_eth ? sizeof(struct ethhdr) : 0;
+ struct ethhdr* eth = is_eth ? data : NULL;
+
+ if (data + l2_header_size > data_end) return;
+
+ int zero = 0;
+ int hdr_size = 0;
+ uint64_t* selectedMap = bpf_switch_comp_map_lookup_elem(&zero);
// use this with HASH map so map lookup only happens once policies have been added?
if (!selectedMap) {
- return TC_ACT_PIPE;
+ return;
}
// used for map lookup
uint64_t cookie = bpf_get_socket_cookie(skb);
+ if (!cookie) return;
- // Do we need separate maps for ipv4/ipv6
- if (skb->protocol == htons(ETH_P_IP)) { //maybe bpf_htons()
- Ipv4RuleEntry* v4Policy;
- if (*selectedMap == MAP_A) {
- v4Policy = bpf_ipv4_socket_to_policies_map_A_lookup_elem(&cookie);
- } else {
- v4Policy = bpf_ipv4_socket_to_policies_map_B_lookup_elem(&cookie);
- }
-
- // How to use bitmask here to compare params efficiently?
- // TODO: add BPF_PROG_TYPE_SK_SKB prog type to Loader?
-
- void* data = (void*)(long)skb->data;
- const void* data_end = (void*)(long)skb->data_end;
- const struct iphdr* const iph = data;
-
+ uint16_t sport = 0;
+ uint16_t dport = 0;
+ uint8_t protocol = 0; // TODO: Use are reserved value? Or int (-1) and cast to uint below?
+ struct in6_addr srcIp = {};
+ struct in6_addr dstIp = {};
+ uint8_t tos = 0; // Only used for IPv4
+ uint8_t priority = 0; // Only used for IPv6
+ uint8_t flow_lbl = 0; // Only used for IPv6
+ if (ipv4) {
+ const struct iphdr* const iph = is_eth ? (void*)(eth + 1) : data;
+ hdr_size = l2_header_size + sizeof(struct iphdr);
// Must have ipv4 header
- if (data + sizeof(*iph) > data_end) return TC_ACT_PIPE;
+ if (data + hdr_size > data_end) return;
// IP version must be 4
- if (iph->version != 4) return TC_ACT_PIPE;
+ if (iph->version != 4) return;
// We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
- if (iph->ihl != 5) return TC_ACT_PIPE;
+ if (iph->ihl != 5) return;
- if (iph->protocol != IPPROTO_UDP) return TC_ACT_PIPE;
+ // V4 mapped address in in6_addr sets 10/11 position to 0xff.
+ srcIp.s6_addr32[2] = htonl(0x0000ffff);
+ dstIp.s6_addr32[2] = htonl(0x0000ffff);
- struct udphdr *udp;
- udp = data + sizeof(struct iphdr); //sizeof(struct ethhdr)
+ // Copy IPv4 address into in6_addr for easy comparison below.
+ srcIp.s6_addr32[3] = iph->saddr;
+ dstIp.s6_addr32[3] = iph->daddr;
+ protocol = iph->protocol;
+ tos = iph->tos;
+ } else {
+ struct ipv6hdr* ip6h = is_eth ? (void*)(eth + 1) : data;
+ hdr_size = l2_header_size + sizeof(struct ipv6hdr);
+ // Must have ipv6 header
+ if (data + hdr_size > data_end) return;
- if ((void*)(udp + 1) > data_end) return TC_ACT_PIPE;
+ if (ip6h->version != 6) return;
- // Source/destination port in udphdr are stored in be16, need to convert to le16.
- // This can be done via ntohs or htons. Is there a more preferred way?
- // Cached policy was found.
- if (v4Policy && iph->saddr == v4Policy->srcIp.s_addr &&
- iph->daddr == v4Policy->dstIp.s_addr &&
- ntohs(udp->source) == v4Policy->srcPort &&
- ntohs(udp->dest) == v4Policy->dstPort &&
- iph->protocol == v4Policy->proto) {
- // set dscpVal in packet. Least sig 2 bits of TOS
- // reference ipv4_change_dsfield()
+ srcIp = ip6h->saddr;
+ dstIp = ip6h->daddr;
+ protocol = ip6h->nexthdr;
+ priority = ip6h->priority;
+ flow_lbl = ip6h->flow_lbl[0];
+ }
- // TODO: fix checksum...
- int ecn = iph->tos & 3;
- uint8_t newDscpVal = (v4Policy->dscpVal << 2) + ecn;
- int oldDscpVal = iph->tos >> 2;
- bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t));
- bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0);
- return TC_ACT_PIPE;
+ switch (protocol) {
+ case IPPROTO_UDP:
+ case IPPROTO_UDPLITE: {
+ struct udphdr* udp;
+ udp = data + hdr_size;
+ if ((void*)(udp + 1) > data_end) return;
+ sport = udp->source;
+ dport = udp->dest;
+ } break;
+ case IPPROTO_TCP: {
+ struct tcphdr* tcp;
+ tcp = data + hdr_size;
+ if ((void*)(tcp + 1) > data_end) return;
+ sport = tcp->source;
+ dport = tcp->dest;
+ } break;
+ default:
+ return;
+ }
+
+ RuleEntry* existingRule;
+ if (ipv4) {
+ if (*selectedMap == MAP_A) {
+ existingRule = bpf_ipv4_socket_to_policies_map_A_lookup_elem(&cookie);
+ } else {
+ existingRule = bpf_ipv4_socket_to_policies_map_B_lookup_elem(&cookie);
+ }
+ } else {
+ if (*selectedMap == MAP_A) {
+ existingRule = bpf_ipv6_socket_to_policies_map_A_lookup_elem(&cookie);
+ } else {
+ existingRule = bpf_ipv6_socket_to_policies_map_B_lookup_elem(&cookie);
+ }
+ }
+
+ if (existingRule && v6_equal(srcIp, existingRule->srcIp) &&
+ v6_equal(dstIp, existingRule->dstIp) && skb->ifindex == existingRule->ifindex &&
+ ntohs(sport) == htons(existingRule->srcPort) &&
+ ntohs(dport) == htons(existingRule->dstPort) && protocol == existingRule->proto) {
+ if (ipv4) {
+ uint8_t newTos = UPDATE_TOS(existingRule->dscpVal, tos);
+ bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(newTos),
+ sizeof(uint16_t));
+ bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &newTos, sizeof(newTos), 0);
+ } else {
+ uint8_t new_priority = UPDATE_PRIORITY(existingRule->dscpVal);
+ uint8_t new_flow_label = UPDATE_FLOW_LABEL(existingRule->dscpVal, flow_lbl);
+ bpf_skb_store_bytes(skb, 0 + l2_header_size, &new_priority, sizeof(uint8_t), 0);
+ bpf_skb_store_bytes(skb, 1 + l2_header_size, &new_flow_label, sizeof(uint8_t), 0);
+ }
+ return;
+ }
+
+ // Linear scan ipv4_dscp_policies_map since no stored params match skb.
+ int bestScore = -1;
+ uint32_t bestMatch = 0;
+
+ for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
+ int score = 0;
+ uint8_t tempMask = 0;
+ // Using a uint64 in for loop prevents infinite loop during BPF load,
+ // but the key is uint32, so convert back.
+ uint32_t key = i;
+
+ DscpPolicy* policy;
+ if (ipv4) {
+ policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
+ } else {
+ policy = bpf_ipv6_dscp_policies_map_lookup_elem(&key);
}
- // linear scan ipv4_dscp_policies_map, stored socket params do not match actual
- int bestScore = -1;
- uint32_t bestMatch = 0;
+ // If the policy lookup failed, presentFields is 0, or iface index does not match
+ // index on skb buff, then we can continue to next policy.
+ if (!policy || policy->presentFields == 0 || policy->ifindex != skb->ifindex) continue;
- for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
- int score = 0;
- uint8_t tempMask = 0;
- // Using a uint62 in for loop prevents infinite loop during BPF load,
- // but the key is uint32, so convert back.
- uint32_t key = i;
- Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
+ if ((policy->presentFields & SRC_IP_MASK_FLAG) == SRC_IP_MASK_FLAG &&
+ v6_equal(srcIp, policy->srcIp)) {
+ score++;
+ tempMask |= SRC_IP_MASK_FLAG;
+ }
+ if ((policy->presentFields & DST_IP_MASK_FLAG) == DST_IP_MASK_FLAG &&
+ v6_equal(dstIp, policy->dstIp)) {
+ score++;
+ tempMask |= DST_IP_MASK_FLAG;
+ }
+ if ((policy->presentFields & SRC_PORT_MASK_FLAG) == SRC_PORT_MASK_FLAG &&
+ ntohs(sport) == htons(policy->srcPort)) {
+ score++;
+ tempMask |= SRC_PORT_MASK_FLAG;
+ }
+ if ((policy->presentFields & DST_PORT_MASK_FLAG) == DST_PORT_MASK_FLAG &&
+ ntohs(dport) >= htons(policy->dstPortStart) &&
+ ntohs(dport) <= htons(policy->dstPortEnd)) {
+ score++;
+ tempMask |= DST_PORT_MASK_FLAG;
+ }
+ if ((policy->presentFields & PROTO_MASK_FLAG) == PROTO_MASK_FLAG &&
+ protocol == policy->proto) {
+ score++;
+ tempMask |= PROTO_MASK_FLAG;
+ }
- // if mask is 0 continue, key does not have corresponding policy value
- if (policy && policy->mask != 0) {
- if ((policy->mask & SRC_IP_MASK) == SRC_IP_MASK &&
- iph->saddr == policy->srcIp.s6_addr32[3]) {
- score++;
- tempMask |= SRC_IP_MASK;
- }
- if ((policy->mask & DST_IP_MASK) == DST_IP_MASK &&
- iph->daddr == policy->dstIp.s6_addr32[3]) {
- score++;
- tempMask |= DST_IP_MASK;
- }
- if ((policy->mask & SRC_PORT_MASK) == SRC_PORT_MASK &&
- ntohs(udp->source) == htons(policy->srcPort)) {
- score++;
- tempMask |= SRC_PORT_MASK;
- }
- if ((policy->mask & DST_PORT_MASK) == DST_PORT_MASK &&
- ntohs(udp->dest) >= htons(policy->dstPortStart) &&
- ntohs(udp->dest) <= htons(policy->dstPortEnd)) {
- score++;
- tempMask |= DST_PORT_MASK;
- }
- if ((policy->mask & PROTO_MASK) == PROTO_MASK &&
- iph->protocol == policy->proto) {
- score++;
- tempMask |= PROTO_MASK;
- }
+ if (score > bestScore && tempMask == policy->presentFields) {
+ bestMatch = i;
+ bestScore = score;
+ }
+ }
- if (score > bestScore && tempMask == policy->mask) {
- bestMatch = i;
- bestScore = score;
- }
+ uint8_t new_tos = 0; // Can 0 be used as default forwarding value?
+ uint8_t new_dscp = 0;
+ uint8_t new_priority = 0;
+ uint8_t new_flow_lbl = 0;
+ if (bestScore > 0) {
+ DscpPolicy* policy;
+ if (ipv4) {
+ policy = bpf_ipv4_dscp_policies_map_lookup_elem(&bestMatch);
+ } else {
+ policy = bpf_ipv6_dscp_policies_map_lookup_elem(&bestMatch);
+ }
+
+ if (policy) {
+ new_dscp = policy->dscpVal;
+ if (ipv4) {
+ new_tos = UPDATE_TOS(new_dscp, tos);
+ } else {
+ new_priority = UPDATE_PRIORITY(new_dscp);
+ new_flow_lbl = UPDATE_FLOW_LABEL(new_dscp, flow_lbl);
}
}
+ } else
+ return;
- uint8_t newDscpVal = 0; // Can 0 be used as default forwarding value?
- uint8_t curDscp = iph->tos & 252;
- if (bestScore > 0) {
- Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&bestMatch);
- if (policy) {
- // TODO: if DSCP value is already set ignore?
- // TODO: update checksum, for testing increment counter...
- int ecn = iph->tos & 3;
- newDscpVal = (policy->dscpVal << 2) + ecn;
- }
- }
+ RuleEntry value = {
+ .srcIp = srcIp,
+ .dstIp = dstIp,
+ .ifindex = skb->ifindex,
+ .srcPort = sport,
+ .dstPort = dport,
+ .proto = protocol,
+ .dscpVal = new_dscp,
+ };
- Ipv4RuleEntry value = {
- .srcIp.s_addr = iph->saddr,
- .dstIp.s_addr = iph->daddr,
- .srcPort = udp->source,
- .dstPort = udp->dest,
- .proto = iph->protocol,
- .dscpVal = newDscpVal,
- };
-
- if (!cookie)
- return TC_ACT_PIPE;
-
- // Update map
+ // Update map with new policy.
+ if (ipv4) {
if (*selectedMap == MAP_A) {
bpf_ipv4_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
} else {
bpf_ipv4_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
}
-
- // Need to store bytes after updating map or program will not load.
- if (newDscpVal != curDscp) {
- // 1 is the offset (Version/Header length)
- int oldDscpVal = iph->tos >> 2;
- bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t));
- bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0);
- }
-
- } else if (skb->protocol == htons(ETH_P_IPV6)) { //maybe bpf_htons()
- Ipv6RuleEntry* v6Policy;
+ } else {
if (*selectedMap == MAP_A) {
- v6Policy = bpf_ipv6_socket_to_policies_map_A_lookup_elem(&cookie);
+ bpf_ipv6_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
} else {
- v6Policy = bpf_ipv6_socket_to_policies_map_B_lookup_elem(&cookie);
+ bpf_ipv6_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
}
+ }
- if (!v6Policy)
- return TC_ACT_PIPE;
+ // Need to store bytes after updating map or program will not load.
+ if (ipv4 && new_tos != (tos & 252)) {
+ bpf_l3_csum_replace(skb, IP4_OFFSET(check, l2_header_size), htons(tos), htons(new_tos), 2);
+ bpf_skb_store_bytes(skb, IP4_OFFSET(tos, l2_header_size), &new_tos, sizeof(new_tos), 0);
+ } else if (!ipv4 && (new_priority != priority || new_flow_lbl != flow_lbl)) {
+ bpf_skb_store_bytes(skb, l2_header_size, &new_priority, sizeof(new_priority), 0);
+ bpf_skb_store_bytes(skb, l2_header_size + 1, &new_flow_lbl, sizeof(new_flow_lbl), 0);
+ }
+ return;
+}
- // TODO: Add code to process IPv6 packet.
+DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM,
+ schedcls_set_dscp_ether, KVER(5, 15, 0))
+(struct __sk_buff* skb) {
+ if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
+
+ if (skb->protocol == htons(ETH_P_IP)) {
+ match_policy(skb, true, true);
+ } else if (skb->protocol == htons(ETH_P_IPV6)) {
+ match_policy(skb, false, true);
+ }
+
+ // Always return TC_ACT_PIPE
+ return TC_ACT_PIPE;
+}
+
+DEFINE_BPF_PROG_KVER("schedcls/set_dscp_raw_ip", AID_ROOT, AID_SYSTEM,
+ schedcls_set_dscp_raw_ip, KVER(5, 15, 0))
+(struct __sk_buff* skb) {
+ if (skb->protocol == htons(ETH_P_IP)) {
+ match_policy(skb, true, false);
+ } else if (skb->protocol == htons(ETH_P_IPV6)) {
+ match_policy(skb, false, false);
}
// Always return TC_ACT_PIPE
diff --git a/bpf_progs/dscp_policy.h b/bpf_progs/dscp_policy.h
new file mode 100644
index 0000000..1637f7a
--- /dev/null
+++ b/bpf_progs/dscp_policy.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define MAX_POLICIES 16
+#define MAP_A 1
+#define MAP_B 2
+
+#define SRC_IP_MASK_FLAG 1
+#define DST_IP_MASK_FLAG 2
+#define SRC_PORT_MASK_FLAG 4
+#define DST_PORT_MASK_FLAG 8
+#define PROTO_MASK_FLAG 16
+
+#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
+
+#define v6_equal(a, b) \
+ (((a.s6_addr32[0] ^ b.s6_addr32[0]) | \
+ (a.s6_addr32[1] ^ b.s6_addr32[1]) | \
+ (a.s6_addr32[2] ^ b.s6_addr32[2]) | \
+ (a.s6_addr32[3] ^ b.s6_addr32[3])) == 0)
+
+// TODO: these are already defined in packages/modules/Connectivity/bpf_progs/bpf_net_helpers.h.
+// smove to common location in future.
+static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
+ (void*)BPF_FUNC_get_socket_cookie;
+static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
+ __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
+static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
+ __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
+static long (*bpf_skb_ecn_set_ce)(struct __sk_buff* skb) =
+ (void*)BPF_FUNC_skb_ecn_set_ce;
+
+typedef struct {
+ struct in6_addr srcIp;
+ struct in6_addr dstIp;
+ uint32_t ifindex;
+ __be16 srcPort;
+ __be16 dstPortStart;
+ __be16 dstPortEnd;
+ uint8_t proto;
+ uint8_t dscpVal;
+ uint8_t presentFields;
+ uint8_t pad[3];
+} DscpPolicy;
+STRUCT_SIZE(DscpPolicy, 2 * 16 + 4 + 3 * 2 + 3 * 1 + 3); // 48
+
+typedef struct {
+ struct in6_addr srcIp;
+ struct in6_addr dstIp;
+ __u32 ifindex;
+ __be16 srcPort;
+ __be16 dstPort;
+ __u8 proto;
+ __u8 dscpVal;
+ __u8 pad[2];
+} RuleEntry;
+STRUCT_SIZE(RuleEntry, 2 * 16 + 1 * 4 + 2 * 2 + 2 * 1 + 2); // 44
\ No newline at end of file
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index fe9a871..44f76de 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -14,6 +14,9 @@
* limitations under the License.
*/
+// The resulting .o needs to load on the Android T Beta 3 bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_T_BETA3_VERSION
+
#include <bpf_helpers.h>
#include <linux/bpf.h>
#include <linux/if.h>
@@ -25,7 +28,6 @@
#include <linux/ipv6.h>
#include <linux/pkt_cls.h>
#include <linux/tcp.h>
-#include <netdutils/UidConstants.h>
#include <stdbool.h>
#include <stdint.h>
#include "bpf_net_helpers.h"
@@ -49,28 +51,57 @@
#define TCP_FLAG_OFF 13
#define RST_OFFSET 2
-DEFINE_BPF_MAP_GRW(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE,
- AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE,
- AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE,
- AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE,
- AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(configuration_map, HASH, uint32_t, uint8_t, CONFIGURATION_MAP_SIZE,
- AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE,
- AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE, AID_NET_BW_ACCT)
+// For maps netd does not need to access
+#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+ AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", false)
+
+// For maps netd only needs read only access to
+#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+ AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", false)
+
+// For maps netd needs to be able to read and write
+#define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+ DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+ AID_ROOT, AID_NET_BW_ACCT, 0660)
+
+// Bpf map arrays on creation are preinitialized to 0 and do not support deletion of a key,
+// see: kernel/bpf/arraymap.c array_map_delete_elem() returns -EINVAL (from both syscall and ebpf)
+// Additionally on newer kernels the bpf jit can optimize out the lookups.
+// only valid indexes are [0..CONFIGURATION_MAP_SIZE-1]
+DEFINE_BPF_MAP_RO_NETD(configuration_map, ARRAY, uint32_t, uint32_t, CONFIGURATION_MAP_SIZE)
+
+DEFINE_BPF_MAP_RW_NETD(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE)
+DEFINE_BPF_MAP_RW_NETD(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
+DEFINE_BPF_MAP_RO_NETD(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE)
+DEFINE_BPF_MAP_RW_NETD(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE)
/* never actually used from ebpf */
-DEFINE_BPF_MAP_GRW(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE,
- AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_NO_NETD(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE)
+
+// iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
+#define DEFINE_XTBPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+ DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog)
+
+// programs that need to be usable by netd, but not by netutils_wrappers
+#define DEFINE_NETD_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
+ KVER_NONE, KVER_INF, false, "fs_bpf_netd_readonly", "")
+
+// programs that only need to be usable by the system server
+#define DEFINE_SYS_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
+ KVER_NONE, KVER_INF, false, "fs_bpf_net_shared", "")
static __always_inline int is_system_uid(uint32_t uid) {
- return (uid <= MAX_SYSTEM_UID) && (uid >= MIN_SYSTEM_UID);
+ // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
+ // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
+ return (uid < AID_APP_START);
}
/*
@@ -186,6 +217,11 @@
return *config;
}
+// DROP_IF_SET is set of rules that BPF_DROP if rule is globally enabled, and per-uid bit is set
+#define DROP_IF_SET (STANDBY_MATCH | OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH)
+// DROP_IF_UNSET is set of rules that should DROP if globally enabled, and per-uid bit is NOT set
+#define DROP_IF_UNSET (DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH | LOW_POWER_STANDBY_MATCH)
+
static inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid, int direction) {
if (skip_owner_match(skb)) return BPF_PASS;
@@ -194,29 +230,26 @@
BpfConfig enabledRules = getConfig(UID_RULES_CONFIGURATION_KEY);
UidOwnerValue* uidEntry = bpf_uid_owner_map_lookup_elem(&uid);
- uint8_t uidRules = uidEntry ? uidEntry->rule : 0;
+ uint32_t uidRules = uidEntry ? uidEntry->rule : 0;
uint32_t allowed_iif = uidEntry ? uidEntry->iif : 0;
- if (enabledRules) {
- if ((enabledRules & DOZABLE_MATCH) && !(uidRules & DOZABLE_MATCH)) {
- return BPF_DROP;
- }
- if ((enabledRules & STANDBY_MATCH) && (uidRules & STANDBY_MATCH)) {
- return BPF_DROP;
- }
- if ((enabledRules & POWERSAVE_MATCH) && !(uidRules & POWERSAVE_MATCH)) {
- return BPF_DROP;
- }
- if ((enabledRules & RESTRICTED_MATCH) && !(uidRules & RESTRICTED_MATCH)) {
- return BPF_DROP;
- }
- if ((enabledRules & LOW_POWER_STANDBY_MATCH) && !(uidRules & LOW_POWER_STANDBY_MATCH)) {
- return BPF_DROP;
- }
- }
- if (direction == BPF_INGRESS && (uidRules & IIF_MATCH)) {
- // Drops packets not coming from lo nor the allowlisted interface
- if (allowed_iif && skb->ifindex != 1 && skb->ifindex != allowed_iif) {
+ // Warning: funky bit-wise arithmetic: in parallel, for all DROP_IF_SET/UNSET rules
+ // check whether the rules are globally enabled, and if so whether the rules are
+ // set/unset for the specific uid. BPF_DROP if that is the case for ANY of the rules.
+ // We achieve this by masking out only the bits/rules we're interested in checking,
+ // and negating (via bit-wise xor) the bits/rules that should drop if unset.
+ if (enabledRules & (DROP_IF_SET | DROP_IF_UNSET) & (uidRules ^ DROP_IF_UNSET)) return BPF_DROP;
+
+ if (direction == BPF_INGRESS && skb->ifindex != 1) {
+ if (uidRules & IIF_MATCH) {
+ if (allowed_iif && skb->ifindex != allowed_iif) {
+ // Drops packets not coming from lo nor the allowed interface
+ // allowed interface=0 is a wildcard and does not drop packets
+ return BPF_DROP_UNLESS_DNS;
+ }
+ } else if (uidRules & LOCKDOWN_VPN_MATCH) {
+ // Drops packets not coming from lo and rule does not have IIF_MATCH but has
+ // LOCKDOWN_VPN_MATCH
return BPF_DROP_UNLESS_DNS;
}
}
@@ -224,7 +257,7 @@
}
static __always_inline inline void update_stats_with_config(struct __sk_buff* skb, int direction,
- StatsKey* key, uint8_t selectedMap) {
+ StatsKey* key, uint32_t selectedMap) {
if (selectedMap == SELECT_MAP_A) {
update_stats_map_A(skb, direction, key);
} else if (selectedMap == SELECT_MAP_B) {
@@ -276,7 +309,7 @@
if (counterSet) key.counterSet = (uint32_t)*counterSet;
uint32_t mapSettingKey = CURRENT_STATS_MAP_CONFIGURATION_KEY;
- uint8_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey);
+ 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
@@ -297,17 +330,18 @@
return match;
}
-DEFINE_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_ingress)
+DEFINE_NETD_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_ingress)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, BPF_INGRESS);
}
-DEFINE_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_egress)
+DEFINE_NETD_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_egress)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, BPF_EGRESS);
}
-DEFINE_BPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
+// WARNING: Android T's non-updatable netd depends on the name of this program.
+DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
(struct __sk_buff* skb) {
// 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,
@@ -326,7 +360,8 @@
return BPF_MATCH;
}
-DEFINE_BPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
+// WARNING: Android T's non-updatable netd depends on the name of this program.
+DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
(struct __sk_buff* skb) {
// Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule
// (in clat_raw_PREROUTING chain), which triggers before this (in bw_raw_PREROUTING chain).
@@ -338,7 +373,8 @@
return BPF_MATCH;
}
-DEFINE_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN, tc_bpf_ingress_account_prog)
+DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
+ tc_bpf_ingress_account_prog)
(struct __sk_buff* skb) {
if (is_received_skb(skb)) {
// Account for ingress traffic before tc drops it.
@@ -348,7 +384,8 @@
return TC_ACT_UNSPEC;
}
-DEFINE_BPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
+// WARNING: Android T's non-updatable netd depends on the name of this program.
+DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
(struct __sk_buff* skb) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
if (is_system_uid(sock_uid)) return BPF_MATCH;
@@ -365,7 +402,8 @@
return BPF_NOMATCH;
}
-DEFINE_BPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
+// WARNING: Android T's non-updatable netd depends on the name of this program.
+DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
(struct __sk_buff* skb) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
@@ -373,8 +411,7 @@
return BPF_NOMATCH;
}
-DEFINE_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
- KVER(4, 14, 0))
+DEFINE_NETD_BPF_PROG("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create)
(struct bpf_sock* sk) {
uint64_t gid_uid = bpf_get_current_uid_gid();
/*
@@ -383,7 +420,7 @@
* user at install time so we only check the appId part of a request uid at
* run time. See UserHandle#isSameApp for detail.
*/
- uint32_t appId = (gid_uid & 0xffffffff) % PER_USER_RANGE;
+ uint32_t appId = (gid_uid & 0xffffffff) % AID_USER_OFFSET; // == PER_USER_RANGE == 100000
uint8_t* permissions = bpf_uid_permission_map_lookup_elem(&appId);
if (!permissions) {
// UID not in map. Default to just INTERNET permission.
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index 92a774c..2ec0792 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -24,8 +24,8 @@
#define __kernel_udphdr udphdr
#include <linux/udp.h>
-// The resulting .o needs to load on the Android S bpfloader v0.2
-#define BPFLOADER_MIN_VER 2u
+// The resulting .o needs to load on the Android S bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
#include "bpf_helpers.h"
#include "bpf_net_helpers.h"
@@ -355,88 +355,10 @@
DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
-static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
- const bool downstream, const bool updatetime) {
- // Require ethernet dst mac address to be our unicast address.
- if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
-
- // Must be meta-ethernet IPv4 frame
- if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
-
- const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
-
- // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
- // not trigger and thus we need to manually make sure we can read packet headers via DPA.
- // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
- // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
- try_make_writable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
-
- void* data = (void*)(long)skb->data;
- const void* data_end = (void*)(long)skb->data_end;
- struct ethhdr* eth = is_ethernet ? data : NULL; // used iff is_ethernet
- struct iphdr* ip = is_ethernet ? (void*)(eth + 1) : data;
-
- // Must have (ethernet and) ipv4 header
- if (data + l2_header_size + sizeof(*ip) > data_end) return TC_ACT_PIPE;
-
- // Ethertype - if present - must be IPv4
- if (is_ethernet && (eth->h_proto != htons(ETH_P_IP))) return TC_ACT_PIPE;
-
- // IP version must be 4
- if (ip->version != 4) TC_PUNT(INVALID_IP_VERSION);
-
- // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
- if (ip->ihl != 5) TC_PUNT(HAS_IP_OPTIONS);
-
- // Calculate the IPv4 one's complement checksum of the IPv4 header.
- __wsum sum4 = 0;
- for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
- sum4 += ((__u16*)ip)[i];
- }
- // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
- sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse u32 into range 1 .. 0x1FFFE
- sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse any potential carry into u16
- // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF
- if (sum4 != 0xFFFF) TC_PUNT(CHECKSUM);
-
- // Minimum IPv4 total length is the size of the header
- if (ntohs(ip->tot_len) < sizeof(*ip)) TC_PUNT(TRUNCATED_IPV4);
-
- // We are incapable of dealing with IPv4 fragments
- if (ip->frag_off & ~htons(IP_DF)) TC_PUNT(IS_IP_FRAG);
-
- // Cannot decrement during forward if already zero or would be zero,
- // Let the kernel's stack handle these cases and generate appropriate ICMP errors.
- if (ip->ttl <= 1) TC_PUNT(LOW_TTL);
-
- // If we cannot update the 'last_used' field due to lack of bpf_ktime_get_boot_ns() helper,
- // then it is not safe to offload UDP due to the small conntrack timeouts, as such,
- // in such a situation we can only support TCP. This also has the added nice benefit of
- // using a separate error counter, and thus making it obvious which version of the program
- // is loaded.
- if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
-
- // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
- // but no need to check this if !updatetime due to check immediately above.
- if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
- TC_PUNT(NON_TCP_UDP);
-
- // We want to make sure that the compiler will, in the !updatetime case, entirely optimize
- // out all the non-tcp logic. Also note that at this point is_udp === !is_tcp.
- const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
-
- // This is a bit of a hack to make things easier on the bpf verifier.
- // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
- // what offsets into the packet are valid and can spuriously reject the program, this is
- // because it fails to realize that is_tcp && !is_tcp is impossible)
- //
- // For both TCP & UDP we'll need to read and modify the src/dst ports, which so happen to
- // always be in the first 4 bytes of the L4 header. Additionally for UDP we'll need access
- // to the checksum field which is in bytes 7 and 8. While for TCP we'll need to read the
- // TCP flags (at offset 13) and access to the checksum field (2 bytes at offset 16).
- // As such we *always* need access to at least 8 bytes.
- if (data + l2_header_size + sizeof(*ip) + 8 > data_end) TC_PUNT(SHORT_L4_HEADER);
-
+static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
+ const int l2_header_size, void* data, const void* data_end,
+ struct ethhdr* eth, struct iphdr* ip, const bool is_ethernet,
+ const bool downstream, const bool updatetime, const bool is_tcp) {
struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
@@ -625,6 +547,102 @@
return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
}
+static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
+ const bool downstream, const bool updatetime) {
+ // Require ethernet dst mac address to be our unicast address.
+ if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
+
+ // Must be meta-ethernet IPv4 frame
+ if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
+
+ const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+
+ // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
+ // not trigger and thus we need to manually make sure we can read packet headers via DPA.
+ // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
+ // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
+ try_make_writable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
+
+ void* data = (void*)(long)skb->data;
+ const void* data_end = (void*)(long)skb->data_end;
+ struct ethhdr* eth = is_ethernet ? data : NULL; // used iff is_ethernet
+ struct iphdr* ip = is_ethernet ? (void*)(eth + 1) : data;
+
+ // Must have (ethernet and) ipv4 header
+ if (data + l2_header_size + sizeof(*ip) > data_end) return TC_ACT_PIPE;
+
+ // Ethertype - if present - must be IPv4
+ if (is_ethernet && (eth->h_proto != htons(ETH_P_IP))) return TC_ACT_PIPE;
+
+ // IP version must be 4
+ if (ip->version != 4) TC_PUNT(INVALID_IP_VERSION);
+
+ // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+ if (ip->ihl != 5) TC_PUNT(HAS_IP_OPTIONS);
+
+ // Calculate the IPv4 one's complement checksum of the IPv4 header.
+ __wsum sum4 = 0;
+ for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
+ sum4 += ((__u16*)ip)[i];
+ }
+ // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
+ sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse u32 into range 1 .. 0x1FFFE
+ sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse any potential carry into u16
+ // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF
+ if (sum4 != 0xFFFF) TC_PUNT(CHECKSUM);
+
+ // Minimum IPv4 total length is the size of the header
+ if (ntohs(ip->tot_len) < sizeof(*ip)) TC_PUNT(TRUNCATED_IPV4);
+
+ // We are incapable of dealing with IPv4 fragments
+ if (ip->frag_off & ~htons(IP_DF)) TC_PUNT(IS_IP_FRAG);
+
+ // Cannot decrement during forward if already zero or would be zero,
+ // Let the kernel's stack handle these cases and generate appropriate ICMP errors.
+ if (ip->ttl <= 1) TC_PUNT(LOW_TTL);
+
+ // If we cannot update the 'last_used' field due to lack of bpf_ktime_get_boot_ns() helper,
+ // then it is not safe to offload UDP due to the small conntrack timeouts, as such,
+ // in such a situation we can only support TCP. This also has the added nice benefit of
+ // using a separate error counter, and thus making it obvious which version of the program
+ // is loaded.
+ if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
+
+ // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
+ // but no need to check this if !updatetime due to check immediately above.
+ if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
+ TC_PUNT(NON_TCP_UDP);
+
+ // We want to make sure that the compiler will, in the !updatetime case, entirely optimize
+ // out all the non-tcp logic. Also note that at this point is_udp === !is_tcp.
+ const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
+
+ // This is a bit of a hack to make things easier on the bpf verifier.
+ // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
+ // what offsets into the packet are valid and can spuriously reject the program, this is
+ // because it fails to realize that is_tcp && !is_tcp is impossible)
+ //
+ // For both TCP & UDP we'll need to read and modify the src/dst ports, which so happen to
+ // always be in the first 4 bytes of the L4 header. Additionally for UDP we'll need access
+ // to the checksum field which is in bytes 7 and 8. While for TCP we'll need to read the
+ // TCP flags (at offset 13) and access to the checksum field (2 bytes at offset 16).
+ // As such we *always* need access to at least 8 bytes.
+ if (data + l2_header_size + sizeof(*ip) + 8 > data_end) TC_PUNT(SHORT_L4_HEADER);
+
+ // We're forcing the compiler to emit two copies of the following code, optimized
+ // separately for is_tcp being true or false. This simplifies the resulting bpf
+ // byte code sufficiently that the 4.14 bpf verifier is able to keep track of things.
+ // Without this (updatetime == true) case would fail to bpf verify on 4.14 even
+ // if the underlying requisite kernel support (bpf_ktime_get_boot_ns) was backported.
+ if (is_tcp) {
+ return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
+ is_ethernet, downstream, updatetime, /* is_tcp */ true);
+ } else {
+ return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
+ is_ethernet, downstream, updatetime, /* is_tcp */ false);
+ }
+}
+
// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index c9c73f1..f2fcc8c 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -18,8 +18,8 @@
#include <linux/in.h>
#include <linux/ip.h>
-// The resulting .o needs to load on the Android S bpfloader v0.2
-#define BPFLOADER_MIN_VER 2u
+// The resulting .o needs to load on the Android S bpfloader
+#define BPFLOADER_MIN_VER BPFLOADER_S_VERSION
#include "bpf_helpers.h"
#include "bpf_net_helpers.h"
diff --git a/common/Android.bp b/common/Android.bp
new file mode 100644
index 0000000..729ef32
--- /dev/null
+++ b/common/Android.bp
@@ -0,0 +1,45 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "connectivity-net-module-utils-bpf",
+ srcs: [
+ "src/com/android/net/module/util/bpf/*.java",
+ ],
+ sdk_version: "module_current",
+ min_sdk_version: "29",
+ visibility: [
+ // Do not add any lib. This library is only shared inside connectivity module
+ // and its tests.
+ "//packages/modules/Connectivity:__subpackages__",
+ ],
+ libs: [
+ "androidx.annotation_annotation",
+ "framework-connectivity.stubs.module_lib",
+ ],
+ static_libs: [
+ "net-utils-device-common-struct",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ ],
+ lint: { strict_updatability_linting: true },
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatEgress4Key.java b/common/src/com/android/net/module/util/bpf/ClatEgress4Key.java
new file mode 100644
index 0000000..12200ec
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/ClatEgress4Key.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+
+/** Key type for clat egress IPv4 maps. */
+public class ClatEgress4Key extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long iif; // The input interface index
+
+ @Field(order = 1, type = Type.Ipv4Address)
+ public final Inet4Address local4; // The source IPv4 address
+
+ public ClatEgress4Key(final long iif, final Inet4Address local4) {
+ this.iif = iif;
+ this.local4 = local4;
+ }
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java b/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java
new file mode 100644
index 0000000..c10cb4d
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/ClatEgress4Value.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet6Address;
+
+/** Value type for clat egress IPv4 maps. */
+public class ClatEgress4Value extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long oif; // The output interface to redirect to
+
+ @Field(order = 1, type = Type.Ipv6Address)
+ public final Inet6Address local6; // The full 128-bits of the source IPv6 address
+
+ @Field(order = 2, type = Type.Ipv6Address)
+ public final Inet6Address pfx96; // The destination /96 nat64 prefix, bottom 32 bits must be 0
+
+ @Field(order = 3, type = Type.U8, padding = 3)
+ public final short oifIsEthernet; // Whether the output interface requires ethernet header
+
+ public ClatEgress4Value(final long oif, final Inet6Address local6, final Inet6Address pfx96,
+ final short oifIsEthernet) {
+ this.oif = oif;
+ this.local6 = local6;
+ this.pfx96 = pfx96;
+ this.oifIsEthernet = oifIsEthernet;
+ }
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatIngress6Key.java b/common/src/com/android/net/module/util/bpf/ClatIngress6Key.java
new file mode 100644
index 0000000..1e2f4e0
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/ClatIngress6Key.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet6Address;
+
+/** Key type for clat ingress IPv6 maps. */
+public class ClatIngress6Key extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long iif; // The input interface index
+
+ @Field(order = 1, type = Type.Ipv6Address)
+ public final Inet6Address pfx96; // The source /96 nat64 prefix, bottom 32 bits must be 0
+
+ @Field(order = 2, type = Type.Ipv6Address)
+ public final Inet6Address local6; // The full 128-bits of the destination IPv6 address
+
+ public ClatIngress6Key(final long iif, final Inet6Address pfx96, final Inet6Address local6) {
+ this.iif = iif;
+ this.pfx96 = pfx96;
+ this.local6 = local6;
+ }
+}
diff --git a/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java b/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java
new file mode 100644
index 0000000..bfec44f
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/ClatIngress6Value.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+
+/** Value type for clat ingress IPv6 maps. */
+public class ClatIngress6Value extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long oif; // The output interface to redirect to (0 means don't redirect)
+
+ @Field(order = 1, type = Type.Ipv4Address)
+ public final Inet4Address local4; // The destination IPv4 address
+
+ public ClatIngress6Value(final long oif, final Inet4Address local4) {
+ this.oif = oif;
+ this.local4 = local4;
+ }
+}
diff --git a/common/src/com/android/net/module/util/bpf/Tether4Key.java b/common/src/com/android/net/module/util/bpf/Tether4Key.java
new file mode 100644
index 0000000..638576f
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/Tether4Key.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** Key type for downstream & upstream IPv4 forwarding maps. */
+public class Tether4Key extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long iif;
+
+ @Field(order = 1, type = Type.EUI48)
+ public final MacAddress dstMac;
+
+ @Field(order = 2, type = Type.U8, padding = 1)
+ public final short l4proto;
+
+ @Field(order = 3, type = Type.ByteArray, arraysize = 4)
+ public final byte[] src4;
+
+ @Field(order = 4, type = Type.ByteArray, arraysize = 4)
+ public final byte[] dst4;
+
+ @Field(order = 5, type = Type.UBE16)
+ public final int srcPort;
+
+ @Field(order = 6, type = Type.UBE16)
+ public final int dstPort;
+
+ public Tether4Key(final long iif, @NonNull final MacAddress dstMac, final short l4proto,
+ final byte[] src4, final byte[] dst4, final int srcPort,
+ final int dstPort) {
+ Objects.requireNonNull(dstMac);
+
+ this.iif = iif;
+ this.dstMac = dstMac;
+ this.l4proto = l4proto;
+ this.src4 = src4;
+ this.dst4 = dst4;
+ this.srcPort = srcPort;
+ this.dstPort = dstPort;
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return String.format(
+ "iif: %d, dstMac: %s, l4proto: %d, src4: %s, dst4: %s, "
+ + "srcPort: %d, dstPort: %d",
+ iif, dstMac, l4proto,
+ Inet4Address.getByAddress(src4), Inet4Address.getByAddress(dst4),
+ Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort));
+ } catch (UnknownHostException | IllegalArgumentException e) {
+ return String.format("Invalid IP address", e);
+ }
+ }
+}
diff --git a/common/src/com/android/net/module/util/bpf/Tether4Value.java b/common/src/com/android/net/module/util/bpf/Tether4Value.java
new file mode 100644
index 0000000..de98766
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/Tether4Value.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** Value type for downstream & upstream IPv4 forwarding maps. */
+public class Tether4Value extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long oif;
+
+ // The ethhdr struct which is defined in uapi/linux/if_ether.h
+ @Field(order = 1, type = Type.EUI48)
+ public final MacAddress ethDstMac;
+ @Field(order = 2, type = Type.EUI48)
+ public final MacAddress ethSrcMac;
+ @Field(order = 3, type = Type.UBE16)
+ public final int ethProto; // Packet type ID field.
+
+ @Field(order = 4, type = Type.U16)
+ public final int pmtu;
+
+ @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+ public final byte[] src46;
+
+ @Field(order = 6, type = Type.ByteArray, arraysize = 16)
+ public final byte[] dst46;
+
+ @Field(order = 7, type = Type.UBE16)
+ public final int srcPort;
+
+ @Field(order = 8, type = Type.UBE16)
+ public final int dstPort;
+
+ // TODO: consider using U64.
+ @Field(order = 9, type = Type.U63)
+ public final long lastUsed;
+
+ public Tether4Value(final long oif, @NonNull final MacAddress ethDstMac,
+ @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu,
+ final byte[] src46, final byte[] dst46, final int srcPort,
+ final int dstPort, final long lastUsed) {
+ Objects.requireNonNull(ethDstMac);
+ Objects.requireNonNull(ethSrcMac);
+
+ this.oif = oif;
+ this.ethDstMac = ethDstMac;
+ this.ethSrcMac = ethSrcMac;
+ this.ethProto = ethProto;
+ this.pmtu = pmtu;
+ this.src46 = src46;
+ this.dst46 = dst46;
+ this.srcPort = srcPort;
+ this.dstPort = dstPort;
+ this.lastUsed = lastUsed;
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return String.format(
+ "oif: %d, ethDstMac: %s, ethSrcMac: %s, ethProto: %d, pmtu: %d, "
+ + "src46: %s, dst46: %s, srcPort: %d, dstPort: %d, "
+ + "lastUsed: %d",
+ oif, ethDstMac, ethSrcMac, ethProto, pmtu,
+ InetAddress.getByAddress(src46), InetAddress.getByAddress(dst46),
+ Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort),
+ lastUsed);
+ } catch (UnknownHostException | IllegalArgumentException e) {
+ return String.format("Invalid IP address", e);
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java b/common/src/com/android/net/module/util/bpf/TetherStatsKey.java
similarity index 96%
rename from Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
rename to common/src/com/android/net/module/util/bpf/TetherStatsKey.java
index 5442480..c6d595b 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
+++ b/common/src/com/android/net/module/util/bpf/TetherStatsKey.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.networkstack.tethering;
+package com.android.net.module.util.bpf;
import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.Field;
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java b/common/src/com/android/net/module/util/bpf/TetherStatsValue.java
similarity index 98%
rename from Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
rename to common/src/com/android/net/module/util/bpf/TetherStatsValue.java
index 844d2e8..028d217 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
+++ b/common/src/com/android/net/module/util/bpf/TetherStatsValue.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.networkstack.tethering;
+package com.android.net.module.util.bpf;
import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.Field;
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 1b47481..eb77288 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -3,7 +3,7 @@
public final class NetworkStats implements java.lang.AutoCloseable {
method public void close();
- method public boolean getNextBucket(android.app.usage.NetworkStats.Bucket);
+ method public boolean getNextBucket(@Nullable android.app.usage.NetworkStats.Bucket);
method public boolean hasNextBucket();
}
@@ -40,21 +40,21 @@
}
public class NetworkStatsManager {
- method @WorkerThread public android.app.usage.NetworkStats queryDetails(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
- method @WorkerThread public android.app.usage.NetworkStats queryDetailsForUid(int, String, long, long, int) throws java.lang.SecurityException;
- method @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTag(int, String, long, long, int, int) throws java.lang.SecurityException;
- method @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTagState(int, String, long, long, int, int, int) throws java.lang.SecurityException;
- method @WorkerThread public android.app.usage.NetworkStats querySummary(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
- method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForDevice(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
- method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForUser(int, String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
- method public void registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback);
- method public void registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, @Nullable android.os.Handler);
- method public void unregisterUsageCallback(android.app.usage.NetworkStatsManager.UsageCallback);
+ method @WorkerThread public android.app.usage.NetworkStats queryDetails(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+ method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUid(int, @Nullable String, long, long, int) throws java.lang.SecurityException;
+ method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTag(int, @Nullable String, long, long, int, int) throws java.lang.SecurityException;
+ method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForUidTagState(int, @Nullable String, long, long, int, int, int) throws java.lang.SecurityException;
+ method @WorkerThread public android.app.usage.NetworkStats querySummary(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+ method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForDevice(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+ method @WorkerThread public android.app.usage.NetworkStats.Bucket querySummaryForUser(int, @Nullable String, long, long) throws android.os.RemoteException, java.lang.SecurityException;
+ method public void registerUsageCallback(int, @Nullable String, long, @NonNull android.app.usage.NetworkStatsManager.UsageCallback);
+ method public void registerUsageCallback(int, @Nullable String, long, @NonNull android.app.usage.NetworkStatsManager.UsageCallback, @Nullable android.os.Handler);
+ method public void unregisterUsageCallback(@NonNull android.app.usage.NetworkStatsManager.UsageCallback);
}
public abstract static class NetworkStatsManager.UsageCallback {
ctor public NetworkStatsManager.UsageCallback();
- method public abstract void onThresholdReached(int, String);
+ method public abstract void onThresholdReached(int, @Nullable String);
}
}
@@ -173,12 +173,12 @@
method public static void incrementOperationCount(int, int);
method public static void setThreadStatsTag(int);
method public static void setThreadStatsUid(int);
- method public static void tagDatagramSocket(java.net.DatagramSocket) throws java.net.SocketException;
- method public static void tagFileDescriptor(java.io.FileDescriptor) throws java.io.IOException;
- method public static void tagSocket(java.net.Socket) throws java.net.SocketException;
- method public static void untagDatagramSocket(java.net.DatagramSocket) throws java.net.SocketException;
- method public static void untagFileDescriptor(java.io.FileDescriptor) throws java.io.IOException;
- method public static void untagSocket(java.net.Socket) throws java.net.SocketException;
+ method public static void tagDatagramSocket(@NonNull java.net.DatagramSocket) throws java.net.SocketException;
+ method public static void tagFileDescriptor(@NonNull java.io.FileDescriptor) throws java.io.IOException;
+ method public static void tagSocket(@NonNull java.net.Socket) throws java.net.SocketException;
+ method public static void untagDatagramSocket(@NonNull java.net.DatagramSocket) throws java.net.SocketException;
+ method public static void untagFileDescriptor(@NonNull java.io.FileDescriptor) throws java.io.IOException;
+ method public static void untagSocket(@NonNull java.net.Socket) throws java.net.SocketException;
field public static final int UNSUPPORTED = -1; // 0xffffffff
}
diff --git a/framework-t/api/lint-baseline.txt b/framework-t/api/lint-baseline.txt
index 53e1beb..2996a3e 100644
--- a/framework-t/api/lint-baseline.txt
+++ b/framework-t/api/lint-baseline.txt
@@ -41,86 +41,18 @@
android.net.IpSecTransform.Builder does not declare a `build()` method, but builder classes are expected to
-MissingNullability: android.app.usage.NetworkStats#getNextBucket(android.app.usage.NetworkStats.Bucket) parameter #0:
- Missing nullability on parameter `bucketOut` in method `getNextBucket`
MissingNullability: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
Missing nullability on method `queryDetails` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long) parameter #1:
- Missing nullability on parameter `subscriberId` in method `queryDetails`
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUid(int, String, long, long, int):
- Missing nullability on method `queryDetailsForUid` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUid(int, String, long, long, int) parameter #1:
- Missing nullability on parameter `subscriberId` in method `queryDetailsForUid`
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTag(int, String, long, long, int, int):
- Missing nullability on method `queryDetailsForUidTag` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTag(int, String, long, long, int, int) parameter #1:
- Missing nullability on parameter `subscriberId` in method `queryDetailsForUidTag`
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTagState(int, String, long, long, int, int, int):
- Missing nullability on method `queryDetailsForUidTagState` return
-MissingNullability: android.app.usage.NetworkStatsManager#queryDetailsForUidTagState(int, String, long, long, int, int, int) parameter #1:
- Missing nullability on parameter `subscriberId` in method `queryDetailsForUidTagState`
MissingNullability: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long):
Missing nullability on method `querySummary` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummary(int, String, long, long) parameter #1:
- Missing nullability on parameter `subscriberId` in method `querySummary`
MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long):
Missing nullability on method `querySummaryForDevice` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForDevice(int, String, long, long) parameter #1:
- Missing nullability on parameter `subscriberId` in method `querySummaryForDevice`
MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long):
Missing nullability on method `querySummaryForUser` return
-MissingNullability: android.app.usage.NetworkStatsManager#querySummaryForUser(int, String, long, long) parameter #1:
- Missing nullability on parameter `subscriberId` in method `querySummaryForUser`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback) parameter #1:
- Missing nullability on parameter `subscriberId` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback) parameter #3:
- Missing nullability on parameter `callback` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, android.os.Handler) parameter #1:
- Missing nullability on parameter `subscriberId` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#registerUsageCallback(int, String, long, android.app.usage.NetworkStatsManager.UsageCallback, android.os.Handler) parameter #3:
- Missing nullability on parameter `callback` in method `registerUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager#unregisterUsageCallback(android.app.usage.NetworkStatsManager.UsageCallback) parameter #0:
- Missing nullability on parameter `callback` in method `unregisterUsageCallback`
-MissingNullability: android.app.usage.NetworkStatsManager.UsageCallback#onThresholdReached(int, String) parameter #1:
- Missing nullability on parameter `subscriberId` in method `onThresholdReached`
MissingNullability: android.net.IpSecAlgorithm#writeToParcel(android.os.Parcel, int) parameter #0:
Missing nullability on parameter `out` in method `writeToParcel`
MissingNullability: android.net.IpSecManager.UdpEncapsulationSocket#getFileDescriptor():
Missing nullability on method `getFileDescriptor` return
-MissingNullability: android.net.TrafficStats#tagDatagramSocket(java.net.DatagramSocket) parameter #0:
- Missing nullability on parameter `socket` in method `tagDatagramSocket`
-MissingNullability: android.net.TrafficStats#tagFileDescriptor(java.io.FileDescriptor) parameter #0:
- Missing nullability on parameter `fd` in method `tagFileDescriptor`
-MissingNullability: android.net.TrafficStats#tagSocket(java.net.Socket) parameter #0:
- Missing nullability on parameter `socket` in method `tagSocket`
-MissingNullability: android.net.TrafficStats#untagDatagramSocket(java.net.DatagramSocket) parameter #0:
- Missing nullability on parameter `socket` in method `untagDatagramSocket`
-MissingNullability: android.net.TrafficStats#untagFileDescriptor(java.io.FileDescriptor) parameter #0:
- Missing nullability on parameter `fd` in method `untagFileDescriptor`
-MissingNullability: android.net.TrafficStats#untagSocket(java.net.Socket) parameter #0:
- Missing nullability on parameter `socket` in method `untagSocket`
-MissingNullability: com.android.internal.util.FileRotator#FileRotator(java.io.File, String, long, long) parameter #0:
- Missing nullability on parameter `basePath` in method `FileRotator`
-MissingNullability: com.android.internal.util.FileRotator#FileRotator(java.io.File, String, long, long) parameter #1:
- Missing nullability on parameter `prefix` in method `FileRotator`
-MissingNullability: com.android.internal.util.FileRotator#dumpAll(java.io.OutputStream) parameter #0:
- Missing nullability on parameter `os` in method `dumpAll`
-MissingNullability: com.android.internal.util.FileRotator#readMatching(com.android.internal.util.FileRotator.Reader, long, long) parameter #0:
- Missing nullability on parameter `reader` in method `readMatching`
-MissingNullability: com.android.internal.util.FileRotator#rewriteActive(com.android.internal.util.FileRotator.Rewriter, long) parameter #0:
- Missing nullability on parameter `rewriter` in method `rewriteActive`
-MissingNullability: com.android.internal.util.FileRotator#rewriteAll(com.android.internal.util.FileRotator.Rewriter) parameter #0:
- Missing nullability on parameter `rewriter` in method `rewriteAll`
-MissingNullability: com.android.internal.util.FileRotator.Reader#read(java.io.InputStream) parameter #0:
- Missing nullability on parameter `in` in method `read`
-MissingNullability: com.android.internal.util.FileRotator.Writer#write(java.io.OutputStream) parameter #0:
- Missing nullability on parameter `out` in method `write`
-MissingNullability: com.android.server.NetworkManagementSocketTagger#kernelToTag(String) parameter #0:
- Missing nullability on parameter `string` in method `kernelToTag`
-MissingNullability: com.android.server.NetworkManagementSocketTagger#tag(java.io.FileDescriptor) parameter #0:
- Missing nullability on parameter `fd` in method `tag`
-MissingNullability: com.android.server.NetworkManagementSocketTagger#untag(java.io.FileDescriptor) parameter #0:
- Missing nullability on parameter `fd` in method `untag`
RethrowRemoteException: android.app.usage.NetworkStatsManager#queryDetails(int, String, long, long):
diff --git a/framework-t/api/module-lib-current.txt b/framework-t/api/module-lib-current.txt
index 658c625..c1f7b39 100644
--- a/framework-t/api/module-lib-current.txt
+++ b/framework-t/api/module-lib-current.txt
@@ -4,6 +4,8 @@
public class NetworkStatsManager {
method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void forceUpdate();
method public static int getCollapsedRatType(int);
+ method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getMobileUidStats();
+ method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getWifiUidStats();
method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void noteUidForeground(int, boolean);
method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void notifyNetworkStatus(@NonNull java.util.List<android.net.Network>, @NonNull java.util.List<android.net.NetworkStateSnapshot>, @Nullable String, @NonNull java.util.List<android.net.UnderlyingNetworkInfo>);
method @NonNull @WorkerThread public android.app.usage.NetworkStats queryDetailsForDevice(@NonNull android.net.NetworkTemplate, long, long);
@@ -182,6 +184,7 @@
public class TrafficStats {
method public static void attachSocketTagger();
method public static void init(@NonNull android.content.Context);
+ method public static void setThreadStatsTagDownload();
}
public final class UnderlyingNetworkInfo implements android.os.Parcelable {
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 0f37b6f..6460fed 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -2,10 +2,8 @@
package android.app.usage {
public class NetworkStatsManager {
- method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getMobileUidStats();
- method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public android.net.NetworkStats getWifiUidStats();
- method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void registerNetworkStatsProvider(@NonNull String, @NonNull android.net.netstats.provider.NetworkStatsProvider);
- method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void unregisterNetworkStatsProvider(@NonNull android.net.netstats.provider.NetworkStatsProvider);
+ method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void registerNetworkStatsProvider(@NonNull String, @NonNull android.net.netstats.provider.NetworkStatsProvider);
+ method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STATS_PROVIDER, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void unregisterNetworkStatsProvider(@NonNull android.net.netstats.provider.NetworkStatsProvider);
}
}
@@ -113,7 +111,6 @@
public class TrafficStats {
method public static void setThreadStatsTagApp();
method public static void setThreadStatsTagBackup();
- method public static void setThreadStatsTagDownload();
method public static void setThreadStatsTagRestore();
field public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_END = -113; // 0xffffff8f
field public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_START = -128; // 0xffffff80
diff --git a/framework-t/src/android/app/usage/NetworkStats.java b/framework-t/src/android/app/usage/NetworkStats.java
index 2b6570a..74fe4bd 100644
--- a/framework-t/src/android/app/usage/NetworkStats.java
+++ b/framework-t/src/android/app/usage/NetworkStats.java
@@ -17,6 +17,7 @@
package android.app.usage;
import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.content.Context;
import android.net.INetworkStatsService;
import android.net.INetworkStatsSession;
@@ -474,10 +475,11 @@
/**
* Fills the recycled bucket with data of the next bin in the enumeration.
- * @param bucketOut Bucket to be filled with data.
+ * @param bucketOut Bucket to be filled with data. If null, the method does
+ * nothing and returning false.
* @return true if successfully filled the bucket, false otherwise.
*/
- public boolean getNextBucket(Bucket bucketOut) {
+ public boolean getNextBucket(@Nullable Bucket bucketOut) {
if (mSummary != null) {
return getNextSummaryBucket(bucketOut);
} else {
@@ -651,7 +653,7 @@
* @param bucketOut Next item will be set here.
* @return true if a next item could be set.
*/
- private boolean getNextSummaryBucket(Bucket bucketOut) {
+ private boolean getNextSummaryBucket(@Nullable Bucket bucketOut) {
if (bucketOut != null && mEnumerationIndex < mSummary.size()) {
mRecycledSummaryEntry = mSummary.getValues(mEnumerationIndex++, mRecycledSummaryEntry);
fillBucketFromSummaryEntry(bucketOut);
@@ -678,7 +680,7 @@
* @param bucketOut Next item will be set here.
* @return true if a next item could be set.
*/
- private boolean getNextHistoryBucket(Bucket bucketOut) {
+ private boolean getNextHistoryBucket(@Nullable Bucket bucketOut) {
if (bucketOut != null && mHistory != null) {
if (mEnumerationIndex < mHistory.size()) {
mRecycledHistoryEntry = mHistory.getValues(mEnumerationIndex++,
diff --git a/framework-t/src/android/app/usage/NetworkStatsManager.java b/framework-t/src/android/app/usage/NetworkStatsManager.java
index bf518b2..f41475b 100644
--- a/framework-t/src/android/app/usage/NetworkStatsManager.java
+++ b/framework-t/src/android/app/usage/NetworkStatsManager.java
@@ -290,7 +290,7 @@
* statistics collection.
*/
@WorkerThread
- public Bucket querySummaryForDevice(int networkType, String subscriberId,
+ public Bucket querySummaryForDevice(int networkType, @Nullable String subscriberId,
long startTime, long endTime) throws SecurityException, RemoteException {
NetworkTemplate template;
try {
@@ -335,8 +335,8 @@
* statistics collection.
*/
@WorkerThread
- public Bucket querySummaryForUser(int networkType, String subscriberId, long startTime,
- long endTime) throws SecurityException, RemoteException {
+ public Bucket querySummaryForUser(int networkType, @Nullable String subscriberId,
+ long startTime, long endTime) throws SecurityException, RemoteException {
NetworkTemplate template;
try {
template = createTemplate(networkType, subscriberId);
@@ -384,7 +384,7 @@
* statistics collection.
*/
@WorkerThread
- public NetworkStats querySummary(int networkType, String subscriberId, long startTime,
+ public NetworkStats querySummary(int networkType, @Nullable String subscriberId, long startTime,
long endTime) throws SecurityException, RemoteException {
NetworkTemplate template;
try {
@@ -508,15 +508,17 @@
*
* @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
*/
+ @NonNull
@WorkerThread
- public NetworkStats queryDetailsForUid(int networkType, String subscriberId,
+ public NetworkStats queryDetailsForUid(int networkType, @Nullable String subscriberId,
long startTime, long endTime, int uid) throws SecurityException {
return queryDetailsForUidTagState(networkType, subscriberId, startTime, endTime, uid,
NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL);
}
/** @hide */
- public NetworkStats queryDetailsForUid(NetworkTemplate template,
+ @NonNull
+ public NetworkStats queryDetailsForUid(@NonNull NetworkTemplate template,
long startTime, long endTime, int uid) throws SecurityException {
return queryDetailsForUidTagState(template, startTime, endTime, uid,
NetworkStats.Bucket.TAG_NONE, NetworkStats.Bucket.STATE_ALL);
@@ -524,23 +526,59 @@
/**
* Query network usage statistics details for a given uid and tag.
+ *
+ * This may take a long time, and apps should avoid calling this on their main thread.
+ * Only usable for uids belonging to calling user. Result is not aggregated over time.
+ * This means buckets' start and end timestamps are going to be between 'startTime' and
+ * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag
+ * the same as the 'tag' parameter, and the state the same as the 'state' parameter.
+ * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
+ * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
+ * interpolate across partial buckets. Since bucket length is in the order of hours, this
+ * method cannot be used to measure data usage on a fine grained time scale.
* This may take a long time, and apps should avoid calling this on their main thread.
*
- * @see #queryDetailsForUidTagState(int, String, long, long, int, int, int)
+ * @param networkType As defined in {@link ConnectivityManager}, e.g.
+ * {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
+ * etc.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * <p>Starting with API level 29, the {@code subscriberId} is guarded by
+ * additional restrictions. Calling apps that do not meet the new
+ * requirements to access the {@code subscriberId} can provide a {@code
+ * null} value when querying for the mobile network type to receive usage
+ * for all mobile networks. For additional details see {@link
+ * TelephonyManager#getSubscriberId()}.
+ * <p>Starting with API level 31, calling apps can provide a
+ * {@code subscriberId} with wifi network type to receive usage for
+ * wifi networks which is under the given subscription if applicable.
+ * Otherwise, pass {@code null} when querying all wifi networks.
+ * @param startTime Start of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param endTime End of period. Defined in terms of "Unix time", see
+ * {@link java.lang.System#currentTimeMillis}.
+ * @param uid UID of app
+ * @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for aggregated data
+ * across all the tags.
+ * @return Statistics which is described above.
+ * @throws SecurityException if permissions are insufficient to read network statistics.
*/
+ @NonNull
@WorkerThread
- public NetworkStats queryDetailsForUidTag(int networkType, String subscriberId,
+ public NetworkStats queryDetailsForUidTag(int networkType, @Nullable String subscriberId,
long startTime, long endTime, int uid, int tag) throws SecurityException {
return queryDetailsForUidTagState(networkType, subscriberId, startTime, endTime, uid,
tag, NetworkStats.Bucket.STATE_ALL);
}
/**
- * Query network usage statistics details for a given uid, tag, and state. Only usable for uids
- * belonging to calling user. Result is not aggregated over time. This means buckets' start and
- * end timestamps are going to be between 'startTime' and 'endTime' parameters. The uid is going
- * to be the same as the 'uid' parameter, the tag the same as the 'tag' parameter, and the state
- * the same as the 'state' parameter.
+ * Query network usage statistics details for a given uid, tag, and state.
+ *
+ * Only usable for uids belonging to calling user. Result is not aggregated over time.
+ * This means buckets' start and end timestamps are going to be between 'startTime' and
+ * 'endTime' parameters. The uid is going to be the same as the 'uid' parameter, the tag
+ * the same as the 'tag' parameter, and the state the same as the 'state' parameter.
* defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
* metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
* roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
@@ -572,11 +610,12 @@
* across all the tags.
* @param state state of interest. Use {@link NetworkStats.Bucket#STATE_ALL} to aggregate
* traffic from all states.
- * @return Statistics object or null if an error happened during statistics collection.
+ * @return Statistics which is described above.
* @throws SecurityException if permissions are insufficient to read network statistics.
*/
+ @NonNull
@WorkerThread
- public NetworkStats queryDetailsForUidTagState(int networkType, String subscriberId,
+ public NetworkStats queryDetailsForUidTagState(int networkType, @Nullable String subscriberId,
long startTime, long endTime, int uid, int tag, int state) throws SecurityException {
NetworkTemplate template;
template = createTemplate(networkType, subscriberId);
@@ -669,7 +708,7 @@
* statistics collection.
*/
@WorkerThread
- public NetworkStats queryDetails(int networkType, String subscriberId, long startTime,
+ public NetworkStats queryDetails(int networkType, @Nullable String subscriberId, long startTime,
long endTime) throws SecurityException, RemoteException {
NetworkTemplate template;
try {
@@ -698,7 +737,7 @@
*
* @hide
*/
- @SystemApi
+ @SystemApi(client = MODULE_LIBRARIES)
@RequiresPermission(anyOf = {
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
android.Manifest.permission.NETWORK_STACK})
@@ -724,7 +763,7 @@
*
* @hide
*/
- @SystemApi
+ @SystemApi(client = MODULE_LIBRARIES)
@RequiresPermission(anyOf = {
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
android.Manifest.permission.NETWORK_STACK})
@@ -785,10 +824,28 @@
/**
* Registers to receive notifications about data usage on specified networks.
*
- * @see #registerUsageCallback(int, String, long, UsageCallback, Handler)
+ * <p>The callbacks will continue to be called as long as the process is live or
+ * {@link #unregisterUsageCallback} is called.
+ *
+ * @param networkType Type of network to monitor. Either
+ {@link ConnectivityManager#TYPE_MOBILE} or {@link ConnectivityManager#TYPE_WIFI}.
+ * @param subscriberId If applicable, the subscriber id of the network interface.
+ * <p>Starting with API level 29, the {@code subscriberId} is guarded by
+ * additional restrictions. Calling apps that do not meet the new
+ * requirements to access the {@code subscriberId} can provide a {@code
+ * null} value when registering for the mobile network type to receive
+ * notifications for all mobile networks. For additional details see {@link
+ * TelephonyManager#getSubscriberId()}.
+ * <p>Starting with API level 31, calling apps can provide a
+ * {@code subscriberId} with wifi network type to receive usage for
+ * wifi networks which is under the given subscription if applicable.
+ * Otherwise, pass {@code null} when querying all wifi networks.
+ * @param thresholdBytes Threshold in bytes to be notified on.
+ * @param callback The {@link UsageCallback} that the system will call when data usage
+ * has exceeded the specified threshold.
*/
- public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes,
- UsageCallback callback) {
+ public void registerUsageCallback(int networkType, @Nullable String subscriberId,
+ long thresholdBytes, @NonNull UsageCallback callback) {
registerUsageCallback(networkType, subscriberId, thresholdBytes, callback,
null /* handler */);
}
@@ -818,8 +875,8 @@
* @param handler to dispatch callback events through, otherwise if {@code null} it uses
* the calling thread.
*/
- public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes,
- UsageCallback callback, @Nullable Handler handler) {
+ public void registerUsageCallback(int networkType, @Nullable String subscriberId,
+ long thresholdBytes, @NonNull UsageCallback callback, @Nullable Handler handler) {
NetworkTemplate template = createTemplate(networkType, subscriberId);
if (DBG) {
Log.d(TAG, "registerUsageCallback called with: {"
@@ -839,7 +896,7 @@
*
* @param callback The {@link UsageCallback} used when registering.
*/
- public void unregisterUsageCallback(UsageCallback callback) {
+ public void unregisterUsageCallback(@NonNull UsageCallback callback) {
if (callback == null || callback.request == null
|| callback.request.requestId == DataUsageRequest.REQUEST_ID_UNSET) {
throw new IllegalArgumentException("Invalid UsageCallback");
@@ -880,7 +937,7 @@
/**
* Called when data usage has reached the given threshold.
*/
- public abstract void onThresholdReached(int networkType, String subscriberId);
+ public abstract void onThresholdReached(int networkType, @Nullable String subscriberId);
/**
* @hide used for internal bookkeeping
@@ -924,7 +981,7 @@
@RequiresPermission(anyOf = {
android.Manifest.permission.NETWORK_STATS_PROVIDER,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
- @NonNull public void registerNetworkStatsProvider(
+ public void registerNetworkStatsProvider(
@NonNull String tag,
@NonNull NetworkStatsProvider provider) {
try {
@@ -950,7 +1007,7 @@
@RequiresPermission(anyOf = {
android.Manifest.permission.NETWORK_STATS_PROVIDER,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK})
- @NonNull public void unregisterNetworkStatsProvider(@NonNull NetworkStatsProvider provider) {
+ public void unregisterNetworkStatsProvider(@NonNull NetworkStatsProvider provider) {
try {
provider.getProviderCallbackBinderOrThrow().unregister();
} catch (RemoteException e) {
@@ -958,7 +1015,7 @@
}
}
- private static NetworkTemplate createTemplate(int networkType, String subscriberId) {
+ private static NetworkTemplate createTemplate(int networkType, @Nullable String subscriberId) {
final NetworkTemplate template;
switch (networkType) {
case ConnectivityManager.TYPE_MOBILE:
diff --git a/framework-t/src/android/net/EthernetManager.java b/framework-t/src/android/net/EthernetManager.java
index 2b76dd9..b8070f0 100644
--- a/framework-t/src/android/net/EthernetManager.java
+++ b/framework-t/src/android/net/EthernetManager.java
@@ -22,23 +22,21 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.RequiresFeature;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
-import android.content.pm.PackageManager;
import android.os.Build;
import android.os.OutcomeReceiver;
import android.os.RemoteException;
+import android.util.ArrayMap;
import com.android.internal.annotations.GuardedBy;
import com.android.modules.utils.BackgroundThread;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -56,37 +54,12 @@
private final IEthernetManager mService;
@GuardedBy("mListenerLock")
- private final ArrayList<ListenerInfo<InterfaceStateListener>> mIfaceListeners =
- new ArrayList<>();
+ private final ArrayMap<InterfaceStateListener, IEthernetServiceListener>
+ mIfaceServiceListeners = new ArrayMap<>();
@GuardedBy("mListenerLock")
- private final ArrayList<ListenerInfo<IntConsumer>> mEthernetStateListeners =
- new ArrayList<>();
+ private final ArrayMap<IntConsumer, IEthernetServiceListener> mStateServiceListeners =
+ new ArrayMap<>();
final Object mListenerLock = new Object();
- private final IEthernetServiceListener.Stub mServiceListener =
- new IEthernetServiceListener.Stub() {
- @Override
- public void onEthernetStateChanged(int state) {
- synchronized (mListenerLock) {
- for (ListenerInfo<IntConsumer> li : mEthernetStateListeners) {
- li.executor.execute(() -> {
- li.listener.accept(state);
- });
- }
- }
- }
-
- @Override
- public void onInterfaceStateChanged(String iface, int state, int role,
- IpConfiguration configuration) {
- synchronized (mListenerLock) {
- for (ListenerInfo<InterfaceStateListener> li : mIfaceListeners) {
- li.executor.execute(() ->
- li.listener.onInterfaceStateChanged(iface, state, role,
- configuration));
- }
- }
- }
- };
/**
* Indicates that Ethernet is disabled.
@@ -104,18 +77,6 @@
@SystemApi(client = MODULE_LIBRARIES)
public static final int ETHERNET_STATE_ENABLED = 1;
- private static class ListenerInfo<T> {
- @NonNull
- public final Executor executor;
- @NonNull
- public final T listener;
-
- private ListenerInfo(@NonNull Executor executor, @NonNull T listener) {
- this.executor = executor;
- this.listener = listener;
- }
- }
-
/**
* The interface is absent.
* @hide
@@ -323,18 +284,28 @@
if (listener == null || executor == null) {
throw new NullPointerException("listener and executor must not be null");
}
+
+ final IEthernetServiceListener.Stub serviceListener = new IEthernetServiceListener.Stub() {
+ @Override
+ public void onEthernetStateChanged(int state) {}
+
+ @Override
+ public void onInterfaceStateChanged(String iface, int state, int role,
+ IpConfiguration configuration) {
+ executor.execute(() ->
+ listener.onInterfaceStateChanged(iface, state, role, configuration));
+ }
+ };
synchronized (mListenerLock) {
- maybeAddServiceListener();
- mIfaceListeners.add(new ListenerInfo<InterfaceStateListener>(executor, listener));
+ addServiceListener(serviceListener);
+ mIfaceServiceListeners.put(listener, serviceListener);
}
}
@GuardedBy("mListenerLock")
- private void maybeAddServiceListener() {
- if (!mIfaceListeners.isEmpty() || !mEthernetStateListeners.isEmpty()) return;
-
+ private void addServiceListener(@NonNull final IEthernetServiceListener listener) {
try {
- mService.addListener(mServiceListener);
+ mService.addListener(listener);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -364,17 +335,16 @@
public void removeInterfaceStateListener(@NonNull InterfaceStateListener listener) {
Objects.requireNonNull(listener);
synchronized (mListenerLock) {
- mIfaceListeners.removeIf(l -> l.listener == listener);
- maybeRemoveServiceListener();
+ maybeRemoveServiceListener(mIfaceServiceListeners.remove(listener));
}
}
@GuardedBy("mListenerLock")
- private void maybeRemoveServiceListener() {
- if (!mIfaceListeners.isEmpty() || !mEthernetStateListeners.isEmpty()) return;
+ private void maybeRemoveServiceListener(@Nullable final IEthernetServiceListener listener) {
+ if (listener == null) return;
try {
- mService.removeListener(mServiceListener);
+ mService.removeListener(listener);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -601,7 +571,6 @@
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
android.Manifest.permission.NETWORK_STACK,
android.Manifest.permission.MANAGE_ETHERNET_NETWORKS})
- @RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
public void enableInterface(
@NonNull String iface,
@Nullable @CallbackExecutor Executor executor,
@@ -610,7 +579,7 @@
final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver(
executor, callback);
try {
- mService.connectNetwork(iface, proxy);
+ mService.enableInterface(iface, proxy);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -638,7 +607,6 @@
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
android.Manifest.permission.NETWORK_STACK,
android.Manifest.permission.MANAGE_ETHERNET_NETWORKS})
- @RequiresFeature(PackageManager.FEATURE_AUTOMOTIVE)
public void disableInterface(
@NonNull String iface,
@Nullable @CallbackExecutor Executor executor,
@@ -647,7 +615,7 @@
final NetworkInterfaceOutcomeReceiver proxy = makeNetworkInterfaceOutcomeReceiver(
executor, callback);
try {
- mService.disconnectNetwork(iface, proxy);
+ mService.disableInterface(iface, proxy);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -687,9 +655,19 @@
@NonNull IntConsumer listener) {
Objects.requireNonNull(executor);
Objects.requireNonNull(listener);
+ final IEthernetServiceListener.Stub serviceListener = new IEthernetServiceListener.Stub() {
+ @Override
+ public void onEthernetStateChanged(int state) {
+ executor.execute(() -> listener.accept(state));
+ }
+
+ @Override
+ public void onInterfaceStateChanged(String iface, int state, int role,
+ IpConfiguration configuration) {}
+ };
synchronized (mListenerLock) {
- maybeAddServiceListener();
- mEthernetStateListeners.add(new ListenerInfo<IntConsumer>(executor, listener));
+ addServiceListener(serviceListener);
+ mStateServiceListeners.put(listener, serviceListener);
}
}
@@ -705,8 +683,7 @@
public void removeEthernetStateListener(@NonNull IntConsumer listener) {
Objects.requireNonNull(listener);
synchronized (mListenerLock) {
- mEthernetStateListeners.removeIf(l -> l.listener == listener);
- maybeRemoveServiceListener();
+ maybeRemoveServiceListener(mStateServiceListeners.remove(listener));
}
}
diff --git a/framework-t/src/android/net/IEthernetManager.aidl b/framework-t/src/android/net/IEthernetManager.aidl
index 42e4c1a..c1efc29 100644
--- a/framework-t/src/android/net/IEthernetManager.aidl
+++ b/framework-t/src/android/net/IEthernetManager.aidl
@@ -43,8 +43,8 @@
void releaseTetheredInterface(in ITetheredInterfaceCallback callback);
void updateConfiguration(String iface, in EthernetNetworkUpdateRequest request,
in INetworkInterfaceOutcomeReceiver listener);
- void connectNetwork(String iface, in INetworkInterfaceOutcomeReceiver listener);
- void disconnectNetwork(String iface, in INetworkInterfaceOutcomeReceiver listener);
+ void enableInterface(String iface, in INetworkInterfaceOutcomeReceiver listener);
+ void disableInterface(String iface, in INetworkInterfaceOutcomeReceiver listener);
void setEthernetEnabled(boolean enabled);
List<String> getInterfaceList();
}
diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java
index 9cb0947..9cceac2 100644
--- a/framework-t/src/android/net/IpSecManager.java
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -817,10 +817,10 @@
* </ol>
*
* @param underlyingNetwork the new {@link Network} that will carry traffic for this tunnel.
- * This network MUST never be the network exposing this IpSecTunnelInterface, otherwise
- * this method will throw an {@link IllegalArgumentException}. If the
- * IpSecTunnelInterface is later added to this network, all outbound traffic will be
- * blackholed.
+ * This network MUST be a functional {@link Network} with valid {@link LinkProperties},
+ * and MUST never be the network exposing this IpSecTunnelInterface, otherwise this
+ * method will throw an {@link IllegalArgumentException}. If the IpSecTunnelInterface is
+ * later added to this network, all outbound traffic will be blackholed.
*/
// TODO: b/169171001 Update the documentation when transform migration is supported.
// The purpose of making updating network and applying transforms separate is to leave open
@@ -962,7 +962,6 @@
* IP header and IPsec Header on all inbound traffic).
* <p>Applications should probably not use this API directly.
*
- *
* @param tunnel The {@link IpSecManager#IpSecTunnelInterface} that will use the supplied
* transform.
* @param direction the direction, {@link DIRECTION_OUT} or {@link #DIRECTION_IN} in which
diff --git a/framework-t/src/android/net/NetworkIdentitySet.java b/framework-t/src/android/net/NetworkIdentitySet.java
index ad3a958..d88408e 100644
--- a/framework-t/src/android/net/NetworkIdentitySet.java
+++ b/framework-t/src/android/net/NetworkIdentitySet.java
@@ -206,6 +206,7 @@
public static int compare(@NonNull NetworkIdentitySet left, @NonNull NetworkIdentitySet right) {
Objects.requireNonNull(left);
Objects.requireNonNull(right);
+ if (left.isEmpty() && right.isEmpty()) return 0;
if (left.isEmpty()) return -1;
if (right.isEmpty()) return 1;
diff --git a/framework-t/src/android/net/NetworkStats.java b/framework-t/src/android/net/NetworkStats.java
index 06f2a62..0bb98f8 100644
--- a/framework-t/src/android/net/NetworkStats.java
+++ b/framework-t/src/android/net/NetworkStats.java
@@ -124,7 +124,6 @@
public @Nullable static final String[] INTERFACES_ALL = null;
/** {@link #tag} value for total data across all tags. */
- // TODO: Rename TAG_NONE to TAG_ALL.
public static final int TAG_NONE = 0;
/** {@link #metered} value to account for all metered states. */
@@ -412,21 +411,24 @@
/**
* @return the metered state.
*/
- @Meteredness public int getMetered() {
+ @Meteredness
+ public int getMetered() {
return metered;
}
/**
* @return the roaming state.
*/
- @Roaming public int getRoaming() {
+ @Roaming
+ public int getRoaming() {
return roaming;
}
/**
* @return the default network state.
*/
- @DefaultNetwork public int getDefaultNetwork() {
+ @DefaultNetwork
+ public int getDefaultNetwork() {
return defaultNetwork;
}
@@ -1298,6 +1300,17 @@
}
/**
+ * Removes the interface name from all entries.
+ * This mutates the original structure in place.
+ * @hide
+ */
+ public void clearInterfaces() {
+ for (int i = 0; i < size; i++) {
+ iface[i] = null;
+ }
+ }
+
+ /**
* Only keep entries that match all specified filters.
*
* <p>This mutates the original structure in place. After this method is called,
diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
index e385b33..6a1d2dd 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -694,6 +694,26 @@
}
}
+ /**
+ * Remove histories which contains or is before the cutoff timestamp.
+ * @hide
+ */
+ public void removeHistoryBefore(long cutoffMillis) {
+ final ArrayList<Key> knownKeys = new ArrayList<>();
+ knownKeys.addAll(mStats.keySet());
+
+ for (Key key : knownKeys) {
+ final NetworkStatsHistory history = mStats.get(key);
+ if (history.getStart() > cutoffMillis) continue;
+
+ history.removeBucketsStartingBefore(cutoffMillis);
+ if (history.size() == 0) {
+ mStats.remove(key);
+ }
+ mDirty = true;
+ }
+ }
+
private void noteRecordedHistory(long startMillis, long endMillis, long totalBytes) {
if (startMillis < mStartMillis) mStartMillis = startMillis;
if (endMillis > mEndMillis) mEndMillis = endMillis;
@@ -776,7 +796,7 @@
if (!templateMatches(groupTemplate, key.ident)) continue;
if (key.set >= NetworkStats.SET_DEBUG_START) continue;
- final Key groupKey = new Key(null, key.uid, key.set, key.tag);
+ final Key groupKey = new Key(new NetworkIdentitySet(), key.uid, key.set, key.tag);
NetworkStatsHistory groupHistory = grouped.get(groupKey);
if (groupHistory == null) {
groupHistory = new NetworkStatsHistory(value.getBucketDuration());
@@ -845,6 +865,9 @@
* Add association of the history with the specified key in this map.
*
* @param key The object used to identify a network, see {@link Key}.
+ * If history already exists for this key, then the passed-in history is appended
+ * to the previously-passed in history. The caller must ensure that the history
+ * passed-in timestamps are greater than all previously-passed-in timestamps.
* @param history {@link NetworkStatsHistory} instance associated to the given {@link Key}.
* @return The builder object.
*/
@@ -854,9 +877,21 @@
Objects.requireNonNull(key);
Objects.requireNonNull(history);
final List<Entry> historyEntries = history.getEntries();
+ final NetworkStatsHistory existing = mEntries.get(key);
+ final int size = historyEntries.size() + ((existing != null) ? existing.size() : 0);
final NetworkStatsHistory.Builder historyBuilder =
- new NetworkStatsHistory.Builder(mBucketDurationMillis, historyEntries.size());
+ new NetworkStatsHistory.Builder(mBucketDurationMillis, size);
+
+ // TODO: this simply appends the entries to any entries that were already present in
+ // the builder, which requires the caller to pass in entries in order. We might be
+ // able to do better with something like recordHistory.
+ if (existing != null) {
+ for (Entry entry : existing.getEntries()) {
+ historyBuilder.addEntry(entry);
+ }
+ }
+
for (Entry entry : historyEntries) {
historyBuilder.addEntry(entry);
}
diff --git a/framework-t/src/android/net/NetworkStatsHistory.java b/framework-t/src/android/net/NetworkStatsHistory.java
index 301fef9..738e9cc 100644
--- a/framework-t/src/android/net/NetworkStatsHistory.java
+++ b/framework-t/src/android/net/NetworkStatsHistory.java
@@ -32,6 +32,7 @@
import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
@@ -57,6 +58,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Random;
+import java.util.TreeMap;
/**
* Collection of historical network statistics, recorded into equally-sized
@@ -252,6 +254,28 @@
+ ", operations=" + operations
+ "}";
}
+
+ /**
+ * Add the given {@link Entry} with this instance and return a new {@link Entry}
+ * instance as the result.
+ *
+ * @hide
+ */
+ @NonNull
+ public Entry plus(@NonNull Entry another, long bucketDuration) {
+ if (this.bucketStart != another.bucketStart) {
+ throw new IllegalArgumentException("bucketStart " + this.bucketStart
+ + " is not equal to " + another.bucketStart);
+ }
+ return new Entry(this.bucketStart,
+ // Active time should not go over bucket duration.
+ Math.min(this.activeTime + another.activeTime, bucketDuration),
+ this.rxBytes + another.rxBytes,
+ this.rxPackets + another.rxPackets,
+ this.txBytes + another.txBytes,
+ this.txPackets + another.txPackets,
+ this.operations + another.operations);
+ }
}
/** @hide */
@@ -680,19 +704,21 @@
}
/**
- * Remove buckets older than requested cutoff.
+ * Remove buckets that start older than requested cutoff.
+ *
+ * This method will remove any bucket that contains any data older than the requested
+ * cutoff, even if that same bucket includes some data from after the cutoff.
+ *
* @hide
*/
- public void removeBucketsBefore(long cutoff) {
+ public void removeBucketsStartingBefore(final long cutoff) {
// TODO: Consider use getIndexBefore.
int i;
for (i = 0; i < bucketCount; i++) {
final long curStart = bucketStart[i];
- final long curEnd = curStart + bucketDuration;
- // cutoff happens before or during this bucket; everything before
- // this bucket should be removed.
- if (curEnd > cutoff) break;
+ // This bucket starts after or at the cutoff, so it should be kept.
+ if (curStart >= cutoff) break;
}
if (i > 0) {
@@ -947,6 +973,25 @@
return writer.toString();
}
+ /**
+ * Same as "equals", but not actually called equals as this would affect public API behavior.
+ * @hide
+ */
+ @Nullable
+ public boolean isSameAs(NetworkStatsHistory other) {
+ return bucketCount == other.bucketCount
+ && Arrays.equals(bucketStart, other.bucketStart)
+ // Don't check activeTime since it can change on import due to the importer using
+ // recordHistory. It's also not exposed by the APIs or present in dumpsys or
+ // toString().
+ && Arrays.equals(rxBytes, other.rxBytes)
+ && Arrays.equals(rxPackets, other.rxPackets)
+ && Arrays.equals(txBytes, other.txBytes)
+ && Arrays.equals(txPackets, other.txPackets)
+ && Arrays.equals(operations, other.operations)
+ && totalBytes == other.totalBytes;
+ }
+
@UnsupportedAppUsage
public static final @android.annotation.NonNull Creator<NetworkStatsHistory> CREATOR = new Creator<NetworkStatsHistory>() {
@Override
@@ -1087,14 +1132,8 @@
* Builder class for {@link NetworkStatsHistory}.
*/
public static final class Builder {
+ private final TreeMap<Long, Entry> mEntries;
private final long mBucketDuration;
- private final List<Long> mBucketStart;
- private final List<Long> mActiveTime;
- private final List<Long> mRxBytes;
- private final List<Long> mRxPackets;
- private final List<Long> mTxBytes;
- private final List<Long> mTxPackets;
- private final List<Long> mOperations;
/**
* Creates a new Builder with given bucket duration and initial capacity to construct
@@ -1105,36 +1144,31 @@
*/
public Builder(long bucketDuration, int initialCapacity) {
mBucketDuration = bucketDuration;
- mBucketStart = new ArrayList<>(initialCapacity);
- mActiveTime = new ArrayList<>(initialCapacity);
- mRxBytes = new ArrayList<>(initialCapacity);
- mRxPackets = new ArrayList<>(initialCapacity);
- mTxBytes = new ArrayList<>(initialCapacity);
- mTxPackets = new ArrayList<>(initialCapacity);
- mOperations = new ArrayList<>(initialCapacity);
+ // Create a collection that is always sorted and can deduplicate items by the timestamp.
+ mEntries = new TreeMap<>();
}
/**
- * Add an {@link Entry} into the {@link NetworkStatsHistory} instance.
+ * Add an {@link Entry} into the {@link NetworkStatsHistory} instance. If the timestamp
+ * already exists, the given {@link Entry} will be combined into existing entry.
*
* @param entry The target {@link Entry} object.
* @return The builder object.
*/
@NonNull
public Builder addEntry(@NonNull Entry entry) {
- mBucketStart.add(entry.bucketStart);
- mActiveTime.add(entry.activeTime);
- mRxBytes.add(entry.rxBytes);
- mRxPackets.add(entry.rxPackets);
- mTxBytes.add(entry.txBytes);
- mTxPackets.add(entry.txPackets);
- mOperations.add(entry.operations);
+ final Entry existing = mEntries.get(entry.bucketStart);
+ if (existing != null) {
+ mEntries.put(entry.bucketStart, existing.plus(entry, mBucketDuration));
+ } else {
+ mEntries.put(entry.bucketStart, entry);
+ }
return this;
}
- private static long sum(@NonNull List<Long> list) {
- long sum = 0;
- for (long entry : list) {
+ private static long sum(@NonNull long[] array) {
+ long sum = 0L;
+ for (long entry : array) {
sum += entry;
}
return sum;
@@ -1147,16 +1181,30 @@
*/
@NonNull
public NetworkStatsHistory build() {
- return new NetworkStatsHistory(mBucketDuration,
- CollectionUtils.toLongArray(mBucketStart),
- CollectionUtils.toLongArray(mActiveTime),
- CollectionUtils.toLongArray(mRxBytes),
- CollectionUtils.toLongArray(mRxPackets),
- CollectionUtils.toLongArray(mTxBytes),
- CollectionUtils.toLongArray(mTxPackets),
- CollectionUtils.toLongArray(mOperations),
- mBucketStart.size(),
- sum(mRxBytes) + sum(mTxBytes));
+ int size = mEntries.size();
+ final long[] bucketStart = new long[size];
+ final long[] activeTime = new long[size];
+ final long[] rxBytes = new long[size];
+ final long[] rxPackets = new long[size];
+ final long[] txBytes = new long[size];
+ final long[] txPackets = new long[size];
+ final long[] operations = new long[size];
+
+ int i = 0;
+ for (Entry entry : mEntries.values()) {
+ bucketStart[i] = entry.bucketStart;
+ activeTime[i] = entry.activeTime;
+ rxBytes[i] = entry.rxBytes;
+ rxPackets[i] = entry.rxPackets;
+ txBytes[i] = entry.txBytes;
+ txPackets[i] = entry.txPackets;
+ operations[i] = entry.operations;
+ i++;
+ }
+
+ return new NetworkStatsHistory(mBucketDuration, bucketStart, activeTime,
+ rxBytes, rxPackets, txBytes, txPackets, operations,
+ size, sum(rxBytes) + sum(txBytes));
}
}
}
diff --git a/framework-t/src/android/net/NetworkTemplate.java b/framework-t/src/android/net/NetworkTemplate.java
index 7b5afd7..b82a126 100644
--- a/framework-t/src/android/net/NetworkTemplate.java
+++ b/framework-t/src/android/net/NetworkTemplate.java
@@ -393,8 +393,9 @@
//constructor passes METERED_YES for these types.
this(matchRule, subscriberId, matchSubscriberIds,
wifiNetworkKey != null ? new String[] { wifiNetworkKey } : new String[0],
- (matchRule == MATCH_MOBILE || matchRule == MATCH_MOBILE_WILDCARD) ? METERED_YES
- : METERED_ALL , ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ (matchRule == MATCH_MOBILE || matchRule == MATCH_MOBILE_WILDCARD
+ || matchRule == MATCH_CARRIER) ? METERED_YES : METERED_ALL,
+ ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
OEM_MANAGED_ALL, NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT);
}
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index bc836d8..dc4ac55 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -205,7 +205,7 @@
* server context.
* @hide
*/
- @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @SystemApi(client = MODULE_LIBRARIES)
@SuppressLint("VisiblySynchronized")
public static synchronized void init(@NonNull final Context context) {
if (sStatsService != null) {
@@ -376,7 +376,7 @@
*
* @hide
*/
- @SystemApi
+ @SystemApi(client = MODULE_LIBRARIES)
public static void setThreadStatsTagDownload() {
setThreadStatsTag(TAG_SYSTEM_DOWNLOAD);
}
@@ -468,7 +468,7 @@
*
* @see #setThreadStatsTag(int)
*/
- public static void tagSocket(Socket socket) throws SocketException {
+ public static void tagSocket(@NonNull Socket socket) throws SocketException {
SocketTagger.get().tag(socket);
}
@@ -483,7 +483,7 @@
* calling {@code untagSocket()} before sending the socket to another
* process.
*/
- public static void untagSocket(Socket socket) throws SocketException {
+ public static void untagSocket(@NonNull Socket socket) throws SocketException {
SocketTagger.get().untag(socket);
}
@@ -496,14 +496,14 @@
*
* @see #setThreadStatsTag(int)
*/
- public static void tagDatagramSocket(DatagramSocket socket) throws SocketException {
+ public static void tagDatagramSocket(@NonNull DatagramSocket socket) throws SocketException {
SocketTagger.get().tag(socket);
}
/**
* Remove any statistics parameters from the given {@link DatagramSocket}.
*/
- public static void untagDatagramSocket(DatagramSocket socket) throws SocketException {
+ public static void untagDatagramSocket(@NonNull DatagramSocket socket) throws SocketException {
SocketTagger.get().untag(socket);
}
@@ -516,7 +516,7 @@
*
* @see #setThreadStatsTag(int)
*/
- public static void tagFileDescriptor(FileDescriptor fd) throws IOException {
+ public static void tagFileDescriptor(@NonNull FileDescriptor fd) throws IOException {
SocketTagger.get().tag(fd);
}
@@ -524,7 +524,7 @@
* Remove any statistics parameters from the given {@link FileDescriptor}
* socket.
*/
- public static void untagFileDescriptor(FileDescriptor fd) throws IOException {
+ public static void untagFileDescriptor(@NonNull FileDescriptor fd) throws IOException {
SocketTagger.get().untag(fd);
}
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 33b44c8..fad63e5 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -175,6 +175,7 @@
*
* @see #ACTION_NSD_STATE_CHANGED
*/
+ // TODO: Deprecate this since NSD service is never disabled.
public static final int NSD_STATE_DISABLED = 1;
/**
@@ -230,17 +231,12 @@
public static final int DAEMON_STARTUP = 19;
/** @hide */
- public static final int ENABLE = 20;
- /** @hide */
- public static final int DISABLE = 21;
+ public static final int MDNS_SERVICE_EVENT = 20;
/** @hide */
- public static final int MDNS_SERVICE_EVENT = 22;
-
+ public static final int REGISTER_CLIENT = 21;
/** @hide */
- public static final int REGISTER_CLIENT = 23;
- /** @hide */
- public static final int UNREGISTER_CLIENT = 24;
+ public static final int UNREGISTER_CLIENT = 22;
/** Dns based service discovery protocol */
public static final int PROTOCOL_DNS_SD = 0x0001;
@@ -266,8 +262,6 @@
EVENT_NAMES.put(RESOLVE_SERVICE_SUCCEEDED, "RESOLVE_SERVICE_SUCCEEDED");
EVENT_NAMES.put(DAEMON_CLEANUP, "DAEMON_CLEANUP");
EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP");
- EVENT_NAMES.put(ENABLE, "ENABLE");
- EVENT_NAMES.put(DISABLE, "DISABLE");
EVENT_NAMES.put(MDNS_SERVICE_EVENT, "MDNS_SERVICE_EVENT");
}
@@ -312,9 +306,12 @@
@Override
public void onAvailable(@NonNull Network network) {
final DelegatingDiscoveryListener wrappedListener = new DelegatingDiscoveryListener(
- network, mBaseListener);
+ network, mBaseListener, mBaseExecutor);
mPerNetworkListeners.put(network, wrappedListener);
- discoverServices(mServiceType, mProtocolType, network, mBaseExecutor,
+ // Run discovery callbacks inline on the service handler thread, which is the
+ // same thread used by this NetworkCallback, but DelegatingDiscoveryListener will
+ // use the base executor to run the wrapped callbacks.
+ discoverServices(mServiceType, mProtocolType, network, Runnable::run,
wrappedListener);
}
@@ -334,7 +331,8 @@
public void start(@NonNull NetworkRequest request) {
final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
cm.registerNetworkCallback(request, mNetworkCb, mHandler);
- mHandler.post(() -> mBaseListener.onDiscoveryStarted(mServiceType));
+ mHandler.post(() -> mBaseExecutor.execute(() ->
+ mBaseListener.onDiscoveryStarted(mServiceType)));
}
/**
@@ -351,7 +349,7 @@
final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
cm.unregisterNetworkCallback(mNetworkCb);
if (mPerNetworkListeners.size() == 0) {
- mBaseListener.onDiscoveryStopped(mServiceType);
+ mBaseExecutor.execute(() -> mBaseListener.onDiscoveryStopped(mServiceType));
return;
}
for (int i = 0; i < mPerNetworkListeners.size(); i++) {
@@ -399,14 +397,23 @@
}
}
+ /**
+ * A listener wrapping calls to an app-provided listener, while keeping track of found
+ * services, so they can all be reported lost when the underlying network is lost.
+ *
+ * This should be registered to run on the service handler.
+ */
private class DelegatingDiscoveryListener implements DiscoveryListener {
private final Network mNetwork;
private final DiscoveryListener mWrapped;
+ private final Executor mWrappedExecutor;
private final ArraySet<TrackedNsdInfo> mFoundInfo = new ArraySet<>();
- private DelegatingDiscoveryListener(Network network, DiscoveryListener listener) {
+ private DelegatingDiscoveryListener(Network network, DiscoveryListener listener,
+ Executor executor) {
mNetwork = network;
mWrapped = listener;
+ mWrappedExecutor = executor;
}
void notifyAllServicesLost() {
@@ -415,7 +422,7 @@
final NsdServiceInfo serviceInfo = new NsdServiceInfo(
trackedInfo.mServiceName, trackedInfo.mServiceType);
serviceInfo.setNetwork(mNetwork);
- mWrapped.onServiceLost(serviceInfo);
+ mWrappedExecutor.execute(() -> mWrapped.onServiceLost(serviceInfo));
}
}
@@ -444,7 +451,7 @@
// Do not report onStopDiscoveryFailed when some underlying listeners failed:
// this does not mean that all listeners did, and onStopDiscoveryFailed is not
// actionable anyway. Just report that discovery stopped.
- mWrapped.onDiscoveryStopped(serviceType);
+ mWrappedExecutor.execute(() -> mWrapped.onDiscoveryStopped(serviceType));
}
}
@@ -452,20 +459,20 @@
public void onDiscoveryStopped(String serviceType) {
mPerNetworkListeners.remove(mNetwork);
if (mStopRequested && mPerNetworkListeners.size() == 0) {
- mWrapped.onDiscoveryStopped(serviceType);
+ mWrappedExecutor.execute(() -> mWrapped.onDiscoveryStopped(serviceType));
}
}
@Override
public void onServiceFound(NsdServiceInfo serviceInfo) {
mFoundInfo.add(new TrackedNsdInfo(serviceInfo));
- mWrapped.onServiceFound(serviceInfo);
+ mWrappedExecutor.execute(() -> mWrapped.onServiceFound(serviceInfo));
}
@Override
public void onServiceLost(NsdServiceInfo serviceInfo) {
mFoundInfo.remove(new TrackedNsdInfo(serviceInfo));
- mWrapped.onServiceLost(serviceInfo);
+ mWrappedExecutor.execute(() -> mWrapped.onServiceLost(serviceInfo));
}
}
}
@@ -648,8 +655,12 @@
@Override
public void handleMessage(Message message) {
+ // Do not use message in the executor lambdas, as it will be recycled once this method
+ // returns. Keep references to its content instead.
final int what = message.what;
+ final int errorCode = message.arg1;
final int key = message.arg2;
+ final Object obj = message.obj;
final Object listener;
final NsdServiceInfo ns;
final Executor executor;
@@ -659,7 +670,7 @@
executor = mExecutorMap.get(key);
}
if (listener == null) {
- Log.d(TAG, "Stale key " + message.arg2);
+ Log.d(TAG, "Stale key " + key);
return;
}
if (DBG) {
@@ -667,28 +678,28 @@
}
switch (what) {
case DISCOVER_SERVICES_STARTED:
- final String s = getNsdServiceInfoType((NsdServiceInfo) message.obj);
+ final String s = getNsdServiceInfoType((NsdServiceInfo) obj);
executor.execute(() -> ((DiscoveryListener) listener).onDiscoveryStarted(s));
break;
case DISCOVER_SERVICES_FAILED:
removeListener(key);
executor.execute(() -> ((DiscoveryListener) listener).onStartDiscoveryFailed(
- getNsdServiceInfoType(ns), message.arg1));
+ getNsdServiceInfoType(ns), errorCode));
break;
case SERVICE_FOUND:
executor.execute(() -> ((DiscoveryListener) listener).onServiceFound(
- (NsdServiceInfo) message.obj));
+ (NsdServiceInfo) obj));
break;
case SERVICE_LOST:
executor.execute(() -> ((DiscoveryListener) listener).onServiceLost(
- (NsdServiceInfo) message.obj));
+ (NsdServiceInfo) obj));
break;
case STOP_DISCOVERY_FAILED:
// TODO: failure to stop discovery should be internal and retried internally, as
// the effect for the client is indistinguishable from STOP_DISCOVERY_SUCCEEDED
removeListener(key);
executor.execute(() -> ((DiscoveryListener) listener).onStopDiscoveryFailed(
- getNsdServiceInfoType(ns), message.arg1));
+ getNsdServiceInfoType(ns), errorCode));
break;
case STOP_DISCOVERY_SUCCEEDED:
removeListener(key);
@@ -698,33 +709,33 @@
case REGISTER_SERVICE_FAILED:
removeListener(key);
executor.execute(() -> ((RegistrationListener) listener).onRegistrationFailed(
- ns, message.arg1));
+ ns, errorCode));
break;
case REGISTER_SERVICE_SUCCEEDED:
executor.execute(() -> ((RegistrationListener) listener).onServiceRegistered(
- (NsdServiceInfo) message.obj));
+ (NsdServiceInfo) obj));
break;
case UNREGISTER_SERVICE_FAILED:
removeListener(key);
executor.execute(() -> ((RegistrationListener) listener).onUnregistrationFailed(
- ns, message.arg1));
+ ns, errorCode));
break;
case UNREGISTER_SERVICE_SUCCEEDED:
// TODO: do not unregister listener until service is unregistered, or provide
// alternative way for unregistering ?
- removeListener(message.arg2);
+ removeListener(key);
executor.execute(() -> ((RegistrationListener) listener).onServiceUnregistered(
ns));
break;
case RESOLVE_SERVICE_FAILED:
removeListener(key);
executor.execute(() -> ((ResolveListener) listener).onResolveFailed(
- ns, message.arg1));
+ ns, errorCode));
break;
case RESOLVE_SERVICE_SUCCEEDED:
removeListener(key);
executor.execute(() -> ((ResolveListener) listener).onServiceResolved(
- (NsdServiceInfo) message.obj));
+ (NsdServiceInfo) obj));
break;
default:
Log.d(TAG, "Ignored " + message);
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 2621594..200c808 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -53,6 +53,8 @@
@Nullable
private Network mNetwork;
+ private int mInterfaceIndex;
+
public NsdServiceInfo() {
}
@@ -312,8 +314,11 @@
/**
* Get the network where the service can be found.
*
- * This is never null if this {@link NsdServiceInfo} was obtained from
- * {@link NsdManager#discoverServices} or {@link NsdManager#resolveService}.
+ * This is set if this {@link NsdServiceInfo} was obtained from
+ * {@link NsdManager#discoverServices} or {@link NsdManager#resolveService}, unless the service
+ * was found on a network interface that does not have a {@link Network} (such as a tethering
+ * downstream, where services are advertised from devices connected to this device via
+ * tethering).
*/
@Nullable
public Network getNetwork() {
@@ -329,6 +334,26 @@
mNetwork = network;
}
+ /**
+ * Get the index of the network interface where the service was found.
+ *
+ * This is only set when the service was found on an interface that does not have a usable
+ * Network, in which case {@link #getNetwork()} returns null.
+ * @return The interface index as per {@link java.net.NetworkInterface#getIndex}, or 0 if unset.
+ * @hide
+ */
+ public int getInterfaceIndex() {
+ return mInterfaceIndex;
+ }
+
+ /**
+ * Set the index of the network interface where the service was found.
+ * @hide
+ */
+ public void setInterfaceIndex(int interfaceIndex) {
+ mInterfaceIndex = interfaceIndex;
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@@ -375,6 +400,7 @@
}
dest.writeParcelable(mNetwork, 0);
+ dest.writeInt(mInterfaceIndex);
}
/** Implement the Parcelable interface */
@@ -405,6 +431,7 @@
info.mTxtRecord.put(in.readString(), valueArray);
}
info.mNetwork = in.readParcelable(null, Network.class);
+ info.mInterfaceIndex = in.readInt();
return info;
}
diff --git a/framework/Android.bp b/framework/Android.bp
index d7de439..6350e14 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -64,7 +64,6 @@
":framework-connectivity-sources",
":net-utils-framework-common-srcs",
":framework-connectivity-api-shared-srcs",
- ":framework-connectivity-javastream-protos",
],
aidl: {
generate_get_transaction_name: true,
@@ -90,8 +89,10 @@
"modules-utils-backgroundthread",
"modules-utils-build",
"modules-utils-preconditions",
+ "framework-connectivity-javastream-protos",
],
libs: [
+ "androidx.annotation_annotation",
"app-compat-annotations",
"framework-connectivity-t.stubs.module_lib",
"unsupportedappusage",
@@ -197,28 +198,16 @@
visibility: ["//frameworks/base"],
}
-gensrcs {
+java_library {
name: "framework-connectivity-javastream-protos",
- depfile: true,
-
- tools: [
- "aprotoc",
- "protoc-gen-javastream",
- "soong_zip",
+ proto: {
+ type: "stream",
+ },
+ srcs: [":framework-connectivity-protos"],
+ installable: false,
+ sdk_version: "module_current",
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.tethering",
],
-
- cmd: "mkdir -p $(genDir)/$(in) " +
- "&& $(location aprotoc) " +
- " --plugin=$(location protoc-gen-javastream) " +
- " --dependency_out=$(depfile) " +
- " --javastream_out=$(genDir)/$(in) " +
- " -Iexternal/protobuf/src " +
- " -I . " +
- " $(in) " +
- "&& $(location soong_zip) -jar -o $(out) -C $(genDir)/$(in) -D $(genDir)/$(in)",
-
- srcs: [
- ":framework-connectivity-protos",
- ],
- output_extension: "srcjar",
}
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index ddac19d..a2a1ac0 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -51,6 +51,9 @@
field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8
field public static final int FIREWALL_CHAIN_DOZABLE = 1; // 0x1
field public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5; // 0x5
+ field public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7; // 0x7
+ field public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8; // 0x8
+ field public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9; // 0x9
field public static final int FIREWALL_CHAIN_POWERSAVE = 3; // 0x3
field public static final int FIREWALL_CHAIN_RESTRICTED = 4; // 0x4
field public static final int FIREWALL_CHAIN_STANDBY = 2; // 0x2
@@ -197,6 +200,8 @@
method public int describeContents();
method @NonNull public android.os.ParcelFileDescriptor getFileDescriptor();
method @NonNull public String getInterfaceName();
+ method @Nullable public android.net.MacAddress getMacAddress();
+ method public int getMtu();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkInterface> CREATOR;
}
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index db1d7e9..f1298ce 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -249,10 +249,10 @@
method public void onValidationStatus(int, @Nullable android.net.Uri);
method @NonNull public android.net.Network register();
method public void sendAddDscpPolicy(@NonNull android.net.DscpPolicy);
- method public final void sendLinkProperties(@NonNull android.net.LinkProperties);
- method public final void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
- method public final void sendNetworkScore(@NonNull android.net.NetworkScore);
- method public final void sendNetworkScore(@IntRange(from=0, to=99) int);
+ method public void sendLinkProperties(@NonNull android.net.LinkProperties);
+ method public void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
+ method public void sendNetworkScore(@NonNull android.net.NetworkScore);
+ method public void sendNetworkScore(@IntRange(from=0, to=99) int);
method public final void sendQosCallbackError(int, int);
method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes);
method public final void sendQosSessionLost(int, int, int);
@@ -262,7 +262,7 @@
method @Deprecated public void setLegacySubtype(int, @NonNull String);
method public void setLingerDuration(@NonNull java.time.Duration);
method public void setTeardownDelayMillis(@IntRange(from=0, to=0x1388) int);
- method public final void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
+ method public void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
method public void unregister();
method public void unregisterAfterReplacement(@IntRange(from=0, to=0x1388) int);
field public static final int DSCP_POLICY_STATUS_DELETED = 4; // 0x4
diff --git a/framework/jarjar-excludes.txt b/framework/jarjar-excludes.txt
new file mode 100644
index 0000000..1311765
--- /dev/null
+++ b/framework/jarjar-excludes.txt
@@ -0,0 +1,25 @@
+# INetworkStatsProvider / INetworkStatsProviderCallback are referenced from net-tests-utils, which
+# may be used by tests that do not apply connectivity jarjar rules.
+# TODO: move files to a known internal package (like android.net.connectivity.visiblefortesting)
+# so that they do not need jarjar
+android\.net\.netstats\.provider\.INetworkStatsProvider(\$.+)?
+android\.net\.netstats\.provider\.INetworkStatsProviderCallback(\$.+)?
+
+# INetworkAgent / INetworkAgentRegistry are used in NetworkAgentTest
+# TODO: move files to android.net.connectivity.visiblefortesting
+android\.net\.INetworkAgent(\$.+)?
+android\.net\.INetworkAgentRegistry(\$.+)?
+
+# IConnectivityDiagnosticsCallback used in ConnectivityDiagnosticsManagerTest
+# TODO: move files to android.net.connectivity.visiblefortesting
+android\.net\.IConnectivityDiagnosticsCallback(\$.+)?
+
+
+# KeepaliveUtils is used by ConnectivityManager CTS
+# TODO: move into service-connectivity so framework-connectivity stops using
+# ServiceConnectivityResources (callers need high permissions to find/query the resource apk anyway)
+# and have a ConnectivityManager test API instead
+android\.net\.util\.KeepaliveUtils(\$.+)?
+
+# TODO (b/217115866): add jarjar rules for Nearby
+android\.nearby\..+
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index a174fe3..eeedfd1 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -982,6 +982,30 @@
@SystemApi(client = MODULE_LIBRARIES)
public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5;
+ /**
+ * Firewall chain used for OEM-specific application restrictions.
+ * Denylist of apps that will not have network access due to OEM-specific restrictions.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7;
+
+ /**
+ * Firewall chain used for OEM-specific application restrictions.
+ * Denylist of apps that will not have network access due to OEM-specific restrictions.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8;
+
+ /**
+ * Firewall chain used for OEM-specific application restrictions.
+ * Denylist of apps that will not have network access due to OEM-specific restrictions.
+ * @hide
+ */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = false, prefix = "FIREWALL_CHAIN_", value = {
@@ -989,7 +1013,10 @@
FIREWALL_CHAIN_STANDBY,
FIREWALL_CHAIN_POWERSAVE,
FIREWALL_CHAIN_RESTRICTED,
- FIREWALL_CHAIN_LOW_POWER_STANDBY
+ FIREWALL_CHAIN_LOW_POWER_STANDBY,
+ FIREWALL_CHAIN_OEM_DENY_1,
+ FIREWALL_CHAIN_OEM_DENY_2,
+ FIREWALL_CHAIN_OEM_DENY_3
})
public @interface FirewallChain {}
// LINT.ThenChange(packages/modules/Connectivity/service/native/include/Common.h)
@@ -2589,9 +2616,24 @@
* {@hide}
*/
public ConnectivityManager(Context context, IConnectivityManager service) {
+ this(context, service, true /* newStatic */);
+ }
+
+ private ConnectivityManager(Context context, IConnectivityManager service, boolean newStatic) {
mContext = Objects.requireNonNull(context, "missing context");
mService = Objects.requireNonNull(service, "missing IConnectivityManager");
- sInstance = this;
+ // sInstance is accessed without a lock, so it may actually be reassigned several times with
+ // different ConnectivityManager, but that's still OK considering its usage.
+ if (sInstance == null && newStatic) {
+ final Context appContext = mContext.getApplicationContext();
+ // Don't create static ConnectivityManager instance again to prevent infinite loop.
+ // If the application context is null, we're either in the system process or
+ // it's the application context very early in app initialization. In both these
+ // cases, the passed-in Context will not be freed, so it's safe to pass it to the
+ // service. http://b/27532714 .
+ sInstance = new ConnectivityManager(appContext != null ? appContext : context, service,
+ false /* newStatic */);
+ }
}
/** {@hide} */
@@ -5861,6 +5903,7 @@
*
* @param chain target chain.
* @param enable whether the chain should be enabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
* @throws IllegalStateException if enabling or disabling the firewall chain failed.
* @hide
*/
@@ -5879,6 +5922,29 @@
}
/**
+ * Get the specified firewall chain's status.
+ *
+ * @param chain target chain.
+ * @return {@code true} if chain is enabled, {@code false} if chain is disabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ * @hide
+ */
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+ })
+ public boolean getFirewallChainEnabled(@FirewallChain final int chain) {
+ try {
+ return mService.getFirewallChainEnabled(chain);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Replaces the contents of the specified UID-based firewall chain.
*
* @param chain target chain to replace.
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index bc73769..29fea00 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -244,5 +244,7 @@
void setFirewallChainEnabled(int chain, boolean enable);
+ boolean getFirewallChainEnabled(int chain);
+
void replaceFirewallChain(int chain, in int[] uids);
}
diff --git a/framework/src/android/net/ITestNetworkManager.aidl b/framework/src/android/net/ITestNetworkManager.aidl
index 847f14e..d18b931 100644
--- a/framework/src/android/net/ITestNetworkManager.aidl
+++ b/framework/src/android/net/ITestNetworkManager.aidl
@@ -29,7 +29,10 @@
*/
interface ITestNetworkManager
{
- TestNetworkInterface createInterface(boolean isTun, boolean bringUp, in LinkAddress[] addrs);
+ TestNetworkInterface createInterface(boolean isTun, boolean hasCarrier, boolean bringUp,
+ in LinkAddress[] addrs, in @nullable String iface);
+
+ void setCarrierEnabled(in TestNetworkInterface iface, boolean enabled);
void setupTestNetwork(in String iface, in LinkProperties lp, in boolean isMetered,
in int[] administratorUids, in IBinder binder);
diff --git a/framework/src/android/net/LinkProperties.java b/framework/src/android/net/LinkProperties.java
index 8782b33..a8f707e 100644
--- a/framework/src/android/net/LinkProperties.java
+++ b/framework/src/android/net/LinkProperties.java
@@ -64,7 +64,7 @@
* @hide
*/
@ChangeId
- @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S) // Switch to S_V2 when it is available.
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S_V2)
@VisibleForTesting
public static final long EXCLUDED_ROUTES = 186082280;
@@ -1366,6 +1366,21 @@
}
/**
+ * Returns true if this link has a throw route.
+ *
+ * @return {@code true} if there is an exclude route, {@code false} otherwise.
+ * @hide
+ */
+ public boolean hasExcludeRoute() {
+ for (RouteInfo r : mRoutes) {
+ if (r.getType() == RouteInfo.RTN_THROW) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Compares this {@code LinkProperties} interface name against the target
*
* @param target LinkProperties to compare.
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 29add1c..5659a35 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -913,7 +913,7 @@
* Must be called by the agent when the network's {@link LinkProperties} change.
* @param linkProperties the new LinkProperties.
*/
- public final void sendLinkProperties(@NonNull LinkProperties linkProperties) {
+ public void sendLinkProperties(@NonNull LinkProperties linkProperties) {
Objects.requireNonNull(linkProperties);
final LinkProperties lp = new LinkProperties(linkProperties);
queueOrSendMessage(reg -> reg.sendLinkProperties(lp));
@@ -938,7 +938,7 @@
* @param underlyingNetworks the new list of underlying networks.
* @see {@link VpnService.Builder#setUnderlyingNetworks(Network[])}
*/
- public final void setUnderlyingNetworks(
+ public void setUnderlyingNetworks(
@SuppressLint("NullableCollection") @Nullable List<Network> underlyingNetworks) {
final ArrayList<Network> underlyingArray = (underlyingNetworks != null)
? new ArrayList<>(underlyingNetworks) : null;
@@ -1076,18 +1076,19 @@
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public final void sendNetworkInfo(NetworkInfo networkInfo) {
- queueOrSendNetworkInfo(new NetworkInfo(networkInfo));
+ queueOrSendNetworkInfo(networkInfo);
}
private void queueOrSendNetworkInfo(NetworkInfo networkInfo) {
- queueOrSendMessage(reg -> reg.sendNetworkInfo(networkInfo));
+ final NetworkInfo ni = new NetworkInfo(networkInfo);
+ queueOrSendMessage(reg -> reg.sendNetworkInfo(ni));
}
/**
* Must be called by the agent when the network's {@link NetworkCapabilities} change.
* @param networkCapabilities the new NetworkCapabilities.
*/
- public final void sendNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
+ public void sendNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
Objects.requireNonNull(networkCapabilities);
mBandwidthUpdatePending.set(false);
mLastBwRefreshTime = System.currentTimeMillis();
@@ -1101,7 +1102,7 @@
*
* @param score the new score.
*/
- public final void sendNetworkScore(@NonNull NetworkScore score) {
+ public void sendNetworkScore(@NonNull NetworkScore score) {
Objects.requireNonNull(score);
queueOrSendMessage(reg -> reg.sendScore(score));
}
@@ -1112,7 +1113,7 @@
* @param score the new score, between 0 and 99.
* deprecated use sendNetworkScore(NetworkScore) TODO : remove in S.
*/
- public final void sendNetworkScore(@IntRange(from = 0, to = 99) int score) {
+ public void sendNetworkScore(@IntRange(from = 0, to = 99) int score) {
sendNetworkScore(new NetworkScore.Builder().setLegacyInt(score).build());
}
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 97b1f32..dbb05a9 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -3043,7 +3043,7 @@
* <p>
* This list cannot be null, but it can be empty to mean that no UID without the
* {@link android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS} permission
- * gets to access this network.
+ * can access this network.
*
* @param uids the list of UIDs that can always access this network
* @return this builder
diff --git a/framework/src/android/net/NetworkProvider.java b/framework/src/android/net/NetworkProvider.java
index 0665af5..3615075 100644
--- a/framework/src/android/net/NetworkProvider.java
+++ b/framework/src/android/net/NetworkProvider.java
@@ -192,21 +192,36 @@
private class NetworkOfferCallbackProxy extends INetworkOfferCallback.Stub {
@NonNull public final NetworkOfferCallback callback;
@NonNull private final Executor mExecutor;
+ /**
+ * Boolean flag that prevents onNetworkNeeded / onNetworkUnneeded callbacks from being
+ * propagated after unregisterNetworkOffer has been called. Since unregisterNetworkOffer
+ * runs on the CS handler thread, it will not go into effect immediately.
+ */
+ private volatile boolean mIsStale;
NetworkOfferCallbackProxy(@NonNull final NetworkOfferCallback callback,
@NonNull final Executor executor) {
this.callback = callback;
this.mExecutor = executor;
+ this.mIsStale = false;
}
@Override
public void onNetworkNeeded(final @NonNull NetworkRequest request) {
- mExecutor.execute(() -> callback.onNetworkNeeded(request));
+ mExecutor.execute(() -> {
+ if (!mIsStale) callback.onNetworkNeeded(request);
+ });
}
@Override
public void onNetworkUnneeded(final @NonNull NetworkRequest request) {
- mExecutor.execute(() -> callback.onNetworkUnneeded(request));
+ mExecutor.execute(() -> {
+ if (!mIsStale) callback.onNetworkUnneeded(request);
+ });
+ }
+
+ public void markStale() {
+ mIsStale = true;
}
}
@@ -326,7 +341,10 @@
public void unregisterNetworkOffer(final @NonNull NetworkOfferCallback callback) {
final NetworkOfferCallbackProxy proxy = findProxyForCallback(callback);
if (null == proxy) return;
- mProxies.remove(proxy);
+ synchronized (mProxies) {
+ proxy.markStale();
+ mProxies.remove(proxy);
+ }
mContext.getSystemService(ConnectivityManager.class).unofferNetwork(proxy);
}
}
diff --git a/framework/src/android/net/ProfileNetworkPreference.java b/framework/src/android/net/ProfileNetworkPreference.java
index fb271e3..fdcab02 100644
--- a/framework/src/android/net/ProfileNetworkPreference.java
+++ b/framework/src/android/net/ProfileNetworkPreference.java
@@ -120,8 +120,8 @@
public String toString() {
return "ProfileNetworkPreference{"
+ "mPreference=" + getPreference()
- + "mIncludedUids=" + mIncludedUids.toString()
- + "mExcludedUids=" + mExcludedUids.toString()
+ + "mIncludedUids=" + Arrays.toString(mIncludedUids)
+ + "mExcludedUids=" + Arrays.toString(mExcludedUids)
+ "mPreferenceEnterpriseId=" + mPreferenceEnterpriseId
+ '}';
}
diff --git a/framework/src/android/net/QosCallbackException.java b/framework/src/android/net/QosCallbackException.java
index ed6eb15..b80cff4 100644
--- a/framework/src/android/net/QosCallbackException.java
+++ b/framework/src/android/net/QosCallbackException.java
@@ -46,8 +46,10 @@
EX_TYPE_FILTER_NONE,
EX_TYPE_FILTER_NETWORK_RELEASED,
EX_TYPE_FILTER_SOCKET_NOT_BOUND,
+ EX_TYPE_FILTER_SOCKET_NOT_CONNECTED,
EX_TYPE_FILTER_NOT_SUPPORTED,
EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED,
+ EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ExceptionType {}
@@ -65,10 +67,16 @@
public static final int EX_TYPE_FILTER_SOCKET_NOT_BOUND = 2;
/** {@hide} */
- public static final int EX_TYPE_FILTER_NOT_SUPPORTED = 3;
+ public static final int EX_TYPE_FILTER_SOCKET_NOT_CONNECTED = 3;
/** {@hide} */
- public static final int EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED = 4;
+ public static final int EX_TYPE_FILTER_NOT_SUPPORTED = 4;
+
+ /** {@hide} */
+ public static final int EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED = 5;
+
+ /** {@hide} */
+ public static final int EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED = 6;
/**
* Creates exception based off of a type and message. Not all types of exceptions accept a
@@ -83,12 +91,17 @@
return new QosCallbackException(new NetworkReleasedException());
case EX_TYPE_FILTER_SOCKET_NOT_BOUND:
return new QosCallbackException(new SocketNotBoundException());
+ case EX_TYPE_FILTER_SOCKET_NOT_CONNECTED:
+ return new QosCallbackException(new SocketNotConnectedException());
case EX_TYPE_FILTER_NOT_SUPPORTED:
return new QosCallbackException(new UnsupportedOperationException(
"This device does not support the specified filter"));
case EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED:
return new QosCallbackException(
new SocketLocalAddressChangedException());
+ case EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED:
+ return new QosCallbackException(
+ new SocketRemoteAddressChangedException());
default:
Log.wtf(TAG, "create: No case setup for exception type: '" + type + "'");
return new QosCallbackException(
diff --git a/framework/src/android/net/QosFilter.java b/framework/src/android/net/QosFilter.java
index 5c1c3cc..b432644 100644
--- a/framework/src/android/net/QosFilter.java
+++ b/framework/src/android/net/QosFilter.java
@@ -90,5 +90,15 @@
*/
public abstract boolean matchesRemoteAddress(@NonNull InetAddress address,
int startPort, int endPort);
+
+ /**
+ * Determines whether or not the parameter will be matched with this filter.
+ *
+ * @param protocol the protocol such as TCP or UDP included in IP packet filter set of a QoS
+ * flow assigned on {@link Network}.
+ * @return whether the parameters match the socket type of the filter
+ * @hide
+ */
+ public abstract boolean matchesProtocol(int protocol);
}
diff --git a/framework/src/android/net/QosFilterParcelable.java b/framework/src/android/net/QosFilterParcelable.java
index da3b2cf..6e71fa3 100644
--- a/framework/src/android/net/QosFilterParcelable.java
+++ b/framework/src/android/net/QosFilterParcelable.java
@@ -104,7 +104,7 @@
if (mQosFilter instanceof QosSocketFilter) {
dest.writeInt(QOS_SOCKET_FILTER);
final QosSocketFilter qosSocketFilter = (QosSocketFilter) mQosFilter;
- qosSocketFilter.getQosSocketInfo().writeToParcel(dest, 0);
+ qosSocketFilter.getQosSocketInfo().writeToParcelWithoutFd(dest, 0);
return;
}
dest.writeInt(NO_FILTER_PRESENT);
diff --git a/framework/src/android/net/QosSocketFilter.java b/framework/src/android/net/QosSocketFilter.java
index 69da7f4..5ceeb67 100644
--- a/framework/src/android/net/QosSocketFilter.java
+++ b/framework/src/android/net/QosSocketFilter.java
@@ -18,6 +18,13 @@
import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE;
import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_CONNECTED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_STREAM;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -74,19 +81,34 @@
* 2. In the scenario that the socket is now bound to a different local address, which can
* happen in the case of UDP, then
* {@link QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED} is returned.
+ * 3. In the scenario that the UDP socket changed remote address, then
+ * {@link QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED} is returned.
+ *
* @return validation error code
*/
@Override
public int validate() {
- final InetSocketAddress sa = getAddressFromFileDescriptor();
- if (sa == null) {
- return QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+ final InetSocketAddress sa = getLocalAddressFromFileDescriptor();
+
+ if (sa == null || (sa.getAddress().isAnyLocalAddress() && sa.getPort() == 0)) {
+ return EX_TYPE_FILTER_SOCKET_NOT_BOUND;
}
if (!sa.equals(mQosSocketInfo.getLocalSocketAddress())) {
return EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
}
+ if (mQosSocketInfo.getRemoteSocketAddress() != null) {
+ final InetSocketAddress da = getRemoteAddressFromFileDescriptor();
+ if (da == null) {
+ return EX_TYPE_FILTER_SOCKET_NOT_CONNECTED;
+ }
+
+ if (!da.equals(mQosSocketInfo.getRemoteSocketAddress())) {
+ return EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED;
+ }
+ }
+
return EX_TYPE_FILTER_NONE;
}
@@ -98,17 +120,14 @@
* @return the local address
*/
@Nullable
- private InetSocketAddress getAddressFromFileDescriptor() {
+ private InetSocketAddress getLocalAddressFromFileDescriptor() {
final ParcelFileDescriptor parcelFileDescriptor = mQosSocketInfo.getParcelFileDescriptor();
- if (parcelFileDescriptor == null) return null;
-
final FileDescriptor fd = parcelFileDescriptor.getFileDescriptor();
- if (fd == null) return null;
final SocketAddress address;
try {
address = Os.getsockname(fd);
- } catch (final ErrnoException e) {
+ } catch (ErrnoException e) {
Log.e(TAG, "getAddressFromFileDescriptor: getLocalAddress exception", e);
return null;
}
@@ -119,6 +138,31 @@
}
/**
+ * The remote address of the socket's connected.
+ *
+ * <p>Note: If the socket is no longer connected, null is returned.
+ *
+ * @return the remote address
+ */
+ @Nullable
+ private InetSocketAddress getRemoteAddressFromFileDescriptor() {
+ final ParcelFileDescriptor parcelFileDescriptor = mQosSocketInfo.getParcelFileDescriptor();
+ final FileDescriptor fd = parcelFileDescriptor.getFileDescriptor();
+
+ final SocketAddress address;
+ try {
+ address = Os.getpeername(fd);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "getAddressFromFileDescriptor: getRemoteAddress exception", e);
+ return null;
+ }
+ if (address instanceof InetSocketAddress) {
+ return (InetSocketAddress) address;
+ }
+ return null;
+ }
+
+ /**
* The network used with this filter.
*
* @return the registered {@link Network}
@@ -156,6 +200,18 @@
}
/**
+ * @inheritDoc
+ */
+ @Override
+ public boolean matchesProtocol(final int protocol) {
+ if ((mQosSocketInfo.getSocketType() == SOCK_STREAM && protocol == IPPROTO_TCP)
+ || (mQosSocketInfo.getSocketType() == SOCK_DGRAM && protocol == IPPROTO_UDP)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
* Called from {@link QosSocketFilter#matchesLocalAddress(InetAddress, int, int)}
* and {@link QosSocketFilter#matchesRemoteAddress(InetAddress, int, int)} with the
* filterSocketAddress coming from {@link QosSocketInfo#getLocalSocketAddress()}.
@@ -174,6 +230,7 @@
final int startPort, final int endPort) {
return startPort <= filterSocketAddress.getPort()
&& endPort >= filterSocketAddress.getPort()
- && filterSocketAddress.getAddress().equals(address);
+ && (address.isAnyLocalAddress()
+ || filterSocketAddress.getAddress().equals(address));
}
}
diff --git a/framework/src/android/net/QosSocketInfo.java b/framework/src/android/net/QosSocketInfo.java
index 39c2f33..49ac22b 100644
--- a/framework/src/android/net/QosSocketInfo.java
+++ b/framework/src/android/net/QosSocketInfo.java
@@ -165,25 +165,28 @@
/* Parcelable methods */
private QosSocketInfo(final Parcel in) {
mNetwork = Objects.requireNonNull(Network.CREATOR.createFromParcel(in));
- mParcelFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(in);
+ final boolean withFd = in.readBoolean();
+ if (withFd) {
+ mParcelFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(in);
+ } else {
+ mParcelFileDescriptor = null;
+ }
- final int localAddressLength = in.readInt();
- mLocalSocketAddress = readSocketAddress(in, localAddressLength);
-
- final int remoteAddressLength = in.readInt();
- mRemoteSocketAddress = remoteAddressLength == 0 ? null
- : readSocketAddress(in, remoteAddressLength);
+ mLocalSocketAddress = readSocketAddress(in);
+ mRemoteSocketAddress = readSocketAddress(in);
mSocketType = in.readInt();
}
- private @NonNull InetSocketAddress readSocketAddress(final Parcel in, final int addressLength) {
- final byte[] address = new byte[addressLength];
- in.readByteArray(address);
+ private InetSocketAddress readSocketAddress(final Parcel in) {
+ final byte[] addrBytes = in.createByteArray();
+ if (addrBytes == null) {
+ return null;
+ }
final int port = in.readInt();
try {
- return new InetSocketAddress(InetAddress.getByAddress(address), port);
+ return new InetSocketAddress(InetAddress.getByAddress(addrBytes), port);
} catch (final UnknownHostException e) {
/* This can never happen. UnknownHostException will never be thrown
since the address provided is numeric and non-null. */
@@ -198,20 +201,35 @@
@Override
public void writeToParcel(@NonNull final Parcel dest, final int flags) {
- mNetwork.writeToParcel(dest, 0);
- mParcelFileDescriptor.writeToParcel(dest, 0);
+ writeToParcelInternal(dest, flags, /*includeFd=*/ true);
+ }
- final byte[] localAddress = mLocalSocketAddress.getAddress().getAddress();
- dest.writeInt(localAddress.length);
- dest.writeByteArray(localAddress);
+ /**
+ * Used when sending QosSocketInfo to telephony, which does not need access to the socket FD.
+ * @hide
+ */
+ public void writeToParcelWithoutFd(@NonNull final Parcel dest, final int flags) {
+ writeToParcelInternal(dest, flags, /*includeFd=*/ false);
+ }
+
+ private void writeToParcelInternal(
+ @NonNull final Parcel dest, final int flags, boolean includeFd) {
+ mNetwork.writeToParcel(dest, 0);
+
+ if (includeFd) {
+ dest.writeBoolean(true);
+ mParcelFileDescriptor.writeToParcel(dest, 0);
+ } else {
+ dest.writeBoolean(false);
+ }
+
+ dest.writeByteArray(mLocalSocketAddress.getAddress().getAddress());
dest.writeInt(mLocalSocketAddress.getPort());
if (mRemoteSocketAddress == null) {
- dest.writeInt(0);
+ dest.writeByteArray(null);
} else {
- final byte[] remoteAddress = mRemoteSocketAddress.getAddress().getAddress();
- dest.writeInt(remoteAddress.length);
- dest.writeByteArray(remoteAddress);
+ dest.writeByteArray(mRemoteSocketAddress.getAddress().getAddress());
dest.writeInt(mRemoteSocketAddress.getPort());
}
dest.writeInt(mSocketType);
diff --git a/framework/src/android/net/SocketNotConnectedException.java b/framework/src/android/net/SocketNotConnectedException.java
new file mode 100644
index 0000000..fa2a615
--- /dev/null
+++ b/framework/src/android/net/SocketNotConnectedException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/**
+ * Thrown when a previously bound socket becomes unbound.
+ *
+ * @hide
+ */
+public class SocketNotConnectedException extends Exception {
+ /** @hide */
+ public SocketNotConnectedException() {
+ super("The socket is not connected");
+ }
+}
diff --git a/framework/src/android/net/SocketRemoteAddressChangedException.java b/framework/src/android/net/SocketRemoteAddressChangedException.java
new file mode 100644
index 0000000..ecaeebc
--- /dev/null
+++ b/framework/src/android/net/SocketRemoteAddressChangedException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/**
+ * Thrown when the local address of the socket has changed.
+ *
+ * @hide
+ */
+public class SocketRemoteAddressChangedException extends Exception {
+ /** @hide */
+ public SocketRemoteAddressChangedException() {
+ super("The remote address of the socket changed");
+ }
+}
diff --git a/framework/src/android/net/TestNetworkInterface.java b/framework/src/android/net/TestNetworkInterface.java
index 4449ff8..26200e1 100644
--- a/framework/src/android/net/TestNetworkInterface.java
+++ b/framework/src/android/net/TestNetworkInterface.java
@@ -16,22 +16,32 @@
package android.net;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
+import android.util.Log;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
/**
- * This class is used to return the interface name and fd of the test interface
+ * This class is used to return the interface name, fd, MAC, and MTU of the test interface
*
* @hide
*/
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public final class TestNetworkInterface implements Parcelable {
+ private static final String TAG = "TestNetworkInterface";
+
@NonNull
private final ParcelFileDescriptor mFileDescriptor;
@NonNull
private final String mInterfaceName;
+ @Nullable
+ private final MacAddress mMacAddress;
+ private final int mMtu;
@Override
public int describeContents() {
@@ -40,18 +50,41 @@
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
- out.writeParcelable(mFileDescriptor, PARCELABLE_WRITE_RETURN_VALUE);
+ out.writeParcelable(mFileDescriptor, flags);
out.writeString(mInterfaceName);
+ out.writeParcelable(mMacAddress, flags);
+ out.writeInt(mMtu);
}
public TestNetworkInterface(@NonNull ParcelFileDescriptor pfd, @NonNull String intf) {
mFileDescriptor = pfd;
mInterfaceName = intf;
+
+ MacAddress macAddress = null;
+ int mtu = 1500;
+ try {
+ // This constructor is called by TestNetworkManager which runs inside the system server,
+ // which has permission to read the MacAddress.
+ NetworkInterface nif = NetworkInterface.getByName(mInterfaceName);
+
+ // getHardwareAddress() returns null for tun interfaces.
+ byte[] hardwareAddress = nif.getHardwareAddress();
+ if (hardwareAddress != null) {
+ macAddress = MacAddress.fromBytes(nif.getHardwareAddress());
+ }
+ mtu = nif.getMTU();
+ } catch (SocketException e) {
+ Log.e(TAG, "Failed to fetch MacAddress or MTU size from NetworkInterface", e);
+ }
+ mMacAddress = macAddress;
+ mMtu = mtu;
}
private TestNetworkInterface(@NonNull Parcel in) {
mFileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
mInterfaceName = in.readString();
+ mMacAddress = in.readParcelable(MacAddress.class.getClassLoader());
+ mMtu = in.readInt();
}
@NonNull
@@ -64,6 +97,15 @@
return mInterfaceName;
}
+ @Nullable
+ public MacAddress getMacAddress() {
+ return mMacAddress;
+ }
+
+ public int getMtu() {
+ return mMtu;
+ }
+
@NonNull
public static final Parcelable.Creator<TestNetworkInterface> CREATOR =
new Parcelable.Creator<TestNetworkInterface>() {
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 280e497..788834a 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -45,6 +45,12 @@
*/
public static final String TEST_TAP_PREFIX = "testtap";
+ /**
+ * Prefix for clat interfaces.
+ * @hide
+ */
+ public static final String CLAT_INTERFACE_PREFIX = "v4-";
+
@NonNull private static final String TAG = TestNetworkManager.class.getSimpleName();
@NonNull private final ITestNetworkManager mService;
@@ -52,6 +58,7 @@
private static final boolean TAP = false;
private static final boolean TUN = true;
private static final boolean BRING_UP = true;
+ private static final boolean CARRIER_UP = true;
private static final LinkAddress[] NO_ADDRS = new LinkAddress[0];
/** @hide */
@@ -160,7 +167,8 @@
public TestNetworkInterface createTunInterface(@NonNull Collection<LinkAddress> linkAddrs) {
try {
final LinkAddress[] arr = new LinkAddress[linkAddrs.size()];
- return mService.createInterface(TUN, BRING_UP, linkAddrs.toArray(arr));
+ return mService.createInterface(TUN, CARRIER_UP, BRING_UP, linkAddrs.toArray(arr),
+ null /* iface */);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -178,7 +186,25 @@
@NonNull
public TestNetworkInterface createTapInterface() {
try {
- return mService.createInterface(TAP, BRING_UP, NO_ADDRS);
+ return mService.createInterface(TAP, CARRIER_UP, BRING_UP, NO_ADDRS, null /* iface */);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Create a tap interface for testing purposes
+ *
+ * @param linkAddrs an array of LinkAddresses to assign to the TAP interface
+ * @return A TestNetworkInterface representing the underlying TAP interface. Close the contained
+ * ParcelFileDescriptor to tear down the TAP interface.
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+ @NonNull
+ public TestNetworkInterface createTapInterface(@NonNull LinkAddress[] linkAddrs) {
+ try {
+ return mService.createInterface(TAP, CARRIER_UP, BRING_UP, linkAddrs, null /* iface */);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -197,7 +223,66 @@
@NonNull
public TestNetworkInterface createTapInterface(boolean bringUp) {
try {
- return mService.createInterface(TAP, bringUp, NO_ADDRS);
+ return mService.createInterface(TAP, CARRIER_UP, bringUp, NO_ADDRS, null /* iface */);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Create a tap interface with a given interface name for testing purposes
+ *
+ * @param bringUp whether to bring up the interface before returning it.
+ * @param iface interface name to be assigned, so far only interface name which starts with
+ * "v4-testtap" or "v4-testtun" is allowed to be created. If it's null, then use
+ * the default name(e.g. testtap or testtun).
+ *
+ * @return A ParcelFileDescriptor of the underlying TAP interface. Close this to tear down the
+ * TAP interface.
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+ @NonNull
+ public TestNetworkInterface createTapInterface(boolean bringUp, @NonNull String iface) {
+ try {
+ return mService.createInterface(TAP, CARRIER_UP, bringUp, NO_ADDRS, iface);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Create a tap interface with or without carrier for testing purposes.
+ *
+ * Note: setting carrierUp = false is not supported until kernel version 5.0.
+ *
+ * @param carrierUp whether the created interface has a carrier or not.
+ * @param bringUp whether to bring up the interface before returning it.
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+ @NonNull
+ public TestNetworkInterface createTapInterface(boolean carrierUp, boolean bringUp) {
+ try {
+ return mService.createInterface(TAP, carrierUp, bringUp, NO_ADDRS, null /* iface */);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Enable / disable carrier on TestNetworkInterface
+ *
+ * Note: TUNSETCARRIER is not supported until kernel version 5.0.
+ *
+ * @param iface the interface to configure.
+ * @param enabled true to turn carrier on, false to turn carrier off.
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+ public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
+ try {
+ mService.setCarrierEnabled(iface, enabled);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/netd/Android.bp b/netd/Android.bp
index 5ac02d3..c731b8b 100644
--- a/netd/Android.bp
+++ b/netd/Android.bp
@@ -55,7 +55,8 @@
cc_test {
name: "netd_updatable_unit_test",
defaults: ["netd_defaults"],
- test_suites: ["general-tests"],
+ test_suites: ["general-tests", "mts-tethering"],
+ test_config_template: ":net_native_test_config_template",
require_root: true, // required by setrlimitForTest()
header_libs: [
"bpf_connectivity_headers",
@@ -72,6 +73,7 @@
"liblog",
"libnetdutils",
],
+ compile_multilib: "both",
multilib: {
lib32: {
suffix: "32",
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index f3dfb57..fad6bbb 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -73,16 +73,7 @@
}
RETURN_IF_NOT_OK(attachProgramToCgroup(BPF_EGRESS_PROG_PATH, cg_fd, BPF_CGROUP_INET_EGRESS));
RETURN_IF_NOT_OK(attachProgramToCgroup(BPF_INGRESS_PROG_PATH, cg_fd, BPF_CGROUP_INET_INGRESS));
-
- // For the devices that support cgroup socket filter, the socket filter
- // should be loaded successfully by bpfloader. So we attach the filter to
- // cgroup if the program is pinned properly.
- // TODO: delete the if statement once all devices should support cgroup
- // socket filter (ie. the minimum kernel version required is 4.14).
- if (!access(CGROUP_SOCKET_PROG_PATH, F_OK)) {
- RETURN_IF_NOT_OK(
- attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
- }
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
return netdutils::status::ok;
}
@@ -110,9 +101,8 @@
RETURN_IF_NOT_OK(mStatsMapA.init(STATS_MAP_A_PATH));
RETURN_IF_NOT_OK(mStatsMapB.init(STATS_MAP_B_PATH));
RETURN_IF_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
- RETURN_IF_NOT_OK(mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, SELECT_MAP_A,
- BPF_ANY));
RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
+ ALOGI("%s successfully", __func__);
return netdutils::status::ok;
}
@@ -207,6 +197,7 @@
BpfMap<StatsKey, StatsValue>& currentMap =
(configuration.value() == SELECT_MAP_A) ? mStatsMapA : mStatsMapB;
+ // HACK: mStatsMapB becomes RW BpfMap here, but countUidStatsEntries doesn't modify so it works
base::Result<void> res = currentMap.iterate(countUidStatsEntries);
if (!res.ok()) {
ALOGE("Failed to count the stats entry in map %d: %s", currentMap.getMap().get(),
@@ -242,7 +233,7 @@
if (sock_cookie == NONEXISTENT_COOKIE) return -errno;
base::Result<void> res = mCookieTagMap.deleteValue(sock_cookie);
if (!res.ok()) {
- ALOGE("Failed to untag socket: %s\n", strerror(res.error().code()));
+ ALOGE("Failed to untag socket: %s", strerror(res.error().code()));
return -res.error().code();
}
return 0;
diff --git a/netd/BpfHandler.h b/netd/BpfHandler.h
index 2ede1c1..5ee04d1 100644
--- a/netd/BpfHandler.h
+++ b/netd/BpfHandler.h
@@ -23,6 +23,7 @@
#include "bpf_shared.h"
using android::bpf::BpfMap;
+using android::bpf::BpfMapRO;
namespace android {
namespace net {
@@ -61,8 +62,8 @@
BpfMap<uint64_t, UidTagValue> mCookieTagMap;
BpfMap<StatsKey, StatsValue> mStatsMapA;
- BpfMap<StatsKey, StatsValue> mStatsMapB;
- BpfMap<uint32_t, uint8_t> mConfigurationMap;
+ BpfMapRO<StatsKey, StatsValue> mStatsMapB;
+ BpfMapRO<uint32_t, uint32_t> mConfigurationMap;
BpfMap<uint32_t, uint8_t> mUidPermissionMap;
std::mutex mMutex;
diff --git a/netd/BpfHandlerTest.cpp b/netd/BpfHandlerTest.cpp
index cd6b565..99160da 100644
--- a/netd/BpfHandlerTest.cpp
+++ b/netd/BpfHandlerTest.cpp
@@ -21,6 +21,7 @@
#include <gtest/gtest.h>
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
#include "BpfHandler.h"
using namespace android::bpf; // NOLINT(google-build-using-namespace): exempted
@@ -48,44 +49,36 @@
BpfHandler mBh;
BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
- BpfMap<uint32_t, uint8_t> mFakeConfigurationMap;
+ BpfMapRO<uint32_t, uint32_t> mFakeConfigurationMap;
BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
void SetUp() {
std::lock_guard guard(mBh.mMutex);
ASSERT_EQ(0, setrlimitForTest());
- mFakeCookieTagMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint64_t), sizeof(UidTagValue),
- TEST_MAP_SIZE, 0));
+ mFakeCookieTagMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
ASSERT_VALID(mFakeCookieTagMap);
- mFakeStatsMapA.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(StatsKey), sizeof(StatsValue),
- TEST_MAP_SIZE, 0));
+ mFakeStatsMapA.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
ASSERT_VALID(mFakeStatsMapA);
- mFakeConfigurationMap.reset(
- createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), 1, 0));
+ mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_ARRAY, CONFIGURATION_MAP_SIZE);
ASSERT_VALID(mFakeConfigurationMap);
- mFakeUidPermissionMap.reset(
- createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0));
+ mFakeUidPermissionMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
ASSERT_VALID(mFakeUidPermissionMap);
- mBh.mCookieTagMap.reset(dupFd(mFakeCookieTagMap.getMap()));
+ mBh.mCookieTagMap = mFakeCookieTagMap;
ASSERT_VALID(mBh.mCookieTagMap);
- mBh.mStatsMapA.reset(dupFd(mFakeStatsMapA.getMap()));
+ mBh.mStatsMapA = mFakeStatsMapA;
ASSERT_VALID(mBh.mStatsMapA);
- mBh.mConfigurationMap.reset(dupFd(mFakeConfigurationMap.getMap()));
+ mBh.mConfigurationMap = mFakeConfigurationMap;
ASSERT_VALID(mBh.mConfigurationMap);
// Always write to stats map A by default.
- ASSERT_RESULT_OK(mBh.mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY,
- SELECT_MAP_A, BPF_ANY));
- mBh.mUidPermissionMap.reset(dupFd(mFakeUidPermissionMap.getMap()));
- ASSERT_VALID(mBh.mUidPermissionMap);
- }
+ static_assert(SELECT_MAP_A == 0, "bpf map arrays are zero-initialized");
- int dupFd(const android::base::unique_fd& mapFd) {
- return fcntl(mapFd.get(), F_DUPFD_CLOEXEC, 0);
+ mBh.mUidPermissionMap = mFakeUidPermissionMap;
+ ASSERT_VALID(mBh.mUidPermissionMap);
}
int setUpSocketAndTag(int protocol, uint64_t* cookie, uint32_t tag, uid_t uid,
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index bf56fd5..5b3d314 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -48,7 +48,8 @@
cc_test {
name: "libnetworkstats_test",
- test_suites: ["general-tests"],
+ test_suites: ["general-tests", "mts-tethering"],
+ test_config_template: ":net_native_test_config_template",
require_root: true, // required by setrlimitForTest()
header_libs: ["bpf_connectivity_headers"],
srcs: [
@@ -68,4 +69,13 @@
"libbase",
"liblog",
],
+ compile_multilib: "both",
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
}
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index 4d605ce..6605428 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -58,13 +58,7 @@
}
int bpfGetUidStats(uid_t uid, Stats* stats) {
- BpfMapRO<uint32_t, StatsValue> appUidStatsMap(APP_UID_STATS_MAP_PATH);
-
- if (!appUidStatsMap.isValid()) {
- int ret = -errno;
- ALOGE("Opening appUidStatsMap(%s) failed: %s", APP_UID_STATS_MAP_PATH, strerror(errno));
- return ret;
- }
+ static BpfMapRO<uint32_t, StatsValue> appUidStatsMap(APP_UID_STATS_MAP_PATH);
return bpfGetUidStatsInternal(uid, stats, appUidStatsMap);
}
@@ -100,19 +94,8 @@
}
int bpfGetIfaceStats(const char* iface, Stats* stats) {
- BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
- int ret;
- if (!ifaceStatsMap.isValid()) {
- ret = -errno;
- ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
- return ret;
- }
- BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
- if (!ifaceIndexNameMap.isValid()) {
- ret = -errno;
- ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
- return ret;
- }
+ static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+ static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
return bpfGetIfaceStatsInternal(iface, stats, ifaceStatsMap, ifaceIndexNameMap);
}
@@ -186,32 +169,21 @@
int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines,
const std::vector<std::string>& limitIfaces, int limitTag,
int limitUid) {
- BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
- if (!ifaceIndexNameMap.isValid()) {
- int ret = -errno;
- ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
- return ret;
- }
-
- BpfMapRO<uint32_t, uint8_t> configurationMap(CONFIGURATION_MAP_PATH);
- if (!configurationMap.isValid()) {
- int ret = -errno;
- ALOGE("get configuration map fd failed: %s", strerror(errno));
- return ret;
- }
+ static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+ static BpfMapRO<uint32_t, uint32_t> configurationMap(CONFIGURATION_MAP_PATH);
auto configuration = configurationMap.readValue(CURRENT_STATS_MAP_CONFIGURATION_KEY);
if (!configuration.ok()) {
ALOGE("Cannot read the old configuration from map: %s",
configuration.error().message().c_str());
return -configuration.error().code();
}
- const char* statsMapPath = STATS_MAP_PATH[configuration.value()];
- BpfMap<StatsKey, StatsValue> statsMap(statsMapPath);
- if (!statsMap.isValid()) {
- int ret = -errno;
- ALOGE("get stats map fd failed: %s, path: %s", strerror(errno), statsMapPath);
- return ret;
+ if (configuration.value() != SELECT_MAP_A && configuration.value() != SELECT_MAP_B) {
+ ALOGE("%s unknown configuration value: %d", __func__, configuration.value());
+ return -EINVAL;
}
+ const char* statsMapPath = STATS_MAP_PATH[configuration.value()];
+ // TODO: fix this to not constantly reopen the bpf map
+ BpfMap<StatsKey, StatsValue> statsMap(statsMapPath);
// It is safe to read and clear the old map now since the
// networkStatsFactory should call netd to swap the map in advance already.
@@ -262,20 +234,8 @@
}
int parseBpfNetworkStatsDev(std::vector<stats_line>* lines) {
- int ret = 0;
- BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
- if (!ifaceIndexNameMap.isValid()) {
- ret = -errno;
- ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
- return ret;
- }
-
- BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
- if (!ifaceStatsMap.isValid()) {
- ret = -errno;
- ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
- return ret;
- }
+ static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+ static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
return parseBpfNetworkStatsDevInternal(lines, ifaceStatsMap, ifaceIndexNameMap);
}
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
index 4974b96..6f9c8c2 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -33,6 +33,7 @@
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
#include "bpf/BpfMap.h"
#include "bpf/BpfUtils.h"
#include "netdbpf/BpfNetworkStats.h"
@@ -80,19 +81,19 @@
ASSERT_EQ(0, setrlimitForTest());
mFakeCookieTagMap = BpfMap<uint64_t, UidTagValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
- ASSERT_LE(0, mFakeCookieTagMap.getMap());
+ ASSERT_TRUE(mFakeCookieTagMap.isValid());
mFakeAppUidStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
- ASSERT_LE(0, mFakeAppUidStatsMap.getMap());
+ ASSERT_TRUE(mFakeAppUidStatsMap.isValid());
mFakeStatsMap = BpfMap<StatsKey, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
- ASSERT_LE(0, mFakeStatsMap.getMap());
+ ASSERT_TRUE(mFakeStatsMap.isValid());
mFakeIfaceIndexNameMap = BpfMap<uint32_t, IfaceValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
- ASSERT_LE(0, mFakeIfaceIndexNameMap.getMap());
+ ASSERT_TRUE(mFakeIfaceIndexNameMap.isValid());
mFakeIfaceStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
- ASSERT_LE(0, mFakeIfaceStatsMap.getMap());
+ ASSERT_TRUE(mFakeIfaceStatsMap.isValid());
}
void expectUidTag(uint64_t cookie, uid_t uid, uint32_t tag) {
diff --git a/service-t/src/com/android/server/IpSecService.java b/service-t/src/com/android/server/IpSecService.java
index 4bc40ea..16b9f1e 100644
--- a/service-t/src/com/android/server/IpSecService.java
+++ b/service-t/src/com/android/server/IpSecService.java
@@ -1452,6 +1452,11 @@
final ConnectivityManager connectivityManager =
mContext.getSystemService(ConnectivityManager.class);
final LinkProperties lp = connectivityManager.getLinkProperties(underlyingNetwork);
+ if (lp == null) {
+ throw new IllegalArgumentException(
+ "LinkProperties is null. The underlyingNetwork may not be functional");
+ }
+
if (tunnelInterfaceInfo.getInterfaceName().equals(lp.getInterfaceName())) {
throw new IllegalArgumentException(
"Underlying network cannot be the network being exposed by this tunnel");
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 4086e4e..7115720 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -23,6 +23,7 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
+import android.net.INetd;
import android.net.LinkProperties;
import android.net.Network;
import android.net.mdns.aidl.DiscoveryInfo;
@@ -100,7 +101,6 @@
private class NsdStateMachine extends StateMachine {
private final DefaultState mDefaultState = new DefaultState();
- private final DisabledState mDisabledState = new DisabledState();
private final EnabledState mEnabledState = new EnabledState();
@Override
@@ -151,7 +151,6 @@
NsdStateMachine(String name, Handler handler) {
super(name, handler);
addState(mDefaultState);
- addState(mDisabledState, mDefaultState);
addState(mEnabledState, mDefaultState);
State initialState = mEnabledState;
setInitialState(initialState);
@@ -249,25 +248,6 @@
}
}
- class DisabledState extends State {
- @Override
- public void enter() {
- sendNsdStateChangeBroadcast(false);
- }
-
- @Override
- public boolean processMessage(Message msg) {
- switch (msg.what) {
- case NsdManager.ENABLE:
- transitionTo(mEnabledState);
- break;
- default:
- return NOT_HANDLED;
- }
- return HANDLED;
- }
- }
-
class EnabledState extends State {
@Override
public void enter() {
@@ -311,10 +291,6 @@
final int clientId = msg.arg2;
final ListenerArgs args;
switch (msg.what) {
- case NsdManager.DISABLE:
- //TODO: cleanup clients
- transitionTo(mDisabledState);
- break;
case NsdManager.DISCOVER_SERVICES:
if (DBG) Log.d(TAG, "Discover services");
args = (ListenerArgs) msg.obj;
@@ -466,7 +442,7 @@
// interfaces that do not have an associated Network.
break;
}
- servInfo.setNetwork(new Network(foundNetId));
+ setServiceNetworkForCallback(servInfo, info.netId, info.interfaceIdx);
clientInfo.onServiceFound(clientId, servInfo);
break;
}
@@ -476,10 +452,11 @@
final String type = info.registrationType;
final int lostNetId = info.netId;
servInfo = new NsdServiceInfo(name, type);
- // The network could be null if it was torn down when the service is lost
- // TODO: avoid returning null in that case, possibly by remembering found
- // services on the same interface index and their network at the time
- servInfo.setNetwork(lostNetId == 0 ? null : new Network(lostNetId));
+ // The network could be set to null (netId 0) if it was torn down when the
+ // service is lost
+ // TODO: avoid returning null in that case, possibly by remembering
+ // found services on the same interface index and their network at the time
+ setServiceNetworkForCallback(servInfo, lostNetId, info.interfaceIdx);
clientInfo.onServiceLost(clientId, servInfo);
break;
}
@@ -513,7 +490,7 @@
break;
}
- String name = fullName.substring(0, index);
+ String name = unescape(fullName.substring(0, index));
String rest = fullName.substring(index);
String type = rest.replace(".local.", "");
@@ -557,7 +534,6 @@
final GetAddressInfo info = (GetAddressInfo) obj;
final String address = info.address;
final int netId = info.netId;
- final Network network = netId == NETID_UNSET ? null : new Network(netId);
InetAddress serviceHost = null;
try {
serviceHost = InetAddress.getByName(address);
@@ -568,9 +544,10 @@
// If the resolved service is on an interface without a network, consider it
// as a failure: it would not be usable by apps as they would need
// privileged permissions.
- if (network != null && serviceHost != null) {
+ if (netId != NETID_UNSET && serviceHost != null) {
clientInfo.mResolvedService.setHost(serviceHost);
- clientInfo.mResolvedService.setNetwork(network);
+ setServiceNetworkForCallback(clientInfo.mResolvedService,
+ netId, info.interfaceIdx);
clientInfo.onResolveServiceSucceeded(
clientId, clientInfo.mResolvedService);
} else {
@@ -590,6 +567,55 @@
}
}
+ private static void setServiceNetworkForCallback(NsdServiceInfo info, int netId, int ifaceIdx) {
+ switch (netId) {
+ case NETID_UNSET:
+ info.setNetwork(null);
+ break;
+ case INetd.LOCAL_NET_ID:
+ // Special case for LOCAL_NET_ID: Networks on netId 99 are not generally
+ // visible / usable for apps, so do not return it. Store the interface
+ // index instead, so at least if the client tries to resolve the service
+ // with that NsdServiceInfo, it will be done on the same interface.
+ // If they recreate the NsdServiceInfo themselves, resolution would be
+ // done on all interfaces as before T, which should also work.
+ info.setNetwork(null);
+ info.setInterfaceIndex(ifaceIdx);
+ break;
+ default:
+ info.setNetwork(new Network(netId));
+ }
+ }
+
+ // The full service name is escaped from standard DNS rules on mdnsresponder, making it suitable
+ // for passing to standard system DNS APIs such as res_query() . Thus, make the service name
+ // unescape for getting right service address. See "Notes on DNS Name Escaping" on
+ // external/mdnsresponder/mDNSShared/dns_sd.h for more details.
+ private String unescape(String s) {
+ StringBuilder sb = new StringBuilder(s.length());
+ for (int i = 0; i < s.length(); ++i) {
+ char c = s.charAt(i);
+ if (c == '\\') {
+ if (++i >= s.length()) {
+ Log.e(TAG, "Unexpected end of escape sequence in: " + s);
+ break;
+ }
+ c = s.charAt(i);
+ if (c != '.' && c != '\\') {
+ if (i + 2 >= s.length()) {
+ Log.e(TAG, "Unexpected end of escape sequence in: " + s);
+ break;
+ }
+ c = (char) ((c - '0') * 100 + (s.charAt(i + 1) - '0') * 10
+ + (s.charAt(i + 2) - '0'));
+ i += 2;
+ }
+ }
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
@VisibleForTesting
NsdService(Context ctx, Handler handler, long cleanupDelayMs) {
mCleanupDelayMs = cleanupDelayMs;
@@ -738,7 +764,12 @@
String type = service.getServiceType();
int port = service.getPort();
byte[] textRecord = service.getTxtRecord();
- return mMDnsManager.registerService(regId, name, type, port, textRecord, IFACE_IDX_ANY);
+ final int registerInterface = getNetworkInterfaceIndex(service);
+ if (service.getNetwork() != null && registerInterface == IFACE_IDX_ANY) {
+ Log.e(TAG, "Interface to register service on not found");
+ return false;
+ }
+ return mMDnsManager.registerService(regId, name, type, port, textRecord, registerInterface);
}
private boolean unregisterService(int regId) {
@@ -746,10 +777,9 @@
}
private boolean discoverServices(int discoveryId, NsdServiceInfo serviceInfo) {
- final Network network = serviceInfo.getNetwork();
final String type = serviceInfo.getServiceType();
- final int discoverInterface = getNetworkInterfaceIndex(network);
- if (network != null && discoverInterface == IFACE_IDX_ANY) {
+ final int discoverInterface = getNetworkInterfaceIndex(serviceInfo);
+ if (serviceInfo.getNetwork() != null && discoverInterface == IFACE_IDX_ANY) {
Log.e(TAG, "Interface to discover service on not found");
return false;
}
@@ -763,9 +793,8 @@
private boolean resolveService(int resolveId, NsdServiceInfo service) {
final String name = service.getServiceName();
final String type = service.getServiceType();
- final Network network = service.getNetwork();
- final int resolveInterface = getNetworkInterfaceIndex(network);
- if (network != null && resolveInterface == IFACE_IDX_ANY) {
+ final int resolveInterface = getNetworkInterfaceIndex(service);
+ if (service.getNetwork() != null && resolveInterface == IFACE_IDX_ANY) {
Log.e(TAG, "Interface to resolve service on not found");
return false;
}
@@ -781,8 +810,17 @@
* this is to support the legacy mdnsresponder implementation, which historically resolved
* services on an unspecified network.
*/
- private int getNetworkInterfaceIndex(Network network) {
- if (network == null) return IFACE_IDX_ANY;
+ private int getNetworkInterfaceIndex(NsdServiceInfo serviceInfo) {
+ final Network network = serviceInfo.getNetwork();
+ if (network == null) {
+ // Fallback to getInterfaceIndex if present (typically if the NsdServiceInfo was
+ // provided by NsdService from discovery results, and the service was found on an
+ // interface that has no app-usable Network).
+ if (serviceInfo.getInterfaceIndex() != 0) {
+ return serviceInfo.getInterfaceIndex();
+ }
+ return IFACE_IDX_ANY;
+ }
final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
if (cm == null) {
diff --git a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
index 6b623f4..6006539 100644
--- a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
+++ b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
@@ -16,23 +16,37 @@
package com.android.server.ethernet;
+import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+
import android.annotation.Nullable;
+import android.content.ApexEnvironment;
import android.net.IpConfiguration;
import android.os.Environment;
import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.net.IpConfigStore;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
/**
* This class provides an API to store and manage Ethernet network configuration.
*/
public class EthernetConfigStore {
- private static final String ipConfigFile = Environment.getDataDirectory() +
- "/misc/ethernet/ipconfig.txt";
+ private static final String TAG = EthernetConfigStore.class.getSimpleName();
+ private static final String CONFIG_FILE = "ipconfig.txt";
+ private static final String FILE_PATH = "/misc/ethernet/";
+ private static final String LEGACY_IP_CONFIG_FILE_PATH = Environment.getDataDirectory()
+ + FILE_PATH;
+ private static final String APEX_IP_CONFIG_FILE_PATH = ApexEnvironment.getApexEnvironment(
+ TETHERING_MODULE_NAME).getDeviceProtectedDataDir() + FILE_PATH;
private IpConfigStore mStore = new IpConfigStore();
- private ArrayMap<String, IpConfiguration> mIpConfigurations;
+ private final ArrayMap<String, IpConfiguration> mIpConfigurations;
private IpConfiguration mIpConfigurationForDefaultInterface;
private final Object mSync = new Object();
@@ -40,22 +54,70 @@
mIpConfigurations = new ArrayMap<>(0);
}
- public void read() {
- synchronized (mSync) {
- ArrayMap<String, IpConfiguration> configs =
- IpConfigStore.readIpConfigurations(ipConfigFile);
+ private static boolean doesConfigFileExist(final String filepath) {
+ return new File(filepath).exists();
+ }
- // This configuration may exist in old file versions when there was only a single active
- // Ethernet interface.
- if (configs.containsKey("0")) {
- mIpConfigurationForDefaultInterface = configs.remove("0");
+ private void writeLegacyIpConfigToApexPath(final String newFilePath, final String oldFilePath,
+ final String filename) {
+ final File directory = new File(newFilePath);
+ if (!directory.exists()) {
+ directory.mkdirs();
+ }
+
+ // Write the legacy IP config to the apex file path.
+ FileOutputStream fos = null;
+ final AtomicFile dst = new AtomicFile(new File(newFilePath + filename));
+ final AtomicFile src = new AtomicFile(new File(oldFilePath + filename));
+ try {
+ final byte[] raw = src.readFully();
+ if (raw.length > 0) {
+ fos = dst.startWrite();
+ fos.write(raw);
+ fos.flush();
+ dst.finishWrite(fos);
}
-
- mIpConfigurations = configs;
+ } catch (IOException e) {
+ Log.e(TAG, "Fail to sync the legacy IP config to the apex file path.");
+ dst.failWrite(fos);
}
}
+ public void read() {
+ read(APEX_IP_CONFIG_FILE_PATH, LEGACY_IP_CONFIG_FILE_PATH, CONFIG_FILE);
+ }
+
+ @VisibleForTesting
+ void read(final String newFilePath, final String oldFilePath, final String filename) {
+ synchronized (mSync) {
+ // Attempt to read the IP configuration from apex file path first.
+ if (doesConfigFileExist(newFilePath + filename)) {
+ loadConfigFileLocked(newFilePath + filename);
+ return;
+ }
+
+ // If the config file doesn't exist in the apex file path, attempt to read it from
+ // the legacy file path, if config file exists, write the legacy IP configuration to
+ // apex config file path, this should just happen on the first boot. New or updated
+ // config entries are only written to the apex config file later.
+ if (!doesConfigFileExist(oldFilePath + filename)) return;
+ loadConfigFileLocked(oldFilePath + filename);
+ writeLegacyIpConfigToApexPath(newFilePath, oldFilePath, filename);
+ }
+ }
+
+ private void loadConfigFileLocked(final String filepath) {
+ final ArrayMap<String, IpConfiguration> configs =
+ IpConfigStore.readIpConfigurations(filepath);
+ mIpConfigurations.putAll(configs);
+ }
+
public void write(String iface, IpConfiguration config) {
+ write(iface, config, APEX_IP_CONFIG_FILE_PATH + CONFIG_FILE);
+ }
+
+ @VisibleForTesting
+ void write(String iface, IpConfiguration config, String filepath) {
boolean modified;
synchronized (mSync) {
@@ -67,7 +129,7 @@
}
if (modified) {
- mStore.writeIpConfigurations(ipConfigFile, mIpConfigurations);
+ mStore.writeIpConfigurations(filepath, mIpConfigurations);
}
}
}
@@ -80,9 +142,6 @@
@Nullable
public IpConfiguration getIpConfigurationForDefaultInterface() {
- synchronized (mSync) {
- return mIpConfigurationForDefaultInterface == null
- ? null : new IpConfiguration(mIpConfigurationForDefaultInterface);
- }
+ return null;
}
}
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index eb22f78..cedffe1 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -19,7 +19,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
-import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.ConnectivityResources;
import android.net.EthernetManager;
@@ -32,10 +31,9 @@
import android.net.LinkProperties;
import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
-import android.net.NetworkFactory;
import android.net.NetworkProvider;
import android.net.NetworkRequest;
-import android.net.NetworkSpecifier;
+import android.net.NetworkScore;
import android.net.ip.IIpClient;
import android.net.ip.IpClientCallbacks;
import android.net.ip.IpClientManager;
@@ -47,6 +45,7 @@
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.AndroidRuntimeException;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
@@ -57,27 +56,25 @@
import java.io.FileDescriptor;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
- * {@link NetworkFactory} that represents Ethernet networks.
+ * Class that manages NetworkOffers for Ethernet networks.
*
- * This class reports a static network score of 70 when it is tracking an interface and that
- * interface's link is up, and a score of 0 otherwise.
+ * TODO: this class should be merged into EthernetTracker.
*/
-public class EthernetNetworkFactory extends NetworkFactory {
+public class EthernetNetworkFactory {
private final static String TAG = EthernetNetworkFactory.class.getSimpleName();
final static boolean DBG = true;
- private final static int NETWORK_SCORE = 70;
private static final String NETWORK_TYPE = "Ethernet";
- private static final String LEGACY_TCP_BUFFER_SIZES =
- "524288,1048576,3145728,524288,1048576,2097152";
private final ConcurrentHashMap<String, NetworkInterfaceState> mTrackingInterfaces =
new ConcurrentHashMap<>();
private final Handler mHandler;
private final Context mContext;
+ private final NetworkProvider mProvider;
final Dependencies mDeps;
public static class Dependencies {
@@ -99,25 +96,9 @@
return InterfaceParams.getByName(name);
}
- // TODO: remove legacy resource fallback after migrating its overlays.
- private String getPlatformTcpBufferSizes(Context context) {
- final Resources r = context.getResources();
- final int resId = r.getIdentifier("config_ethernet_tcp_buffers", "string",
- context.getPackageName());
- return r.getString(resId);
- }
-
public String getTcpBufferSizesFromResource(Context context) {
- final String tcpBufferSizes;
- final String platformTcpBufferSizes = getPlatformTcpBufferSizes(context);
- if (!LEGACY_TCP_BUFFER_SIZES.equals(platformTcpBufferSizes)) {
- // Platform resource is not the historical default: use the overlay.
- tcpBufferSizes = platformTcpBufferSizes;
- } else {
- final ConnectivityResources resources = new ConnectivityResources(context);
- tcpBufferSizes = resources.get().getString(R.string.config_ethernet_tcp_buffers);
- }
- return tcpBufferSizes;
+ final ConnectivityResources resources = new ConnectivityResources(context);
+ return resources.get().getString(R.string.config_ethernet_tcp_buffers);
}
}
@@ -128,54 +109,24 @@
}
public EthernetNetworkFactory(Handler handler, Context context) {
- this(handler, context, new Dependencies());
+ this(handler, context, new NetworkProvider(context, handler.getLooper(), TAG),
+ new Dependencies());
}
@VisibleForTesting
- EthernetNetworkFactory(Handler handler, Context context, Dependencies deps) {
- super(handler.getLooper(), context, NETWORK_TYPE, createDefaultNetworkCapabilities());
-
+ EthernetNetworkFactory(Handler handler, Context context, NetworkProvider provider,
+ Dependencies deps) {
mHandler = handler;
mContext = context;
+ mProvider = provider;
mDeps = deps;
-
- setScoreFilter(NETWORK_SCORE);
}
- @Override
- public boolean acceptRequest(NetworkRequest request) {
- if (DBG) {
- Log.d(TAG, "acceptRequest, request: " + request);
- }
-
- return networkForRequest(request) != null;
- }
-
- @Override
- protected void needNetworkFor(NetworkRequest networkRequest) {
- NetworkInterfaceState network = networkForRequest(networkRequest);
-
- if (network == null) {
- Log.e(TAG, "needNetworkFor, failed to get a network for " + networkRequest);
- return;
- }
-
- if (++network.refCount == 1) {
- network.start();
- }
- }
-
- @Override
- protected void releaseNetworkFor(NetworkRequest networkRequest) {
- NetworkInterfaceState network = networkForRequest(networkRequest);
- if (network == null) {
- Log.e(TAG, "releaseNetworkFor, failed to get a network for " + networkRequest);
- return;
- }
-
- if (--network.refCount == 0) {
- network.stop();
- }
+ /**
+ * Registers the network provider with the system.
+ */
+ public void register() {
+ mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
}
/**
@@ -213,9 +164,8 @@
}
final NetworkInterfaceState iface = new NetworkInterfaceState(
- ifaceName, hwAddress, mHandler, mContext, ipConfig, nc, this, mDeps);
+ ifaceName, hwAddress, mHandler, mContext, ipConfig, nc, mProvider, mDeps);
mTrackingInterfaces.put(ifaceName, iface);
- updateCapabilityFilter();
}
@VisibleForTesting
@@ -256,7 +206,6 @@
final NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
iface.updateInterface(ipConfig, capabilities, listener);
mTrackingInterfaces.put(ifaceName, iface);
- updateCapabilityFilter();
}
private static NetworkCapabilities mixInCapabilities(NetworkCapabilities nc,
@@ -267,16 +216,6 @@
return builder.build();
}
- private void updateCapabilityFilter() {
- NetworkCapabilities capabilitiesFilter = createDefaultNetworkCapabilities();
- for (NetworkInterfaceState iface: mTrackingInterfaces.values()) {
- capabilitiesFilter = mixInCapabilities(capabilitiesFilter, iface.mCapabilities);
- }
-
- if (DBG) Log.d(TAG, "updateCapabilityFilter: " + capabilitiesFilter);
- setCapabilityFilter(capabilitiesFilter);
- }
-
private static NetworkCapabilities createDefaultNetworkCapabilities() {
return NetworkCapabilities.Builder
.withoutDefaultCapabilities()
@@ -284,14 +223,17 @@
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
- protected void removeInterface(String interfaceName) {
+ protected boolean removeInterface(String interfaceName) {
NetworkInterfaceState iface = mTrackingInterfaces.remove(interfaceName);
if (iface != null) {
- iface.maybeSendNetworkManagementCallbackForAbort();
- iface.stop();
+ iface.unregisterNetworkOfferAndStop();
+ return true;
}
-
- updateCapabilityFilter();
+ // TODO(b/236892130): if an interface is currently in server mode, it may not be properly
+ // removed.
+ // TODO: when false is returned, do not send a STATE_ABSENT callback.
+ Log.w(TAG, interfaceName + " is not tracked and cannot be removed");
+ return false;
}
/** Returns true if state has been modified */
@@ -323,37 +265,6 @@
return mTrackingInterfaces.containsKey(ifaceName);
}
- private NetworkInterfaceState networkForRequest(NetworkRequest request) {
- String requestedIface = null;
-
- NetworkSpecifier specifier = request.getNetworkSpecifier();
- if (specifier instanceof EthernetNetworkSpecifier) {
- requestedIface = ((EthernetNetworkSpecifier) specifier)
- .getInterfaceName();
- }
-
- NetworkInterfaceState network = null;
- if (!TextUtils.isEmpty(requestedIface)) {
- NetworkInterfaceState n = mTrackingInterfaces.get(requestedIface);
- if (n != null && request.canBeSatisfiedBy(n.mCapabilities)) {
- network = n;
- }
- } else {
- for (NetworkInterfaceState n : mTrackingInterfaces.values()) {
- if (request.canBeSatisfiedBy(n.mCapabilities) && n.mLinkUp) {
- network = n;
- break;
- }
- }
- }
-
- if (DBG) {
- Log.i(TAG, "networkForRequest, request: " + request + ", network: " + network);
- }
-
- return network;
- }
-
private static void maybeSendNetworkManagementCallback(
@Nullable final INetworkInterfaceOutcomeReceiver listener,
@Nullable final String iface,
@@ -380,14 +291,16 @@
private final String mHwAddress;
private final Handler mHandler;
private final Context mContext;
- private final NetworkFactory mNetworkFactory;
+ private final NetworkProvider mNetworkProvider;
private final Dependencies mDeps;
+ private final NetworkProvider.NetworkOfferCallback mNetworkOfferCallback;
private static String sTcpBufferSizes = null; // Lazy initialized.
private boolean mLinkUp;
private int mLegacyType;
private LinkProperties mLinkProperties = new LinkProperties();
+ private final Set<Integer> mRequestIds = new ArraySet<>();
private volatile @Nullable IpClientManager mIpClient;
private @NonNull NetworkCapabilities mCapabilities;
@@ -416,8 +329,6 @@
ConnectivityManager.TYPE_NONE);
}
- long refCount = 0;
-
private class EthernetIpClientCallback extends IpClientCallbacks {
private final ConditionVariable mIpClientStartCv = new ConditionVariable(false);
private final ConditionVariable mIpClientShutdownCv = new ConditionVariable(false);
@@ -488,17 +399,51 @@
}
}
+ private class EthernetNetworkOfferCallback implements NetworkProvider.NetworkOfferCallback {
+ @Override
+ public void onNetworkNeeded(@NonNull NetworkRequest request) {
+ if (DBG) {
+ Log.d(TAG, String.format("%s: onNetworkNeeded for request: %s", name, request));
+ }
+ // When the network offer is first registered, onNetworkNeeded is called with all
+ // existing requests.
+ // ConnectivityService filters requests for us based on the NetworkCapabilities
+ // passed in the registerNetworkOffer() call.
+ mRequestIds.add(request.requestId);
+ // if the network is already started, this is a no-op.
+ start();
+ }
+
+ @Override
+ public void onNetworkUnneeded(@NonNull NetworkRequest request) {
+ if (DBG) {
+ Log.d(TAG,
+ String.format("%s: onNetworkUnneeded for request: %s", name, request));
+ }
+ if (!mRequestIds.remove(request.requestId)) {
+ // This can only happen if onNetworkNeeded was not called for a request or if
+ // the requestId changed. Both should *never* happen.
+ Log.wtf(TAG, "onNetworkUnneeded called for unknown request");
+ }
+ if (mRequestIds.isEmpty()) {
+ // not currently serving any requests, stop the network.
+ stop();
+ }
+ }
+ }
+
NetworkInterfaceState(String ifaceName, String hwAddress, Handler handler, Context context,
@NonNull IpConfiguration ipConfig, @NonNull NetworkCapabilities capabilities,
- NetworkFactory networkFactory, Dependencies deps) {
+ NetworkProvider networkProvider, Dependencies deps) {
name = ifaceName;
mIpConfig = Objects.requireNonNull(ipConfig);
mCapabilities = Objects.requireNonNull(capabilities);
mLegacyType = getLegacyType(mCapabilities);
mHandler = handler;
mContext = context;
- mNetworkFactory = networkFactory;
+ mNetworkProvider = networkProvider;
mDeps = deps;
+ mNetworkOfferCallback = new EthernetNetworkOfferCallback();
mHwAddress = hwAddress;
}
@@ -521,9 +466,19 @@
+ "transport type.");
}
+ private static NetworkScore getNetworkScore() {
+ return new NetworkScore.Builder().build();
+ }
+
private void setCapabilities(@NonNull final NetworkCapabilities capabilities) {
mCapabilities = new NetworkCapabilities(capabilities);
mLegacyType = getLegacyType(mCapabilities);
+
+ if (mLinkUp) {
+ // registering a new network offer will update the existing one, not install a
+ // new one.
+ registerNetworkOffer();
+ }
}
void updateInterface(@Nullable final IpConfiguration ipConfig,
@@ -543,8 +498,6 @@
if (null != capabilities) {
setCapabilities(capabilities);
}
- // Send an abort callback if a request is filed before the previous one has completed.
- maybeSendNetworkManagementCallbackForAbort();
// TODO: Update this logic to only do a restart if required. Although a restart may
// be required due to the capabilities or ipConfiguration values, not all
// capabilities changes require a restart.
@@ -594,7 +547,7 @@
.setLegacyExtraInfo(mHwAddress)
.build();
mNetworkAgent = mDeps.makeEthernetNetworkAgent(mContext, mHandler.getLooper(),
- mCapabilities, mLinkProperties, config, mNetworkFactory.getProvider(),
+ mCapabilities, mLinkProperties, config, mNetworkProvider,
new EthernetNetworkAgent.Callbacks() {
@Override
public void onNetworkUnwanted() {
@@ -685,26 +638,27 @@
mLinkUp = up;
if (!up) { // was up, goes down
- // Send an abort on a provisioning request callback if necessary before stopping.
- maybeSendNetworkManagementCallbackForAbort();
- stop();
+ // retract network offer and stop IpClient.
+ unregisterNetworkOfferAndStop();
// If only setting the interface down, send a callback to signal completion.
EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null);
} else { // was down, goes up
- stop();
- start(listener);
+ // register network offer
+ registerNetworkOffer();
}
return true;
}
- void stop() {
+ private void stop() {
// Invalidate all previous start requests
if (mIpClient != null) {
mIpClient.shutdown();
mIpClientCallback.awaitIpClientShutdown();
mIpClient = null;
}
+ // Send an abort callback if an updateInterface request was in progress.
+ maybeSendNetworkManagementCallbackForAbort();
mIpClientCallback = null;
if (mNetworkAgent != null) {
@@ -714,6 +668,18 @@
mLinkProperties.clear();
}
+ private void registerNetworkOffer() {
+ mNetworkProvider.registerNetworkOffer(getNetworkScore(),
+ new NetworkCapabilities(mCapabilities), cmd -> mHandler.post(cmd),
+ mNetworkOfferCallback);
+ }
+
+ public void unregisterNetworkOfferAndStop() {
+ mNetworkProvider.unregisterNetworkOffer(mNetworkOfferCallback);
+ stop();
+ mRequestIds.clear();
+ }
+
private static void provisionIpClient(@NonNull final IpClientManager ipClient,
@NonNull final IpConfiguration config, @NonNull final String tcpBufferSizes) {
if (config.getProxySettings() == ProxySettings.STATIC ||
@@ -753,7 +719,6 @@
@Override
public String toString() {
return getClass().getSimpleName() + "{ "
- + "refCount: " + refCount + ", "
+ "iface: " + name + ", "
+ "up: " + mLinkUp + ", "
+ "hwAddress: " + mHwAddress + ", "
@@ -766,7 +731,6 @@
}
void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
- super.dump(fd, pw, args);
pw.println(getClass().getSimpleName());
pw.println("Tracking interfaces:");
pw.increaseIndent();
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index 5e830ad..f058f94 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -16,19 +16,22 @@
package com.android.server.ethernet;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.net.EthernetNetworkSpecifier;
+import android.net.EthernetNetworkUpdateRequest;
import android.net.IEthernetManager;
import android.net.IEthernetServiceListener;
import android.net.INetworkInterfaceOutcomeReceiver;
import android.net.ITetheredInterfaceCallback;
-import android.net.EthernetNetworkUpdateRequest;
import android.net.IpConfiguration;
import android.net.NetworkCapabilities;
+import android.net.NetworkSpecifier;
import android.os.Binder;
import android.os.Handler;
import android.os.RemoteException;
@@ -216,19 +219,39 @@
"EthernetServiceImpl");
}
- private void maybeValidateTestCapabilities(final String iface,
- @Nullable final NetworkCapabilities nc) {
+ private void validateOrSetNetworkSpecifier(String iface, NetworkCapabilities nc) {
+ final NetworkSpecifier spec = nc.getNetworkSpecifier();
+ if (spec == null) {
+ nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));
+ return;
+ }
+ if (!(spec instanceof EthernetNetworkSpecifier)) {
+ throw new IllegalArgumentException("Invalid specifier type for request.");
+ }
+ if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {
+ throw new IllegalArgumentException("Invalid interface name set on specifier.");
+ }
+ }
+
+ private void maybeValidateTestCapabilities(String iface, NetworkCapabilities nc) {
if (!mTracker.isValidTestInterface(iface)) {
return;
}
- // For test interfaces, only null or capabilities that include TRANSPORT_TEST are
- // allowed.
- if (nc != null && !nc.hasTransport(TRANSPORT_TEST)) {
+ if (!nc.hasTransport(TRANSPORT_TEST)) {
throw new IllegalArgumentException(
"Updates to test interfaces must have NetworkCapabilities.TRANSPORT_TEST.");
}
}
+ private void maybeValidateEthernetTransport(String iface, NetworkCapabilities nc) {
+ if (mTracker.isValidTestInterface(iface)) {
+ return;
+ }
+ if (!nc.hasSingleTransport(TRANSPORT_ETHERNET)) {
+ throw new IllegalArgumentException("Invalid transport type for request.");
+ }
+ }
+
private void enforceAdminPermission(final String iface, boolean enforceAutomotive,
final String logMessage) {
if (mTracker.isValidTestInterface(iface)) {
@@ -251,36 +274,41 @@
// TODO: validate that iface is listed in overlay config_ethernet_interfaces
// only automotive devices are allowed to set the NetworkCapabilities using this API
- enforceAdminPermission(iface, request.getNetworkCapabilities() != null,
- "updateConfiguration() with non-null capabilities");
- maybeValidateTestCapabilities(iface, request.getNetworkCapabilities());
+ final NetworkCapabilities nc = request.getNetworkCapabilities();
+ enforceAdminPermission(
+ iface, nc != null, "updateConfiguration() with non-null capabilities");
+ if (nc != null) {
+ validateOrSetNetworkSpecifier(iface, nc);
+ maybeValidateTestCapabilities(iface, nc);
+ maybeValidateEthernetTransport(iface, nc);
+ }
mTracker.updateConfiguration(
- iface, request.getIpConfiguration(), request.getNetworkCapabilities(), listener);
+ iface, request.getIpConfiguration(), nc, listener);
}
@Override
- public void connectNetwork(@NonNull final String iface,
+ public void enableInterface(@NonNull final String iface,
@Nullable final INetworkInterfaceOutcomeReceiver listener) {
- Log.i(TAG, "connectNetwork called with: iface=" + iface + ", listener=" + listener);
+ Log.i(TAG, "enableInterface called with: iface=" + iface + ", listener=" + listener);
Objects.requireNonNull(iface);
throwIfEthernetNotStarted();
- enforceAdminPermission(iface, true, "connectNetwork()");
+ enforceAdminPermission(iface, false, "enableInterface()");
- mTracker.connectNetwork(iface, listener);
+ mTracker.enableInterface(iface, listener);
}
@Override
- public void disconnectNetwork(@NonNull final String iface,
+ public void disableInterface(@NonNull final String iface,
@Nullable final INetworkInterfaceOutcomeReceiver listener) {
- Log.i(TAG, "disconnectNetwork called with: iface=" + iface + ", listener=" + listener);
+ Log.i(TAG, "disableInterface called with: iface=" + iface + ", listener=" + listener);
Objects.requireNonNull(iface);
throwIfEthernetNotStarted();
- enforceAdminPermission(iface, true, "connectNetwork()");
+ enforceAdminPermission(iface, false, "disableInterface()");
- mTracker.disconnectNetwork(iface, listener);
+ mTracker.disableInterface(iface, listener);
}
@Override
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 693d91a..3e71093 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -25,7 +25,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
-import android.content.res.Resources;
import android.net.ConnectivityResources;
import android.net.EthernetManager;
import android.net.IEthernetServiceListener;
@@ -86,7 +85,6 @@
private static final boolean DBG = EthernetNetworkFactory.DBG;
private static final String TEST_IFACE_REGEXP = TEST_TAP_PREFIX + "\\d+";
- private static final String LEGACY_IFACE_REGEXP = "eth\\d";
/**
* Interface names we track. This is a product-dependent regular expression, plus,
@@ -134,48 +132,16 @@
}
public static class Dependencies {
- // TODO: remove legacy resource fallback after migrating its overlays.
- private String getPlatformRegexResource(Context context) {
- final Resources r = context.getResources();
- final int resId =
- r.getIdentifier("config_ethernet_iface_regex", "string", context.getPackageName());
- return r.getString(resId);
- }
-
- // TODO: remove legacy resource fallback after migrating its overlays.
- private String[] getPlatformInterfaceConfigs(Context context) {
- final Resources r = context.getResources();
- final int resId = r.getIdentifier("config_ethernet_interfaces", "array",
- context.getPackageName());
- return r.getStringArray(resId);
- }
-
public String getInterfaceRegexFromResource(Context context) {
- final String platformRegex = getPlatformRegexResource(context);
- final String match;
- if (!LEGACY_IFACE_REGEXP.equals(platformRegex)) {
- // Platform resource is not the historical default: use the overlay
- match = platformRegex;
- } else {
- final ConnectivityResources resources = new ConnectivityResources(context);
- match = resources.get().getString(
- com.android.connectivity.resources.R.string.config_ethernet_iface_regex);
- }
- return match;
+ final ConnectivityResources resources = new ConnectivityResources(context);
+ return resources.get().getString(
+ com.android.connectivity.resources.R.string.config_ethernet_iface_regex);
}
public String[] getInterfaceConfigFromResource(Context context) {
- final String[] platformInterfaceConfigs = getPlatformInterfaceConfigs(context);
- final String[] interfaceConfigs;
- if (platformInterfaceConfigs.length != 0) {
- // Platform resource is not the historical default: use the overlay
- interfaceConfigs = platformInterfaceConfigs;
- } else {
- final ConnectivityResources resources = new ConnectivityResources(context);
- interfaceConfigs = resources.get().getStringArray(
- com.android.connectivity.resources.R.array.config_ethernet_interfaces);
- }
- return interfaceConfigs;
+ final ConnectivityResources resources = new ConnectivityResources(context);
+ return resources.get().getStringArray(
+ com.android.connectivity.resources.R.array.config_ethernet_interfaces);
}
}
@@ -262,7 +228,7 @@
*/
protected void broadcastInterfaceStateChange(@NonNull String iface) {
ensureRunningOnEthernetServiceThread();
- final int state = mFactory.getInterfaceState(iface);
+ final int state = getInterfaceState(iface);
final int role = getInterfaceRole(iface);
final IpConfiguration config = getIpConfigurationForCallback(iface, state);
final int n = mListeners.beginBroadcast();
@@ -303,6 +269,8 @@
+ ", ipConfig: " + ipConfig);
}
+ // TODO: do the right thing if the interface was in server mode: either fail this operation,
+ // or take the interface out of server mode.
final IpConfiguration localIpConfig = ipConfig == null
? null : new IpConfiguration(ipConfig);
if (ipConfig != null) {
@@ -319,13 +287,13 @@
}
@VisibleForTesting(visibility = PACKAGE)
- protected void connectNetwork(@NonNull final String iface,
+ protected void enableInterface(@NonNull final String iface,
@Nullable final INetworkInterfaceOutcomeReceiver listener) {
mHandler.post(() -> updateInterfaceState(iface, true, listener));
}
@VisibleForTesting(visibility = PACKAGE)
- protected void disconnectNetwork(@NonNull final String iface,
+ protected void disableInterface(@NonNull final String iface,
@Nullable final INetworkInterfaceOutcomeReceiver listener) {
mHandler.post(() -> updateInterfaceState(iface, false, listener));
}
@@ -469,15 +437,34 @@
if (mDefaultInterface != null) {
removeInterface(mDefaultInterface);
addInterface(mDefaultInterface);
+ // when this broadcast is sent, any calls to notifyTetheredInterfaceAvailable or
+ // notifyTetheredInterfaceUnavailable have already happened
+ broadcastInterfaceStateChange(mDefaultInterface);
}
}
+ private int getInterfaceState(final String iface) {
+ if (mFactory.hasInterface(iface)) {
+ return mFactory.getInterfaceState(iface);
+ }
+ if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
+ // server mode interfaces are not tracked by the factory.
+ // TODO(b/234743836): interface state for server mode interfaces is not tracked
+ // properly; just return link up.
+ return EthernetManager.STATE_LINK_UP;
+ }
+ return EthernetManager.STATE_ABSENT;
+ }
+
private int getInterfaceRole(final String iface) {
- if (!mFactory.hasInterface(iface)) return EthernetManager.ROLE_NONE;
- final int mode = getInterfaceMode(iface);
- return (mode == INTERFACE_MODE_CLIENT)
- ? EthernetManager.ROLE_CLIENT
- : EthernetManager.ROLE_SERVER;
+ if (mFactory.hasInterface(iface)) {
+ // only client mode interfaces are tracked by the factory.
+ return EthernetManager.ROLE_CLIENT;
+ }
+ if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
+ return EthernetManager.ROLE_SERVER;
+ }
+ return EthernetManager.ROLE_NONE;
}
private int getInterfaceMode(final String iface) {
@@ -594,8 +581,8 @@
}
if (DBG) Log.i(TAG, "maybeTrackInterface: " + iface);
- // TODO: avoid making an interface default if it has configured NetworkCapabilities.
- if (mDefaultInterface == null) {
+ // Do not make an interface default if it has configured NetworkCapabilities.
+ if (mDefaultInterface == null && !mNetworkCapabilities.containsKey(iface)) {
mDefaultInterface = iface;
}
@@ -620,14 +607,18 @@
}
}
- private class InterfaceObserver extends BaseNetdUnsolicitedEventListener {
+ @VisibleForTesting
+ class InterfaceObserver extends BaseNetdUnsolicitedEventListener {
@Override
public void onInterfaceLinkStateChanged(String iface, boolean up) {
if (DBG) {
Log.i(TAG, "interfaceLinkStateChanged, iface: " + iface + ", up: " + up);
}
- mHandler.post(() -> updateInterfaceState(iface, up));
+ mHandler.post(() -> {
+ if (mEthernetState == ETHERNET_STATE_DISABLED) return;
+ updateInterfaceState(iface, up);
+ });
}
@Override
@@ -635,7 +626,10 @@
if (DBG) {
Log.i(TAG, "onInterfaceAdded, iface: " + iface);
}
- mHandler.post(() -> maybeTrackInterface(iface));
+ mHandler.post(() -> {
+ if (mEthernetState == ETHERNET_STATE_DISABLED) return;
+ maybeTrackInterface(iface);
+ });
}
@Override
@@ -643,7 +637,10 @@
if (DBG) {
Log.i(TAG, "onInterfaceRemoved, iface: " + iface);
}
- mHandler.post(() -> stopTrackingInterface(iface));
+ mHandler.post(() -> {
+ if (mEthernetState == ETHERNET_STATE_DISABLED) return;
+ stopTrackingInterface(iface);
+ });
}
}
@@ -922,6 +919,8 @@
void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
postAndWaitForRunnable(() -> {
pw.println(getClass().getSimpleName());
+ pw.println("Ethernet State: "
+ + (mEthernetState == ETHERNET_STATE_ENABLED ? "enabled" : "disabled"));
pw.println("Ethernet interface name filter: " + mIfaceMatch);
pw.println("Default interface: " + mDefaultInterface);
pw.println("Default interface mode: " + mDefaultInterfaceMode);
diff --git a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
index 25c88eb..3b44d81 100644
--- a/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
+++ b/service-t/src/com/android/server/net/BpfInterfaceMapUpdater.java
@@ -38,7 +38,7 @@
private static final String TAG = BpfInterfaceMapUpdater.class.getSimpleName();
// This is current path but may be changed soon.
private static final String IFACE_INDEX_NAME_MAP_PATH =
- "/sys/fs/bpf/map_netd_iface_index_name_map";
+ "/sys/fs/bpf/netd_shared/map_netd_iface_index_name_map";
private final IBpfMap<U32, InterfaceMapValue> mBpfMap;
private final INetd mNetd;
private final Handler mHandler;
diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java
index 3b93f1a..b628251 100644
--- a/service-t/src/com/android/server/net/NetworkStatsFactory.java
+++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java
@@ -164,16 +164,17 @@
}
public NetworkStatsFactory(@NonNull Context ctx) {
- this(ctx, new File("/proc/"), true);
+ this(ctx, new File("/proc/"), true, new BpfNetMaps());
}
@VisibleForTesting
- public NetworkStatsFactory(@NonNull Context ctx, File procRoot, boolean useBpfStats) {
+ public NetworkStatsFactory(@NonNull Context ctx, File procRoot, boolean useBpfStats,
+ BpfNetMaps bpfNetMaps) {
mStatsXtIfaceAll = new File(procRoot, "net/xt_qtaguid/iface_stat_all");
mStatsXtIfaceFmt = new File(procRoot, "net/xt_qtaguid/iface_stat_fmt");
mStatsXtUid = new File(procRoot, "net/xt_qtaguid/stats");
mUseBpfStats = useBpfStats;
- mBpfNetMaps = new BpfNetMaps();
+ mBpfNetMaps = bpfNetMaps;
synchronized (mPersistentDataLock) {
mPersistSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), -1);
mTunAnd464xlatAdjustedStats = new NetworkStats(SystemClock.elapsedRealtime(), -1);
diff --git a/service-t/src/com/android/server/net/NetworkStatsObservers.java b/service-t/src/com/android/server/net/NetworkStatsObservers.java
index fdfc893..1cd670a 100644
--- a/service-t/src/com/android/server/net/NetworkStatsObservers.java
+++ b/service-t/src/com/android/server/net/NetworkStatsObservers.java
@@ -18,6 +18,7 @@
import static android.app.usage.NetworkStatsManager.MIN_THRESHOLD_BYTES;
+import android.annotation.NonNull;
import android.app.usage.NetworkStatsManager;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -38,10 +39,12 @@
import android.os.Process;
import android.os.RemoteException;
import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.PerUidCounter;
import java.util.concurrent.atomic.AtomicInteger;
@@ -52,16 +55,26 @@
*/
class NetworkStatsObservers {
private static final String TAG = "NetworkStatsObservers";
+ private static final boolean LOG = true;
private static final boolean LOGV = false;
private static final int MSG_REGISTER = 1;
private static final int MSG_UNREGISTER = 2;
private static final int MSG_UPDATE_STATS = 3;
+ private static final int DUMP_USAGE_REQUESTS_COUNT = 200;
+
+ // The maximum number of request allowed per uid before an exception is thrown.
+ @VisibleForTesting
+ static final int MAX_REQUESTS_PER_UID = 100;
+
// All access to this map must be done from the handler thread.
// indexed by DataUsageRequest#requestId
private final SparseArray<RequestInfo> mDataUsageRequests = new SparseArray<>();
+ // Request counters per uid, this is thread safe.
+ private final PerUidCounter mDataUsageRequestsPerUid = new PerUidCounter(MAX_REQUESTS_PER_UID);
+
// Sequence number of DataUsageRequests
private final AtomicInteger mNextDataUsageRequestId = new AtomicInteger();
@@ -77,13 +90,16 @@
*
* @return the normalized request wrapped within {@link RequestInfo}.
*/
- public DataUsageRequest register(Context context, DataUsageRequest inputRequest,
- IUsageCallback callback, int callingUid, @NetworkStatsAccess.Level int accessLevel) {
+ public DataUsageRequest register(@NonNull Context context,
+ @NonNull DataUsageRequest inputRequest, @NonNull IUsageCallback callback,
+ int callingPid, int callingUid, @NonNull String callingPackage,
+ @NetworkStatsAccess.Level int accessLevel) {
DataUsageRequest request = buildRequest(context, inputRequest, callingUid);
- RequestInfo requestInfo = buildRequestInfo(request, callback, callingUid,
- accessLevel);
+ RequestInfo requestInfo = buildRequestInfo(request, callback, callingPid, callingUid,
+ callingPackage, accessLevel);
+ if (LOG) Log.d(TAG, "Registering observer for " + requestInfo);
+ mDataUsageRequestsPerUid.incrementCountOrThrow(callingUid);
- if (LOGV) Log.v(TAG, "Registering observer for " + request);
getHandler().sendMessage(mHandler.obtainMessage(MSG_REGISTER, requestInfo));
return request;
}
@@ -172,7 +188,7 @@
RequestInfo requestInfo;
requestInfo = mDataUsageRequests.get(request.requestId);
if (requestInfo == null) {
- if (LOGV) Log.v(TAG, "Trying to unregister unknown request " + request);
+ if (LOG) Log.d(TAG, "Trying to unregister unknown request " + request);
return;
}
if (Process.SYSTEM_UID != callingUid && requestInfo.mCallingUid != callingUid) {
@@ -180,8 +196,9 @@
return;
}
- if (LOGV) Log.v(TAG, "Unregistering " + request);
+ if (LOG) Log.d(TAG, "Unregistering " + requestInfo);
mDataUsageRequests.remove(request.requestId);
+ mDataUsageRequestsPerUid.decrementCountOrThrow(requestInfo.mCallingUid);
requestInfo.unlinkDeathRecipient();
requestInfo.callCallback(NetworkStatsManager.CALLBACK_RELEASED);
}
@@ -214,18 +231,19 @@
}
private RequestInfo buildRequestInfo(DataUsageRequest request, IUsageCallback callback,
- int callingUid, @NetworkStatsAccess.Level int accessLevel) {
+ int callingPid, int callingUid, @NonNull String callingPackage,
+ @NetworkStatsAccess.Level int accessLevel) {
if (accessLevel <= NetworkStatsAccess.Level.USER) {
- return new UserUsageRequestInfo(this, request, callback, callingUid,
- accessLevel);
+ return new UserUsageRequestInfo(this, request, callback, callingPid,
+ callingUid, callingPackage, accessLevel);
} else {
// Safety check in case a new access level is added and we forgot to update this
if (accessLevel < NetworkStatsAccess.Level.DEVICESUMMARY) {
throw new IllegalArgumentException(
"accessLevel " + accessLevel + " is less than DEVICESUMMARY.");
}
- return new NetworkUsageRequestInfo(this, request, callback, callingUid,
- accessLevel);
+ return new NetworkUsageRequestInfo(this, request, callback, callingPid,
+ callingUid, callingPackage, accessLevel);
}
}
@@ -237,18 +255,22 @@
private final NetworkStatsObservers mStatsObserver;
protected final DataUsageRequest mRequest;
private final IUsageCallback mCallback;
+ protected final int mCallingPid;
protected final int mCallingUid;
+ protected final String mCallingPackage;
protected final @NetworkStatsAccess.Level int mAccessLevel;
protected NetworkStatsRecorder mRecorder;
protected NetworkStatsCollection mCollection;
RequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
- IUsageCallback callback, int callingUid,
- @NetworkStatsAccess.Level int accessLevel) {
+ IUsageCallback callback, int callingPid, int callingUid,
+ @NonNull String callingPackage, @NetworkStatsAccess.Level int accessLevel) {
mStatsObserver = statsObserver;
mRequest = request;
mCallback = callback;
+ mCallingPid = callingPid;
mCallingUid = callingUid;
+ mCallingPackage = callingPackage;
mAccessLevel = accessLevel;
try {
@@ -269,7 +291,8 @@
@Override
public String toString() {
- return "RequestInfo from uid:" + mCallingUid
+ return "RequestInfo from pid/uid:" + mCallingPid + "/" + mCallingUid
+ + "(" + mCallingPackage + ")"
+ " for " + mRequest + " accessLevel:" + mAccessLevel;
}
@@ -338,9 +361,10 @@
private static class NetworkUsageRequestInfo extends RequestInfo {
NetworkUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
- IUsageCallback callback, int callingUid,
- @NetworkStatsAccess.Level int accessLevel) {
- super(statsObserver, request, callback, callingUid, accessLevel);
+ IUsageCallback callback, int callingPid, int callingUid,
+ @NonNull String callingPackage, @NetworkStatsAccess.Level int accessLevel) {
+ super(statsObserver, request, callback, callingPid, callingUid, callingPackage,
+ accessLevel);
}
@Override
@@ -380,9 +404,10 @@
private static class UserUsageRequestInfo extends RequestInfo {
UserUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
- IUsageCallback callback, int callingUid,
- @NetworkStatsAccess.Level int accessLevel) {
- super(statsObserver, request, callback, callingUid, accessLevel);
+ IUsageCallback callback, int callingPid, int callingUid,
+ @NonNull String callingPackage, @NetworkStatsAccess.Level int accessLevel) {
+ super(statsObserver, request, callback, callingPid, callingUid,
+ callingPackage, accessLevel);
}
@Override
@@ -448,4 +473,10 @@
mCurrentTime = currentTime;
}
}
+
+ public void dump(IndentingPrintWriter pw) {
+ for (int i = 0; i < Math.min(mDataUsageRequests.size(), DUMP_USAGE_REQUESTS_COUNT); i++) {
+ pw.println(mDataUsageRequests.valueAt(i));
+ }
+ }
}
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index f62765d..3da1585 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -21,6 +21,7 @@
import static android.net.TrafficStats.MB_IN_BYTES;
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
+import android.annotation.NonNull;
import android.net.NetworkIdentitySet;
import android.net.NetworkStats;
import android.net.NetworkStats.NonMonotonicObserver;
@@ -42,7 +43,6 @@
import libcore.io.IoUtils;
import java.io.ByteArrayOutputStream;
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -68,7 +68,7 @@
private static final String TAG_NETSTATS_DUMP = "netstats_dump";
- /** Dump before deleting in {@link #recoverFromWtf()}. */
+ /** Dump before deleting in {@link #recoverAndDeleteData()}. */
private static final boolean DUMP_BEFORE_DELETE = true;
private final FileRotator mRotator;
@@ -78,6 +78,7 @@
private final long mBucketDuration;
private final boolean mOnlyTags;
+ private final boolean mWipeOnError;
private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
private NetworkStats mLastSnapshot;
@@ -102,6 +103,7 @@
// slack to avoid overflow
mBucketDuration = YEAR_IN_MILLIS;
mOnlyTags = false;
+ mWipeOnError = true;
mPending = null;
mSinceBoot = new NetworkStatsCollection(mBucketDuration);
@@ -113,7 +115,8 @@
* Persisted recorder.
*/
public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
- DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags) {
+ DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags,
+ boolean wipeOnError) {
mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
@@ -121,6 +124,7 @@
mBucketDuration = bucketDuration;
mOnlyTags = onlyTags;
+ mWipeOnError = wipeOnError;
mPending = new NetworkStatsCollection(bucketDuration);
mSinceBoot = new NetworkStatsCollection(bucketDuration);
@@ -156,6 +160,15 @@
return mSinceBoot;
}
+ public long getBucketDuration() {
+ return mBucketDuration;
+ }
+
+ @NonNull
+ public String getCookie() {
+ return mCookie;
+ }
+
/**
* Load complete history represented by {@link FileRotator}. Caches
* internally as a {@link WeakReference}, and updated with future
@@ -189,10 +202,10 @@
res.recordCollection(mPending);
} catch (IOException e) {
Log.wtf(TAG, "problem completely reading network stats", e);
- recoverFromWtf();
+ recoverAndDeleteData();
} catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem completely reading network stats", e);
- recoverFromWtf();
+ recoverAndDeleteData();
}
return res;
}
@@ -300,10 +313,10 @@
mPending.reset();
} catch (IOException e) {
Log.wtf(TAG, "problem persisting pending stats", e);
- recoverFromWtf();
+ recoverAndDeleteData();
} catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem persisting pending stats", e);
- recoverFromWtf();
+ recoverAndDeleteData();
}
}
}
@@ -319,10 +332,10 @@
mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids));
} catch (IOException e) {
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
- recoverFromWtf();
+ recoverAndDeleteData();
} catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
- recoverFromWtf();
+ recoverAndDeleteData();
}
}
@@ -347,8 +360,7 @@
/**
* Rewriter that will combine current {@link NetworkStatsCollection} values
- * with anything read from disk, and write combined set to disk. Clears the
- * original {@link NetworkStatsCollection} when finished writing.
+ * with anything read from disk, and write combined set to disk.
*/
private static class CombiningRewriter implements FileRotator.Rewriter {
private final NetworkStatsCollection mCollection;
@@ -375,7 +387,6 @@
@Override
public void write(OutputStream out) throws IOException {
mCollection.write(out);
- mCollection.reset();
}
}
@@ -415,43 +426,87 @@
}
}
- public void importLegacyNetworkLocked(File file) throws IOException {
- Objects.requireNonNull(mRotator, "missing FileRotator");
+ /**
+ * Import a specified {@link NetworkStatsCollection} instance into this recorder,
+ * and write it into a standalone file.
+ * @param collection The target {@link NetworkStatsCollection} instance to be imported.
+ */
+ public void importCollectionLocked(@NonNull NetworkStatsCollection collection)
+ throws IOException {
+ if (mRotator != null) {
+ mRotator.rewriteSingle(new CombiningRewriter(collection), collection.getStartMillis(),
+ collection.getEndMillis());
+ }
- // legacy file still exists; start empty to avoid double importing
- mRotator.deleteAll();
-
- final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
- collection.readLegacyNetwork(file);
-
- final long startMillis = collection.getStartMillis();
- final long endMillis = collection.getEndMillis();
-
- if (!collection.isEmpty()) {
- // process legacy data, creating active file at starting time, then
- // using end time to possibly trigger rotation.
- mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
- mRotator.maybeRotate(endMillis);
+ if (mComplete != null) {
+ throw new IllegalStateException("cannot import data when data already loaded");
}
}
- public void importLegacyUidLocked(File file) throws IOException {
- Objects.requireNonNull(mRotator, "missing FileRotator");
+ /**
+ * Rewriter that will remove any histories or persisted data points before the
+ * specified cutoff time, only writing data back when modified.
+ */
+ public static class RemoveDataBeforeRewriter implements FileRotator.Rewriter {
+ private final NetworkStatsCollection mTemp;
+ private final long mCutoffMills;
- // legacy file still exists; start empty to avoid double importing
- mRotator.deleteAll();
+ public RemoveDataBeforeRewriter(long bucketDuration, long cutoffMills) {
+ mTemp = new NetworkStatsCollection(bucketDuration);
+ mCutoffMills = cutoffMills;
+ }
- final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
- collection.readLegacyUid(file, mOnlyTags);
+ @Override
+ public void reset() {
+ mTemp.reset();
+ }
- final long startMillis = collection.getStartMillis();
- final long endMillis = collection.getEndMillis();
+ @Override
+ public void read(InputStream in) throws IOException {
+ mTemp.read(in);
+ mTemp.clearDirty();
+ mTemp.removeHistoryBefore(mCutoffMills);
+ }
- if (!collection.isEmpty()) {
- // process legacy data, creating active file at starting time, then
- // using end time to possibly trigger rotation.
- mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
- mRotator.maybeRotate(endMillis);
+ @Override
+ public boolean shouldWrite() {
+ return mTemp.isDirty();
+ }
+
+ @Override
+ public void write(OutputStream out) throws IOException {
+ mTemp.write(out);
+ }
+ }
+
+ /**
+ * Remove persisted data which contains or is before the cutoff timestamp.
+ */
+ public void removeDataBefore(long cutoffMillis) throws IOException {
+ if (mRotator != null) {
+ try {
+ mRotator.rewriteAll(new RemoveDataBeforeRewriter(
+ mBucketDuration, cutoffMillis));
+ } catch (IOException e) {
+ Log.wtf(TAG, "problem importing netstats", e);
+ recoverAndDeleteData();
+ } catch (OutOfMemoryError e) {
+ Log.wtf(TAG, "problem importing netstats", e);
+ recoverAndDeleteData();
+ }
+ }
+
+ // Clean up any pending stats
+ if (mPending != null) {
+ mPending.removeHistoryBefore(cutoffMillis);
+ }
+ if (mSinceBoot != null) {
+ mSinceBoot.removeHistoryBefore(cutoffMillis);
+ }
+
+ final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
+ if (complete != null) {
+ complete.removeHistoryBefore(cutoffMillis);
}
}
@@ -486,9 +541,10 @@
/**
* Recover from {@link FileRotator} failure by dumping state to
- * {@link DropBoxManager} and deleting contents.
+ * {@link DropBoxManager} and deleting contents if this recorder
+ * sets {@code mWipeOnError} to true, otherwise keep the contents.
*/
- private void recoverFromWtf() {
+ void recoverAndDeleteData() {
if (DUMP_BEFORE_DELETE) {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
@@ -501,7 +557,9 @@
}
mDropBox.addData(TAG_NETSTATS_DUMP, os.toByteArray(), 0);
}
-
- mRotator.deleteAll();
+ // Delete all files if this recorder is set wipe on error.
+ if (mWipeOnError) {
+ mRotator.deleteAll();
+ }
}
}
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index e3794e4..424dcd9 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -25,6 +25,7 @@
import static android.content.Intent.ACTION_USER_REMOVED;
import static android.content.Intent.EXTRA_UID;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkStats.DEFAULT_NETWORK_ALL;
import static android.net.NetworkStats.IFACE_ALL;
@@ -67,6 +68,7 @@
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.usage.NetworkStatsManager;
+import android.content.ApexEnvironment;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
@@ -74,7 +76,10 @@
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.database.ContentObserver;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityResources;
import android.net.DataUsageRequest;
import android.net.INetd;
import android.net.INetworkStatsService;
@@ -100,6 +105,7 @@
import android.net.UnderlyingNetworkInfo;
import android.net.Uri;
import android.net.netstats.IUsageCallback;
+import android.net.netstats.NetworkStatsDataMigrationUtils;
import android.net.netstats.provider.INetworkStatsProvider;
import android.net.netstats.provider.INetworkStatsProviderCallback;
import android.net.netstats.provider.NetworkStatsProvider;
@@ -118,6 +124,7 @@
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
+import android.provider.DeviceConfig;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.service.NetworkInterfaceProto;
@@ -135,6 +142,7 @@
import android.util.SparseIntArray;
import android.util.proto.ProtoOutputStream;
+import com.android.connectivity.resources.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FileRotator;
@@ -143,6 +151,7 @@
import com.android.net.module.util.BinderUtils;
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.LocationPermissionChecker;
import com.android.net.module.util.NetworkStatsUtils;
@@ -155,13 +164,19 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
+import java.nio.file.Path;
import java.time.Clock;
+import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
@@ -218,17 +233,33 @@
private static final String NETSTATS_COMBINE_SUBTYPE_ENABLED =
"netstats_combine_subtype_enabled";
- // This is current path but may be changed soon.
private static final String UID_COUNTERSET_MAP_PATH =
- "/sys/fs/bpf/map_netd_uid_counterset_map";
+ "/sys/fs/bpf/netd_shared/map_netd_uid_counterset_map";
private static final String COOKIE_TAG_MAP_PATH =
- "/sys/fs/bpf/map_netd_cookie_tag_map";
+ "/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map";
private static final String APP_UID_STATS_MAP_PATH =
- "/sys/fs/bpf/map_netd_app_uid_stats_map";
+ "/sys/fs/bpf/netd_shared/map_netd_app_uid_stats_map";
private static final String STATS_MAP_A_PATH =
- "/sys/fs/bpf/map_netd_stats_map_A";
+ "/sys/fs/bpf/netd_shared/map_netd_stats_map_A";
private static final String STATS_MAP_B_PATH =
- "/sys/fs/bpf/map_netd_stats_map_B";
+ "/sys/fs/bpf/netd_shared/map_netd_stats_map_B";
+
+ /**
+ * DeviceConfig flag used to indicate whether the files should be stored in the apex data
+ * directory.
+ */
+ static final String NETSTATS_STORE_FILES_IN_APEXDATA = "netstats_store_files_in_apexdata";
+ /**
+ * DeviceConfig flag is used to indicate whether the legacy files need to be imported, and
+ * retry count before giving up. Only valid when {@link #NETSTATS_STORE_FILES_IN_APEXDATA}
+ * set to true. Note that the value gets rollback when the mainline module gets rollback.
+ */
+ static final String NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS =
+ "netstats_import_legacy_target_attempts";
+ static final int DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS = 1;
+ static final String NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME = "import.attempts";
+ static final String NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME = "import.successes";
+ static final String NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME = "import.fallbacks";
private final Context mContext;
private final NetworkStatsFactory mStatsFactory;
@@ -237,8 +268,7 @@
private final NetworkStatsSettings mSettings;
private final NetworkStatsObservers mStatsObservers;
- private final File mSystemDir;
- private final File mBaseDir;
+ private final File mStatsDir;
private final PowerManager.WakeLock mWakeLock;
@@ -248,6 +278,13 @@
protected INetd mNetd;
private final AlertObserver mAlertObserver = new AlertObserver();
+ // Persistent counters that backed by AtomicFile which stored in the data directory as a file,
+ // to track attempts/successes/fallbacks count across reboot. Note that these counter values
+ // will be rollback as the module rollbacks.
+ private PersistentInt mImportLegacyAttemptsCounter = null;
+ private PersistentInt mImportLegacySuccessesCounter = null;
+ private PersistentInt mImportLegacyFallbacksCounter = null;
+
@VisibleForTesting
public static final String ACTION_NETWORK_STATS_POLL =
"com.android.server.action.NETWORK_STATS_POLL";
@@ -311,11 +348,16 @@
@GuardedBy("mStatsLock")
private String mActiveIface;
- /** Set of any ifaces associated with mobile networks since boot. */
+ /** Set of all ifaces currently associated with mobile networks. */
private volatile String[] mMobileIfaces = new String[0];
- /** Set of any ifaces associated with wifi networks since boot. */
- private volatile String[] mWifiIfaces = new String[0];
+ /* A set of all interfaces that have ever been associated with mobile networks since boot. */
+ @GuardedBy("mStatsLock")
+ private final Set<String> mAllMobileIfacesSinceBoot = new ArraySet<>();
+
+ /* A set of all interfaces that have ever been associated with wifi networks since boot. */
+ @GuardedBy("mStatsLock")
+ private final Set<String> mAllWifiIfacesSinceBoot = new ArraySet<>();
/** Set of all ifaces currently used by traffic that does not explicitly specify a Network. */
@GuardedBy("mStatsLock")
@@ -375,9 +417,19 @@
private long mLastStatsSessionPoll;
- /** Map from UID to number of opened sessions */
- @GuardedBy("mOpenSessionCallsPerUid")
+ private final Object mOpenSessionCallsLock = new Object();
+ /**
+ * Map from UID to number of opened sessions. This is used for rate-limt an app to open
+ * session frequently
+ */
+ @GuardedBy("mOpenSessionCallsLock")
private final SparseIntArray mOpenSessionCallsPerUid = new SparseIntArray();
+ /**
+ * Map from key {@code OpenSessionKey} to count of opened sessions. This is for recording
+ * the caller of open session and it is only for debugging.
+ */
+ @GuardedBy("mOpenSessionCallsLock")
+ private final HashMap<OpenSessionKey, Integer> mOpenSessionCallsPerCaller = new HashMap<>();
private final static int DUMP_STATS_SESSION_COUNT = 20;
@@ -393,21 +445,49 @@
@NonNull
private final BpfInterfaceMapUpdater mInterfaceMapUpdater;
- private static @NonNull File getDefaultSystemDir() {
- return new File(Environment.getDataDirectory(), "system");
- }
-
- private static @NonNull File getDefaultBaseDir() {
- File baseDir = new File(getDefaultSystemDir(), "netstats");
- baseDir.mkdirs();
- return baseDir;
- }
-
private static @NonNull Clock getDefaultClock() {
return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
Clock.systemUTC());
}
+ /**
+ * This class is a key that used in {@code mOpenSessionCallsPerCaller} to identify the count of
+ * the caller.
+ */
+ private static class OpenSessionKey {
+ public final int uid;
+ public final String packageName;
+
+ OpenSessionKey(int uid, @NonNull String packageName) {
+ this.uid = uid;
+ this.packageName = packageName;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("{");
+ sb.append("uid=").append(uid).append(",");
+ sb.append("package=").append(packageName);
+ sb.append("}");
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(@NonNull Object o) {
+ if (this == o) return true;
+ if (o.getClass() != getClass()) return false;
+
+ final OpenSessionKey key = (OpenSessionKey) o;
+ return this.uid == key.uid && TextUtils.equals(this.packageName, key.packageName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(uid, packageName);
+ }
+ }
+
private final class NetworkStatsHandler extends Handler {
NetworkStatsHandler(@NonNull Looper looper) {
super(looper);
@@ -456,8 +536,7 @@
INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
alarmManager, wakeLock, getDefaultClock(),
new DefaultNetworkStatsSettings(), new NetworkStatsFactory(context),
- new NetworkStatsObservers(), getDefaultSystemDir(), getDefaultBaseDir(),
- new Dependencies());
+ new NetworkStatsObservers(), new Dependencies());
return service;
}
@@ -467,8 +546,8 @@
@VisibleForTesting
NetworkStatsService(Context context, INetd netd, AlarmManager alarmManager,
PowerManager.WakeLock wakeLock, Clock clock, NetworkStatsSettings settings,
- NetworkStatsFactory factory, NetworkStatsObservers statsObservers, File systemDir,
- File baseDir, @NonNull Dependencies deps) {
+ NetworkStatsFactory factory, NetworkStatsObservers statsObservers,
+ @NonNull Dependencies deps) {
mContext = Objects.requireNonNull(context, "missing Context");
mNetd = Objects.requireNonNull(netd, "missing Netd");
mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager");
@@ -477,9 +556,11 @@
mWakeLock = Objects.requireNonNull(wakeLock, "missing WakeLock");
mStatsFactory = Objects.requireNonNull(factory, "missing factory");
mStatsObservers = Objects.requireNonNull(statsObservers, "missing NetworkStatsObservers");
- mSystemDir = Objects.requireNonNull(systemDir, "missing systemDir");
- mBaseDir = Objects.requireNonNull(baseDir, "missing baseDir");
mDeps = Objects.requireNonNull(deps, "missing Dependencies");
+ mStatsDir = mDeps.getOrCreateStatsDir();
+ if (!mStatsDir.exists()) {
+ throw new IllegalStateException("Persist data directory does not exist: " + mStatsDir);
+ }
final HandlerThread handlerThread = mDeps.makeHandlerThread();
handlerThread.start();
@@ -506,6 +587,80 @@
@VisibleForTesting
public static class Dependencies {
/**
+ * Get legacy platform stats directory.
+ */
+ @NonNull
+ public File getLegacyStatsDir() {
+ final File systemDataDir = new File(Environment.getDataDirectory(), "system");
+ return new File(systemDataDir, "netstats");
+ }
+
+ /**
+ * Get or create the directory that stores the persisted data usage.
+ */
+ @NonNull
+ public File getOrCreateStatsDir() {
+ final boolean storeInApexDataDir = getStoreFilesInApexData();
+
+ final File statsDataDir;
+ if (storeInApexDataDir) {
+ final File apexDataDir = ApexEnvironment
+ .getApexEnvironment(DeviceConfigUtils.TETHERING_MODULE_NAME)
+ .getDeviceProtectedDataDir();
+ statsDataDir = new File(apexDataDir, "netstats");
+
+ } else {
+ statsDataDir = getLegacyStatsDir();
+ }
+
+ if (statsDataDir.exists() || statsDataDir.mkdirs()) {
+ return statsDataDir;
+ }
+ throw new IllegalStateException("Cannot write into stats data directory: "
+ + statsDataDir);
+ }
+
+ /**
+ * Get the count of import legacy target attempts.
+ */
+ public int getImportLegacyTargetAttempts() {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(
+ DeviceConfig.NAMESPACE_TETHERING,
+ NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS,
+ DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS);
+ }
+
+ /**
+ * Create a persistent counter for given directory and name.
+ */
+ public PersistentInt createPersistentCounter(@NonNull Path dir, @NonNull String name)
+ throws IOException {
+ // TODO: Modify PersistentInt to call setStartTime every time a write is made.
+ // Create and pass a real logger here.
+ final String path = dir.resolve(name).toString();
+ return new PersistentInt(path, null /* logger */);
+ }
+
+ /**
+ * Get the flag of storing files in the apex data directory.
+ * @return whether to store files in the apex data directory.
+ */
+ public boolean getStoreFilesInApexData() {
+ return DeviceConfigUtils.getDeviceConfigPropertyBoolean(
+ DeviceConfig.NAMESPACE_TETHERING,
+ NETSTATS_STORE_FILES_IN_APEXDATA, true);
+ }
+
+ /**
+ * Read legacy persisted network stats from disk.
+ */
+ @NonNull
+ public NetworkStatsCollection readPlatformCollection(
+ @NonNull String prefix, long bucketDuration) throws IOException {
+ return NetworkStatsDataMigrationUtils.readPlatformCollection(prefix, bucketDuration);
+ }
+
+ /**
* Create a HandlerThread to use in NetworkStatsService.
*/
@NonNull
@@ -613,6 +768,11 @@
return null;
}
}
+
+ /** Gets whether the build is userdebug. */
+ public boolean isDebuggable() {
+ return Build.isDebuggable();
+ }
}
/**
@@ -640,14 +800,18 @@
mSystemReady = true;
// create data recorders along with historical rotators
- mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false);
- mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false);
- mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false);
- mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true);
+ mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, mStatsDir,
+ true /* wipeOnError */);
+ mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+ true /* wipeOnError */);
+ mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+ true /* wipeOnError */);
+ mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
+ mStatsDir, true /* wipeOnError */);
updatePersistThresholdsLocked();
- // upgrade any legacy stats, migrating them to rotated files
+ // upgrade any legacy stats
maybeUpgradeLegacyStatsLocked();
// read historical network stats from disk, since policy service
@@ -707,12 +871,14 @@
}
private NetworkStatsRecorder buildRecorder(
- String prefix, NetworkStatsSettings.Config config, boolean includeTags) {
+ String prefix, NetworkStatsSettings.Config config, boolean includeTags,
+ File baseDir, boolean wipeOnError) {
final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
Context.DROPBOX_SERVICE);
return new NetworkStatsRecorder(new FileRotator(
- mBaseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
- mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags);
+ baseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
+ mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags,
+ wipeOnError);
}
@GuardedBy("mStatsLock")
@@ -741,32 +907,371 @@
mSystemReady = false;
}
+ private static class MigrationInfo {
+ public final NetworkStatsRecorder recorder;
+ public NetworkStatsCollection collection;
+ public boolean imported;
+ MigrationInfo(@NonNull final NetworkStatsRecorder recorder) {
+ this.recorder = recorder;
+ collection = null;
+ imported = false;
+ }
+ }
+
@GuardedBy("mStatsLock")
private void maybeUpgradeLegacyStatsLocked() {
- File file;
- try {
- file = new File(mSystemDir, "netstats.bin");
- if (file.exists()) {
- mDevRecorder.importLegacyNetworkLocked(file);
- file.delete();
- }
-
- file = new File(mSystemDir, "netstats_xt.bin");
- if (file.exists()) {
- file.delete();
- }
-
- file = new File(mSystemDir, "netstats_uid.bin");
- if (file.exists()) {
- mUidRecorder.importLegacyUidLocked(file);
- mUidTagRecorder.importLegacyUidLocked(file);
- file.delete();
- }
- } catch (IOException e) {
- Log.wtf(TAG, "problem during legacy upgrade", e);
- } catch (OutOfMemoryError e) {
- Log.wtf(TAG, "problem during legacy upgrade", e);
+ final boolean storeFilesInApexData = mDeps.getStoreFilesInApexData();
+ if (!storeFilesInApexData) {
+ return;
}
+ try {
+ mImportLegacyAttemptsCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME);
+ mImportLegacySuccessesCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME);
+ mImportLegacyFallbacksCounter = mDeps.createPersistentCounter(mStatsDir.toPath(),
+ NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME);
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to create persistent counters, skip.", e);
+ return;
+ }
+
+ final int targetAttempts = mDeps.getImportLegacyTargetAttempts();
+ final int attempts;
+ final int fallbacks;
+ final boolean runComparison;
+ try {
+ attempts = mImportLegacyAttemptsCounter.get();
+ // Fallbacks counter would be set to non-zero value to indicate the migration was
+ // not successful.
+ fallbacks = mImportLegacyFallbacksCounter.get();
+ runComparison = shouldRunComparison();
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to read counters, skip.", e);
+ return;
+ }
+
+ // If the target number of attempts are reached, don't import any data.
+ // However, if comparison is requested, still read the legacy data and compare
+ // it to the importer output. This allows OEMs to debug issues with the
+ // importer code and to collect signals from the field.
+ final boolean dryRunImportOnly =
+ fallbacks != 0 && runComparison && (attempts >= targetAttempts);
+ // Return if target attempts are reached and there is no need to dry run.
+ if (attempts >= targetAttempts && !dryRunImportOnly) return;
+
+ if (dryRunImportOnly) {
+ Log.i(TAG, "Starting import : only perform read");
+ } else {
+ Log.i(TAG, "Starting import : attempts " + attempts + "/" + targetAttempts);
+ }
+
+ final MigrationInfo[] migrations = new MigrationInfo[]{
+ new MigrationInfo(mDevRecorder), new MigrationInfo(mXtRecorder),
+ new MigrationInfo(mUidRecorder), new MigrationInfo(mUidTagRecorder)
+ };
+
+ // Legacy directories will be created by recorders if they do not exist
+ final NetworkStatsRecorder[] legacyRecorders;
+ if (runComparison) {
+ final File legacyBaseDir = mDeps.getLegacyStatsDir();
+ // Set wipeOnError flag false so the recorder won't damage persistent data if reads
+ // failed and calling deleteAll.
+ legacyRecorders = new NetworkStatsRecorder[]{
+ buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, legacyBaseDir,
+ false /* wipeOnError */),
+ buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir,
+ false /* wipeOnError */),
+ buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir,
+ false /* wipeOnError */),
+ buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir,
+ false /* wipeOnError */)};
+ } else {
+ legacyRecorders = null;
+ }
+
+ long migrationEndTime = Long.MIN_VALUE;
+ try {
+ // First, read all legacy collections. This is OEM code and it can throw. Don't
+ // commit any data to disk until all are read.
+ for (int i = 0; i < migrations.length; i++) {
+ final MigrationInfo migration = migrations[i];
+
+ // Read the collection from platform code, and set fallbacks counter if throws
+ // for better debugging.
+ try {
+ migration.collection = readPlatformCollectionForRecorder(migration.recorder);
+ } catch (Throwable e) {
+ if (dryRunImportOnly) {
+ Log.wtf(TAG, "Platform data read failed. ", e);
+ return;
+ } else {
+ // Data is not imported successfully, set fallbacks counter to non-zero
+ // value to trigger dry run every later boot when the runComparison is
+ // true, in order to make it easier to debug issues.
+ tryIncrementLegacyFallbacksCounter();
+ // Re-throw for error handling. This will increase attempts counter.
+ throw e;
+ }
+ }
+
+ if (runComparison) {
+ final boolean success =
+ compareImportedToLegacyStats(migration, legacyRecorders[i]);
+ if (!success && !dryRunImportOnly) {
+ tryIncrementLegacyFallbacksCounter();
+ }
+ }
+ }
+
+ // For cases where the fallbacks are not zero but target attempts counts reached,
+ // only perform reads above and return here.
+ if (dryRunImportOnly) return;
+
+ // Find the latest end time.
+ for (final MigrationInfo migration : migrations) {
+ final long migrationEnd = migration.collection.getEndMillis();
+ if (migrationEnd > migrationEndTime) migrationEndTime = migrationEnd;
+ }
+
+ // Reading all collections from legacy data has succeeded. At this point it is
+ // safe to start overwriting the files on disk. The next step is to remove all
+ // data in the new location that overlaps with imported data. This ensures that
+ // any data in the new location that was created by a previous failed import is
+ // ignored. After that, write the imported data into the recorder. The code
+ // below can still possibly throw (disk error or OutOfMemory for example), but
+ // does not depend on code from non-mainline code.
+ Log.i(TAG, "Rewriting data with imported collections with cutoff "
+ + Instant.ofEpochMilli(migrationEndTime));
+ for (final MigrationInfo migration : migrations) {
+ migration.imported = true;
+ migration.recorder.removeDataBefore(migrationEndTime);
+ if (migration.collection.isEmpty()) continue;
+ migration.recorder.importCollectionLocked(migration.collection);
+ }
+
+ // Success normally or uses fallback method.
+ } catch (Throwable e) {
+ // The code above calls OEM code that may behave differently across devices.
+ // It can throw any exception including RuntimeExceptions and
+ // OutOfMemoryErrors. Try to recover anyway.
+ Log.wtf(TAG, "Platform data import failed. Remaining tries "
+ + (targetAttempts - attempts), e);
+
+ // Failed this time around : try again next time unless we're out of tries.
+ try {
+ mImportLegacyAttemptsCounter.set(attempts + 1);
+ } catch (IOException ex) {
+ Log.wtf(TAG, "Failed to update attempts counter.", ex);
+ }
+
+ // Try to remove any data from the failed import.
+ if (migrationEndTime > Long.MIN_VALUE) {
+ try {
+ for (final MigrationInfo migration : migrations) {
+ if (migration.imported) {
+ migration.recorder.removeDataBefore(migrationEndTime);
+ }
+ }
+ } catch (Throwable f) {
+ // If rollback still throws, there isn't much left to do. Try nuking
+ // all data, since that's the last stop. If nuking still throws, the
+ // framework will reboot, and if there are remaining tries, the migration
+ // process will retry, which is fine because it's idempotent.
+ for (final MigrationInfo migration : migrations) {
+ migration.recorder.recoverAndDeleteData();
+ }
+ }
+ }
+
+ return;
+ }
+
+ // Success ! No need to import again next time.
+ try {
+ mImportLegacyAttemptsCounter.set(targetAttempts);
+ Log.i(TAG, "Successfully imported platform collections");
+ // The successes counter is only for debugging. Hence, the synchronization
+ // between successes counter and attempts counter are not very critical.
+ final int successCount = mImportLegacySuccessesCounter.get();
+ mImportLegacySuccessesCounter.set(successCount + 1);
+ } catch (IOException e) {
+ Log.wtf(TAG, "Succeed but failed to update counters.", e);
+ }
+ }
+
+ void tryIncrementLegacyFallbacksCounter() {
+ try {
+ final int fallbacks = mImportLegacyFallbacksCounter.get();
+ mImportLegacyFallbacksCounter.set(fallbacks + 1);
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to update fallback counter.", e);
+ }
+ }
+
+ @VisibleForTesting
+ boolean shouldRunComparison() {
+ final ConnectivityResources resources = new ConnectivityResources(mContext);
+ // 0 if id not found.
+ Boolean overlayValue = null;
+ try {
+ switch (resources.get().getInteger(R.integer.config_netstats_validate_import)) {
+ case 1:
+ overlayValue = Boolean.TRUE;
+ break;
+ case 0:
+ overlayValue = Boolean.FALSE;
+ break;
+ }
+ } catch (Resources.NotFoundException e) {
+ // Overlay value is not defined.
+ }
+ return overlayValue != null ? overlayValue : mDeps.isDebuggable();
+ }
+
+ /**
+ * Compare imported data with the data returned by legacy recorders.
+ *
+ * @return true if the data matches, false if the data does not match or throw with exceptions.
+ */
+ private boolean compareImportedToLegacyStats(@NonNull MigrationInfo migration,
+ @NonNull NetworkStatsRecorder legacyRecorder) {
+ final NetworkStatsCollection legacyStats;
+ try {
+ legacyStats = legacyRecorder.getOrLoadCompleteLocked();
+ } catch (Throwable e) {
+ Log.wtf(TAG, "Failed to read stats with legacy method for recorder "
+ + legacyRecorder.getCookie(), e);
+ // Cannot read data from legacy method, skip comparison.
+ return false;
+ }
+
+ // The result of comparison is only for logging.
+ try {
+ final String error = compareStats(migration.collection, legacyStats);
+ if (error != null) {
+ Log.wtf(TAG, "Unexpected comparison result for recorder "
+ + legacyRecorder.getCookie() + ": " + error);
+ return false;
+ }
+ } catch (Throwable e) {
+ Log.wtf(TAG, "Failed to compare migrated stats with legacy stats for recorder "
+ + legacyRecorder.getCookie(), e);
+ return false;
+ }
+ return true;
+ }
+
+ private static String str(NetworkStatsCollection.Key key) {
+ StringBuilder sb = new StringBuilder()
+ .append(key.ident.toString())
+ .append(" uid=").append(key.uid);
+ if (key.set != SET_FOREGROUND) {
+ sb.append(" set=").append(key.set);
+ }
+ if (key.tag != 0) {
+ sb.append(" tag=").append(key.tag);
+ }
+ return sb.toString();
+ }
+
+ // The importer will modify some keys when importing them.
+ // In order to keep the comparison code simple, add such special cases here and simply
+ // ignore them. This should not impact fidelity much because the start/end checks and the total
+ // bytes check still need to pass.
+ private static boolean couldKeyChangeOnImport(NetworkStatsCollection.Key key) {
+ if (key.ident.isEmpty()) return false;
+ final NetworkIdentity firstIdent = key.ident.iterator().next();
+
+ // Non-mobile network with non-empty RAT type.
+ // This combination is invalid and the NetworkIdentity.Builder will throw if it is passed
+ // in, but it looks like it was previously possible to persist it to disk. The importer sets
+ // the RAT type to NETWORK_TYPE_ALL.
+ if (firstIdent.getType() != ConnectivityManager.TYPE_MOBILE
+ && firstIdent.getRatType() != NetworkTemplate.NETWORK_TYPE_ALL) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Nullable
+ private static String compareStats(
+ NetworkStatsCollection migrated, NetworkStatsCollection legacy) {
+ final Map<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
+ migrated.getEntries();
+ final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
+
+ final ArraySet<NetworkStatsCollection.Key> unmatchedLegKeys =
+ new ArraySet<>(legEntries.keySet());
+
+ for (NetworkStatsCollection.Key legKey : legEntries.keySet()) {
+ final NetworkStatsHistory legHistory = legEntries.get(legKey);
+ final NetworkStatsHistory migHistory = migEntries.get(legKey);
+
+ if (migHistory == null && couldKeyChangeOnImport(legKey)) {
+ unmatchedLegKeys.remove(legKey);
+ continue;
+ }
+
+ if (migHistory == null) {
+ return "Missing migrated history for legacy key " + str(legKey)
+ + ", legacy history was " + legHistory;
+ }
+ if (!migHistory.isSameAs(legHistory)) {
+ return "Difference in history for key " + legKey + "; legacy history " + legHistory
+ + ", migrated history " + migHistory;
+ }
+ unmatchedLegKeys.remove(legKey);
+ }
+
+ if (!unmatchedLegKeys.isEmpty()) {
+ final NetworkStatsHistory first = legEntries.get(unmatchedLegKeys.valueAt(0));
+ return "Found unmatched legacy keys: count=" + unmatchedLegKeys.size()
+ + ", first unmatched collection " + first;
+ }
+
+ if (migrated.getStartMillis() != legacy.getStartMillis()
+ || migrated.getEndMillis() != legacy.getEndMillis()) {
+ return "Start / end of the collections "
+ + migrated.getStartMillis() + "/" + legacy.getStartMillis() + " and "
+ + migrated.getEndMillis() + "/" + legacy.getEndMillis()
+ + " don't match";
+ }
+
+ if (migrated.getTotalBytes() != legacy.getTotalBytes()) {
+ return "Total bytes " + migrated.getTotalBytes() + " and " + legacy.getTotalBytes()
+ + " don't match for collections with start/end "
+ + migrated.getStartMillis()
+ + "/" + legacy.getStartMillis();
+ }
+
+ return null;
+ }
+
+ @GuardedBy("mStatsLock")
+ @NonNull
+ private NetworkStatsCollection readPlatformCollectionForRecorder(
+ @NonNull final NetworkStatsRecorder rec) throws IOException {
+ final String prefix = rec.getCookie();
+ Log.i(TAG, "Importing platform collection for prefix " + prefix);
+ final NetworkStatsCollection collection = Objects.requireNonNull(
+ mDeps.readPlatformCollection(prefix, rec.getBucketDuration()),
+ "Imported platform collection for prefix " + prefix + " must not be null");
+
+ final long bootTimestamp = System.currentTimeMillis() - SystemClock.elapsedRealtime();
+ if (!collection.isEmpty() && bootTimestamp < collection.getStartMillis()) {
+ throw new IllegalArgumentException("Platform collection for prefix " + prefix
+ + " contains data that could not possibly come from the previous boot "
+ + "(start timestamp = " + Instant.ofEpochMilli(collection.getStartMillis())
+ + ", last booted at " + Instant.ofEpochMilli(bootTimestamp));
+ }
+
+ Log.i(TAG, "Successfully read platform collection spanning from "
+ // Instant uses ISO-8601 for toString()
+ + Instant.ofEpochMilli(collection.getStartMillis()).toString() + " to "
+ + Instant.ofEpochMilli(collection.getEndMillis()).toString());
+ return collection;
}
/**
@@ -795,16 +1300,27 @@
return openSessionInternal(flags, callingPackage);
}
- private boolean isRateLimitedForPoll(int callingUid) {
- if (callingUid == android.os.Process.SYSTEM_UID) {
- return false;
- }
-
+ private boolean isRateLimitedForPoll(@NonNull OpenSessionKey key) {
final long lastCallTime;
final long now = SystemClock.elapsedRealtime();
- synchronized (mOpenSessionCallsPerUid) {
- int calls = mOpenSessionCallsPerUid.get(callingUid, 0);
- mOpenSessionCallsPerUid.put(callingUid, calls + 1);
+
+ synchronized (mOpenSessionCallsLock) {
+ Integer callsPerCaller = mOpenSessionCallsPerCaller.get(key);
+ if (callsPerCaller == null) {
+ mOpenSessionCallsPerCaller.put((key), 1);
+ } else {
+ mOpenSessionCallsPerCaller.put(key, Integer.sum(callsPerCaller, 1));
+ }
+
+ int callsPerUid = mOpenSessionCallsPerUid.get(key.uid, 0);
+ mOpenSessionCallsPerUid.put(key.uid, callsPerUid + 1);
+
+ if (key.uid == android.os.Process.SYSTEM_UID) {
+ return false;
+ }
+
+ // To avoid a non-system user to be rate-limited after system users open sessions,
+ // so update mLastStatsSessionPoll after checked if the uid is SYSTEM_UID.
lastCallTime = mLastStatsSessionPoll;
mLastStatsSessionPoll = now;
}
@@ -812,7 +1328,7 @@
return now - lastCallTime < POLL_RATE_LIMIT_MS;
}
- private int restrictFlagsForCaller(int flags) {
+ private int restrictFlagsForCaller(int flags, @NonNull String callingPackage) {
// All non-privileged callers are not allowed to turn off POLL_ON_OPEN.
final boolean isPrivileged = PermissionUtils.checkAnyPermissionOf(mContext,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
@@ -822,14 +1338,15 @@
}
// Non-system uids are rate limited for POLL_ON_OPEN.
final int callingUid = Binder.getCallingUid();
- flags = isRateLimitedForPoll(callingUid)
+ final OpenSessionKey key = new OpenSessionKey(callingUid, callingPackage);
+ flags = isRateLimitedForPoll(key)
? flags & (~NetworkStatsManager.FLAG_POLL_ON_OPEN)
: flags;
return flags;
}
private INetworkStatsSession openSessionInternal(final int flags, final String callingPackage) {
- final int restrictedFlags = restrictFlagsForCaller(flags);
+ final int restrictedFlags = restrictFlagsForCaller(flags, callingPackage);
if ((restrictedFlags & (NetworkStatsManager.FLAG_POLL_ON_OPEN
| NetworkStatsManager.FLAG_POLL_FORCE)) != 0) {
final long ident = Binder.clearCallingIdentity();
@@ -1038,9 +1555,9 @@
// We've been using pure XT stats long enough that we no longer need to
// splice DEV and XT together.
final NetworkStatsHistory history = internalGetHistoryForNetwork(template, flags, FIELD_ALL,
- accessLevel, callingUid, start, end);
+ accessLevel, callingUid, Long.MIN_VALUE, Long.MAX_VALUE);
- final long now = System.currentTimeMillis();
+ final long now = mClock.millis();
final NetworkStatsHistory.Entry entry = history.getValues(start, end, now, null);
final NetworkStats stats = new NetworkStats(end - start, 1);
@@ -1121,17 +1638,37 @@
return dataLayer;
}
+ private String[] getAllIfacesSinceBoot(int transport) {
+ synchronized (mStatsLock) {
+ final Set<String> ifaceSet;
+ if (transport == TRANSPORT_WIFI) {
+ ifaceSet = mAllWifiIfacesSinceBoot;
+ } else if (transport == TRANSPORT_CELLULAR) {
+ ifaceSet = mAllMobileIfacesSinceBoot;
+ } else {
+ throw new IllegalArgumentException("Invalid transport " + transport);
+ }
+
+ return ifaceSet.toArray(new String[0]);
+ }
+ }
+
@Override
public NetworkStats getUidStatsForTransport(int transport) {
PermissionUtils.enforceNetworkStackPermission(mContext);
try {
- final String[] relevantIfaces =
- transport == TRANSPORT_WIFI ? mWifiIfaces : mMobileIfaces;
+ final String[] ifaceArray = getAllIfacesSinceBoot(transport);
// TODO(b/215633405) : mMobileIfaces and mWifiIfaces already contain the stacked
// interfaces, so this is not useful, remove it.
final String[] ifacesToQuery =
- mStatsFactory.augmentWithStackedInterfaces(relevantIfaces);
- return getNetworkStatsUidDetail(ifacesToQuery);
+ mStatsFactory.augmentWithStackedInterfaces(ifaceArray);
+ final NetworkStats stats = getNetworkStatsUidDetail(ifacesToQuery);
+ // Clear the interfaces of the stats before returning, so callers won't get this
+ // information. This is because no caller needs this information for now, and it
+ // makes it easier to change the implementation later by using the histories in the
+ // recorder.
+ stats.clearInterfaces();
+ return stats;
} catch (RemoteException e) {
Log.wtf(TAG, "Error compiling UID stats", e);
return new NetworkStats(0L, 0);
@@ -1140,11 +1677,6 @@
@Override
public String[] getMobileIfaces() {
- // TODO (b/192758557): Remove debug log.
- if (CollectionUtils.contains(mMobileIfaces, null)) {
- throw new NullPointerException(
- "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces));
- }
return mMobileIfaces.clone();
}
@@ -1280,13 +1812,14 @@
Objects.requireNonNull(request.template, "NetworkTemplate is null");
Objects.requireNonNull(callback, "callback is null");
- int callingUid = Binder.getCallingUid();
+ final int callingPid = Binder.getCallingPid();
+ final int callingUid = Binder.getCallingUid();
@NetworkStatsAccess.Level int accessLevel = checkAccessLevel(callingPackage);
DataUsageRequest normalizedRequest;
final long token = Binder.clearCallingIdentity();
try {
normalizedRequest = mStatsObservers.register(mContext,
- request, callback, callingUid, accessLevel);
+ request, callback, callingPid, callingUid, callingPackage, accessLevel);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1511,7 +2044,6 @@
final boolean combineSubtypeEnabled = mSettings.getCombineSubtypeEnabled();
final ArraySet<String> mobileIfaces = new ArraySet<>();
- final ArraySet<String> wifiIfaces = new ArraySet<>();
for (NetworkStateSnapshot snapshot : snapshots) {
final int displayTransport =
getDisplayTransport(snapshot.getNetworkCapabilities().getTransportTypes());
@@ -1556,9 +2088,12 @@
if (isMobile) {
mobileIfaces.add(baseIface);
+ // If the interface name was present in the wifi set, the interface won't
+ // be removed from it to prevent stats from getting rollback.
+ mAllMobileIfacesSinceBoot.add(baseIface);
}
if (isWifi) {
- wifiIfaces.add(baseIface);
+ mAllWifiIfacesSinceBoot.add(baseIface);
}
}
@@ -1600,9 +2135,10 @@
findOrCreateNetworkIdentitySet(mActiveUidIfaces, iface).add(ident);
if (isMobile) {
mobileIfaces.add(iface);
+ mAllMobileIfacesSinceBoot.add(iface);
}
if (isWifi) {
- wifiIfaces.add(iface);
+ mAllWifiIfacesSinceBoot.add(iface);
}
mStatsFactory.noteStackedIface(iface, baseIface);
@@ -1611,16 +2147,6 @@
}
mMobileIfaces = mobileIfaces.toArray(new String[0]);
- mWifiIfaces = wifiIfaces.toArray(new String[0]);
- // TODO (b/192758557): Remove debug log.
- if (CollectionUtils.contains(mMobileIfaces, null)) {
- throw new NullPointerException(
- "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces));
- }
- if (CollectionUtils.contains(mWifiIfaces, null)) {
- throw new NullPointerException(
- "null element in mWifiIfaces: " + Arrays.toString(mWifiIfaces));
- }
}
private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) {
@@ -1938,6 +2464,9 @@
for (int uid : uids) {
deleteKernelTagData(uid);
}
+
+ // TODO: Remove the UID's entries from mOpenSessionCallsPerUid and
+ // mOpenSessionCallsPerCaller
}
/**
@@ -2036,10 +2565,35 @@
return;
}
+ pw.println("Directory:");
+ pw.increaseIndent();
+ pw.println(mStatsDir);
+ pw.decreaseIndent();
+
pw.println("Configs:");
pw.increaseIndent();
pw.print(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled());
pw.println();
+ pw.print(NETSTATS_STORE_FILES_IN_APEXDATA, mDeps.getStoreFilesInApexData());
+ pw.println();
+ pw.print(NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS, mDeps.getImportLegacyTargetAttempts());
+ pw.println();
+ if (mDeps.getStoreFilesInApexData()) {
+ try {
+ pw.print("platform legacy stats import attempts count",
+ mImportLegacyAttemptsCounter.get());
+ pw.println();
+ pw.print("platform legacy stats import successes count",
+ mImportLegacySuccessesCounter.get());
+ pw.println();
+ pw.print("platform legacy stats import fallbacks count",
+ mImportLegacyFallbacksCounter.get());
+ pw.println();
+ } catch (IOException e) {
+ pw.println("(failed to dump platform legacy stats import counters)");
+ }
+ }
+
pw.decreaseIndent();
pw.println("Active interfaces:");
@@ -2060,26 +2614,38 @@
}
pw.decreaseIndent();
- // Get the top openSession callers
- final SparseIntArray calls;
- synchronized (mOpenSessionCallsPerUid) {
- calls = mOpenSessionCallsPerUid.clone();
- }
-
- final int N = calls.size();
- final long[] values = new long[N];
- for (int j = 0; j < N; j++) {
- values[j] = ((long) calls.valueAt(j) << 32) | calls.keyAt(j);
- }
- Arrays.sort(values);
-
- pw.println("Top openSession callers (uid=count):");
+ pw.println("All wifi interfaces:");
pw.increaseIndent();
- final int end = Math.max(0, N - DUMP_STATS_SESSION_COUNT);
- for (int j = N - 1; j >= end; j--) {
- final int uid = (int) (values[j] & 0xffffffff);
- final int count = (int) (values[j] >> 32);
- pw.print(uid); pw.print("="); pw.println(count);
+ for (String iface : mAllWifiIfacesSinceBoot) {
+ pw.print(iface + " ");
+ }
+ pw.println();
+ pw.decreaseIndent();
+
+ pw.println("All mobile interfaces:");
+ pw.increaseIndent();
+ for (String iface : mAllMobileIfacesSinceBoot) {
+ pw.print(iface + " ");
+ }
+ pw.println();
+ pw.decreaseIndent();
+
+ // Get the top openSession callers
+ final HashMap calls;
+ synchronized (mOpenSessionCallsLock) {
+ calls = new HashMap<>(mOpenSessionCallsPerCaller);
+ }
+ final List<Map.Entry<OpenSessionKey, Integer>> list = new ArrayList<>(calls.entrySet());
+ Collections.sort(list,
+ (left, right) -> Integer.compare(left.getValue(), right.getValue()));
+ final int num = list.size();
+ final int end = Math.max(0, num - DUMP_STATS_SESSION_COUNT);
+ pw.println("Top openSession callers:");
+ pw.increaseIndent();
+ for (int j = num - 1; j >= end; j--) {
+ final Map.Entry<OpenSessionKey, Integer> entry = list.get(j);
+ pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue());
+
}
pw.decreaseIndent();
pw.println();
@@ -2099,6 +2665,13 @@
}
});
pw.decreaseIndent();
+ pw.println();
+
+ pw.println("Stats Observers:");
+ pw.increaseIndent();
+ mStatsObservers.dump(pw);
+ pw.decreaseIndent();
+ pw.println();
pw.println("Dev stats:");
pw.increaseIndent();
diff --git a/service-t/src/com/android/server/net/PersistentInt.java b/service-t/src/com/android/server/net/PersistentInt.java
new file mode 100644
index 0000000..c212b77
--- /dev/null
+++ b/service-t/src/com/android/server/net/PersistentInt.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.AtomicFile;
+import android.util.SystemConfigFileCommitEventLogger;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * A simple integer backed by an on-disk {@link AtomicFile}. Not thread-safe.
+ */
+public class PersistentInt {
+ private final String mPath;
+ private final AtomicFile mFile;
+
+ /**
+ * Constructs a new {@code PersistentInt}. The counter is set to 0 if the file does not exist.
+ * Before returning, the constructor checks that the file is readable and writable. This
+ * indicates that in the future {@link #get} and {@link #set} are likely to succeed,
+ * though other events (data corruption, other code deleting the file, etc.) may cause these
+ * calls to fail in the future.
+ *
+ * @param path the path of the file to use.
+ * @param logger the logger
+ * @throws IOException the counter could not be read or written
+ */
+ public PersistentInt(@NonNull String path, @Nullable SystemConfigFileCommitEventLogger logger)
+ throws IOException {
+ mPath = path;
+ mFile = new AtomicFile(new File(path), logger);
+ checkReadWrite();
+ }
+
+ private void checkReadWrite() throws IOException {
+ int value;
+ try {
+ value = get();
+ } catch (FileNotFoundException e) {
+ // Counter does not exist. Attempt to initialize to 0.
+ // Note that we cannot tell here if the file does not exist or if opening it failed,
+ // because in Java both of those throw FileNotFoundException.
+ value = 0;
+ }
+ set(value);
+ get();
+ // No exceptions? Good.
+ }
+
+ /**
+ * Gets the current value.
+ *
+ * @return the current value of the counter.
+ * @throws IOException if reading the value failed.
+ */
+ public int get() throws IOException {
+ try (FileInputStream fin = mFile.openRead();
+ DataInputStream din = new DataInputStream(fin)) {
+ return din.readInt();
+ }
+ }
+
+ /**
+ * Sets the current value.
+ * @param value the value to set
+ * @throws IOException if writing the value failed.
+ */
+ public void set(int value) throws IOException {
+ FileOutputStream fout = null;
+ try {
+ fout = mFile.startWrite();
+ DataOutputStream dout = new DataOutputStream(fout);
+ dout.writeInt(value);
+ mFile.finishWrite(fout);
+ } catch (IOException e) {
+ if (fout != null) {
+ mFile.failWrite(fout);
+ }
+ throw e;
+ }
+ }
+
+ public String getPath() {
+ return mPath;
+ }
+}
diff --git a/service/Android.bp b/service/Android.bp
index 25b970a..7a4fb33 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -158,6 +158,7 @@
static_libs: [
// Do not add libs here if they are already included
// in framework-connectivity
+ "connectivity-net-module-utils-bpf",
"connectivity_native_aidl_interface-lateststable-java",
"dnsresolver_aidl_interface-V9-java",
"modules-utils-shell-command-handler",
@@ -169,7 +170,7 @@
"networkstack-client",
"PlatformProperties",
"service-connectivity-protos",
- "NetworkStackApiCurrentShims",
+ "NetworkStackApiStableShims",
],
apex_available: [
"com.android.tethering",
@@ -181,6 +182,25 @@
],
}
+// TODO: Remove this temporary library and put code into module when test coverage is enough.
+java_library {
+ name: "service-mdns",
+ sdk_version: "system_server_current",
+ min_sdk_version: "30",
+ srcs: [
+ "mdns/**/*.java",
+ ],
+ libs: [
+ "framework-annotations-lib",
+ "framework-connectivity-pre-jarjar",
+ "framework-wifi.stubs.module_lib",
+ "service-connectivity-pre-jarjar",
+ ],
+ visibility: [
+ "//packages/modules/Connectivity/tests:__subpackages__",
+ ],
+}
+
java_library {
name: "service-connectivity-protos",
sdk_version: "system_current",
@@ -198,11 +218,10 @@
lint: { strict_updatability_linting: true },
}
-java_library {
- name: "service-connectivity",
+java_defaults {
+ name: "service-connectivity-defaults",
sdk_version: "system_server_current",
min_sdk_version: "30",
- installable: true,
// This library combines system server jars that have access to different bootclasspath jars.
// Lower SDK service jars must not depend on higher SDK jars as that would let them
// transitively depend on the wrong bootclasspath jars. Sources also cannot be added here as
@@ -216,9 +235,32 @@
apex_available: [
"com.android.tethering",
],
+ optimize: {
+ enabled: true,
+ shrink: true,
+ proguard_flags_files: ["proguard.flags"],
+ },
lint: { strict_updatability_linting: true },
}
+// A special library created strictly for use by the tests as they need the
+// implementation library but that is not available when building from prebuilts.
+// Using a library with a different name to what is used by the prebuilts ensures
+// that this will never depend on the prebuilt.
+// Switching service-connectivity to a java_sdk_library would also have worked as
+// that has built in support for managing this but that is too big a change at this
+// point.
+java_library {
+ name: "service-connectivity-for-tests",
+ defaults: ["service-connectivity-defaults"],
+}
+
+java_library {
+ name: "service-connectivity",
+ defaults: ["service-connectivity-defaults"],
+ installable: true,
+}
+
filegroup {
name: "connectivity-jarjar-rules",
srcs: ["jarjar-rules.txt"],
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
index f491cc7..02b2875 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -23,6 +23,7 @@
name: "ServiceConnectivityResources",
sdk_version: "module_30",
min_sdk_version: "30",
+ target_sdk_version: "33",
resource_dirs: [
"res",
],
diff --git a/service/ServiceConnectivityResources/res/values-sq/strings.xml b/service/ServiceConnectivityResources/res/values-sq/strings.xml
index 385c75c..85bd84f 100644
--- a/service/ServiceConnectivityResources/res/values-sq/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-sq/strings.xml
@@ -35,7 +35,7 @@
<string-array name="network_switch_type_name">
<item msgid="3004933964374161223">"të dhënat celulare"</item>
<item msgid="5624324321165953608">"Wi-Fi"</item>
- <item msgid="5667906231066981731">"Bluetooth"</item>
+ <item msgid="5667906231066981731">"Bluetooth-i"</item>
<item msgid="346574747471703768">"Eternet"</item>
<item msgid="5734728378097476003">"VPN"</item>
</string-array>
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 81782f9..bff6953 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -179,4 +179,13 @@
Only supported up to S. On T+, the Wi-Fi code should use unregisterAfterReplacement in order
to ensure that apps see the network disconnect and reconnect. -->
<integer translatable="false" name="config_validationFailureAfterRoamIgnoreTimeMillis">-1</integer>
+
+ <!-- Whether the network stats service should run compare on the result of
+ {@link NetworkStatsDataMigrationUtils#readPlatformCollection} and the result
+ of reading from legacy recorders. Possible values are:
+ 0 = never compare,
+ 1 = always compare,
+ 2 = compare on debuggable builds (default value)
+ -->
+ <integer translatable="false" name="config_netstats_validate_import">2</integer>
</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index b92dd08..3389d63 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -41,6 +41,7 @@
<item type="array" name="config_ethernet_interfaces"/>
<item type="string" name="config_ethernet_iface_regex"/>
<item type="integer" name="config_validationFailureAfterRoamIgnoreTimeMillis" />
+ <item type="integer" name="config_netstats_validate_import" />
</policy>
</overlayable>
</resources>
diff --git a/service/jarjar-excludes.txt b/service/jarjar-excludes.txt
new file mode 100644
index 0000000..b0d6763
--- /dev/null
+++ b/service/jarjar-excludes.txt
@@ -0,0 +1,9 @@
+# Classes loaded by SystemServer via their hardcoded name, so they can't be jarjared
+com\.android\.server\.ConnectivityServiceInitializer(\$.+)?
+com\.android\.server\.NetworkStatsServiceInitializer(\$.+)?
+
+# Do not jarjar com.android.server, as several unit tests fail because they lose
+# package-private visibility between jarjared and non-jarjared classes.
+# TODO: fix the tests and also jarjar com.android.server, or at least only exclude a package that
+# is specific to the module like com.android.server.connectivity
+com\.android\.server\..+
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
index 4b21569..1ad75e3 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -92,7 +92,6 @@
rule android.net.util.KeepalivePacketDataUtil* com.android.connectivity.@0
# From connectivity-module-utils
-rule android.net.util.SharedLog* com.android.connectivity.@0
rule android.net.shared.** com.android.connectivity.@0
# From services-connectivity-shared-srcs
@@ -111,5 +110,14 @@
# From mdns-aidl-interface
rule android.net.mdns.aidl.** android.net.connectivity.@0
+# From nearby-service, including proto
+rule service.proto.** com.android.server.nearby.@0
+rule androidx.annotation.Keep* com.android.server.nearby.@0
+rule androidx.collection.** com.android.server.nearby.@0
+rule androidx.core.** com.android.server.nearby.@0
+rule androidx.versionedparcelable.** com.android.server.nearby.@0
+rule com.google.common.** com.android.server.nearby.@0
+rule android.support.v4.** com.android.server.nearby.@0
+
# Remaining are connectivity sources in com.android.server and com.android.server.connectivity:
# TODO: move to a subpackage of com.android.connectivity (such as com.android.connectivity.server)
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
index f13c68d..49392e0 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -39,148 +39,125 @@
namespace android {
-static void native_init(JNIEnv* env, jobject clazz) {
+#define CHECK_LOG(status) \
+ do { \
+ if (!isOk(status)) \
+ ALOGE("%s failed, error code = %d", __func__, status.code()); \
+ } while (0)
+
+static void native_init(JNIEnv* env, jclass clazz) {
Status status = mTc.start();
- if (!isOk(status)) {
- ALOGE("%s failed, error code = %d", __func__, status.code());
- }
+ CHECK_LOG(status);
}
-static jint native_addNaughtyApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_addNaughtyApp(JNIEnv* env, jobject self, jint uid) {
const uint32_t appUids = static_cast<uint32_t>(abs(uid));
Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
TrafficController::IptOp::IptOpInsert);
- if (!isOk(status)) {
- ALOGE("%s failed, error code = %d", __func__, status.code());
- }
+ CHECK_LOG(status);
return (jint)status.code();
}
-static jint native_removeNaughtyApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_removeNaughtyApp(JNIEnv* env, jobject self, jint uid) {
const uint32_t appUids = static_cast<uint32_t>(abs(uid));
Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
TrafficController::IptOp::IptOpDelete);
- if (!isOk(status)) {
- ALOGE("%s failed, error code = %d", __func__, status.code());
- }
+ CHECK_LOG(status);
return (jint)status.code();
}
-static jint native_addNiceApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_addNiceApp(JNIEnv* env, jobject self, jint uid) {
const uint32_t appUids = static_cast<uint32_t>(abs(uid));
Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
TrafficController::IptOp::IptOpInsert);
- if (!isOk(status)) {
- ALOGE("%s failed, error code = %d", __func__, status.code());
- }
+ CHECK_LOG(status);
return (jint)status.code();
}
-static jint native_removeNiceApp(JNIEnv* env, jobject clazz, jint uid) {
+static jint native_removeNiceApp(JNIEnv* env, jobject self, jint uid) {
const uint32_t appUids = static_cast<uint32_t>(abs(uid));
Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
TrafficController::IptOp::IptOpDelete);
- if (!isOk(status)) {
- ALOGD("%s failed, error code = %d", __func__, status.code());
- }
+ CHECK_LOG(status);
return (jint)status.code();
}
-static jint native_setChildChain(JNIEnv* env, jobject clazz, jint childChain, jboolean enable) {
- auto chain = static_cast<ChildChain>(childChain);
- int res = mTc.toggleUidOwnerMap(chain, enable);
- if (res) {
- ALOGE("%s failed, error code = %d", __func__, res);
- }
- return (jint)res;
-}
-
-static jint native_replaceUidChain(JNIEnv* env, jobject clazz, jstring name, jboolean isAllowlist,
- jintArray jUids) {
+static jint native_replaceUidChain(JNIEnv* env, jobject self, jstring name, jboolean isAllowlist,
+ jintArray jUids) {
const ScopedUtfChars chainNameUtf8(env, name);
- if (chainNameUtf8.c_str() == nullptr) {
- return -EINVAL;
- }
+ if (chainNameUtf8.c_str() == nullptr) return -EINVAL;
const std::string chainName(chainNameUtf8.c_str());
ScopedIntArrayRO uids(env, jUids);
- if (uids.get() == nullptr) {
- return -EINVAL;
- }
+ if (uids.get() == nullptr) return -EINVAL;
size_t size = uids.size();
static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
int res = mTc.replaceUidOwnerMap(chainName, isAllowlist, data);
- if (res) {
- ALOGE("%s failed, error code = %d", __func__, res);
- }
+ if (res) ALOGE("%s failed, error code = %d", __func__, res);
return (jint)res;
}
-static jint native_setUidRule(JNIEnv* env, jobject clazz, jint childChain, jint uid,
- jint firewallRule) {
+static jint native_setUidRule(JNIEnv* env, jobject self, jint childChain, jint uid,
+ jint firewallRule) {
auto chain = static_cast<ChildChain>(childChain);
auto rule = static_cast<FirewallRule>(firewallRule);
FirewallType fType = mTc.getFirewallType(chain);
int res = mTc.changeUidOwnerRule(chain, uid, rule, fType);
- if (res) {
- ALOGE("%s failed, error code = %d", __func__, res);
- }
+ if (res) ALOGE("%s failed, error code = %d", __func__, res);
return (jint)res;
}
-static jint native_addUidInterfaceRules(JNIEnv* env, jobject clazz, jstring ifName,
- jintArray jUids) {
- const ScopedUtfChars ifNameUtf8(env, ifName);
- if (ifNameUtf8.c_str() == nullptr) {
- return -EINVAL;
+static jint native_addUidInterfaceRules(JNIEnv* env, jobject self, jstring ifName,
+ jintArray jUids) {
+ // Null ifName is a wildcard to allow apps to receive packets on all interfaces and ifIndex is
+ // set to 0.
+ int ifIndex = 0;
+ if (ifName != nullptr) {
+ const ScopedUtfChars ifNameUtf8(env, ifName);
+ const std::string interfaceName(ifNameUtf8.c_str());
+ ifIndex = if_nametoindex(interfaceName.c_str());
}
- const std::string interfaceName(ifNameUtf8.c_str());
- const int ifIndex = if_nametoindex(interfaceName.c_str());
ScopedIntArrayRO uids(env, jUids);
- if (uids.get() == nullptr) {
- return -EINVAL;
- }
+ if (uids.get() == nullptr) return -EINVAL;
size_t size = uids.size();
static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
Status status = mTc.addUidInterfaceRules(ifIndex, data);
- if (!isOk(status)) {
- ALOGE("%s failed, error code = %d", __func__, status.code());
- }
+ CHECK_LOG(status);
return (jint)status.code();
}
-static jint native_removeUidInterfaceRules(JNIEnv* env, jobject clazz, jintArray jUids) {
+static jint native_removeUidInterfaceRules(JNIEnv* env, jobject self, jintArray jUids) {
ScopedIntArrayRO uids(env, jUids);
- if (uids.get() == nullptr) {
- return -EINVAL;
- }
+ if (uids.get() == nullptr) return -EINVAL;
size_t size = uids.size();
static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
Status status = mTc.removeUidInterfaceRules(data);
- if (!isOk(status)) {
- ALOGE("%s failed, error code = %d", __func__, status.code());
- }
+ CHECK_LOG(status);
return (jint)status.code();
}
-static jint native_swapActiveStatsMap(JNIEnv* env, jobject clazz) {
+static jint native_updateUidLockdownRule(JNIEnv* env, jobject self, jint uid, jboolean add) {
+ Status status = mTc.updateUidLockdownRule(uid, add);
+ CHECK_LOG(status);
+ return (jint)status.code();
+}
+
+static jint native_swapActiveStatsMap(JNIEnv* env, jobject self) {
Status status = mTc.swapActiveStatsMap();
- if (!isOk(status)) {
- ALOGD("%s failed, error code = %d", __func__, status.code());
- }
+ CHECK_LOG(status);
return (jint)status.code();
}
-static void native_setPermissionForUids(JNIEnv* env, jobject clazz, jint permission,
- jintArray jUids) {
+static void native_setPermissionForUids(JNIEnv* env, jobject self, jint permission,
+ jintArray jUids) {
ScopedIntArrayRO uids(env, jUids);
if (uids.get() == nullptr) return;
@@ -190,7 +167,7 @@
mTc.setPermissionForUids(permission, data);
}
-static void native_dump(JNIEnv* env, jobject clazz, jobject javaFd, jboolean verbose) {
+static void native_dump(JNIEnv* env, jobject self, jobject javaFd, jboolean verbose) {
int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
if (fd < 0) {
jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor");
@@ -215,8 +192,6 @@
(void*)native_addNiceApp},
{"native_removeNiceApp", "(I)I",
(void*)native_removeNiceApp},
- {"native_setChildChain", "(IZ)I",
- (void*)native_setChildChain},
{"native_replaceUidChain", "(Ljava/lang/String;Z[I)I",
(void*)native_replaceUidChain},
{"native_setUidRule", "(III)I",
@@ -225,6 +200,8 @@
(void*)native_addUidInterfaceRules},
{"native_removeUidInterfaceRules", "([I)I",
(void*)native_removeUidInterfaceRules},
+ {"native_updateUidLockdownRule", "(IZ)I",
+ (void*)native_updateUidLockdownRule},
{"native_swapActiveStatsMap", "()I",
(void*)native_swapActiveStatsMap},
{"native_setPermissionForUids", "(I[I)V",
@@ -235,9 +212,8 @@
// clang-format on
int register_com_android_server_BpfNetMaps(JNIEnv* env) {
- return jniRegisterNativeMethods(env,
- "com/android/server/BpfNetMaps",
- gMethods, NELEM(gMethods));
+ return jniRegisterNativeMethods(env, "com/android/server/BpfNetMaps",
+ gMethods, NELEM(gMethods));
}
}; // namespace android
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
index 4efd0e1..9c7a761 100644
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -51,7 +51,15 @@
jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
}
-static int createTunTapInterface(JNIEnv* env, bool isTun, const char* iface) {
+// enable or disable carrier on tun / tap interface.
+static void setTunTapCarrierEnabledImpl(JNIEnv* env, const char* iface, int tunFd, bool enabled) {
+ uint32_t carrierOn = enabled;
+ if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
+ throwException(env, errno, "set carrier", iface);
+ }
+}
+
+static int createTunTapImpl(JNIEnv* env, bool isTun, bool hasCarrier, const char* iface) {
base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
ifreq ifr{};
@@ -63,6 +71,11 @@
return -1;
}
+ if (!hasCarrier) {
+ // disable carrier before setting IFF_UP
+ setTunTapCarrierEnabledImpl(env, iface, tun.get(), hasCarrier);
+ }
+
// Activate interface using an unconnected datagram socket.
base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
ifr.ifr_flags = IFF_UP;
@@ -79,23 +92,31 @@
//------------------------------------------------------------------------------
-static jint create(JNIEnv* env, jobject /* thiz */, jboolean isTun, jstring jIface) {
+static void setTunTapCarrierEnabled(JNIEnv* env, jclass /* clazz */, jstring
+ jIface, jint tunFd, jboolean enabled) {
+ ScopedUtfChars iface(env, jIface);
+ if (!iface.c_str()) {
+ jniThrowNullPointerException(env, "iface");
+ }
+ setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
+}
+
+static jint createTunTap(JNIEnv* env, jclass /* clazz */, jboolean isTun,
+ jboolean hasCarrier, jstring jIface) {
ScopedUtfChars iface(env, jIface);
if (!iface.c_str()) {
jniThrowNullPointerException(env, "iface");
return -1;
}
- int tun = createTunTapInterface(env, isTun, iface.c_str());
-
- // Any exceptions will be thrown from the createTunTapInterface call
- return tun;
+ return createTunTapImpl(env, isTun, hasCarrier, iface.c_str());
}
//------------------------------------------------------------------------------
static const JNINativeMethod gMethods[] = {
- {"jniCreateTunTap", "(ZLjava/lang/String;)I", (void*)create},
+ {"nativeSetTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V", (void*)setTunTapCarrierEnabled},
+ {"nativeCreateTunTap", "(ZZLjava/lang/String;)I", (void*)createTunTap},
};
int register_com_android_server_TestNetworkService(JNIEnv* env) {
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index 500c696..e2c5a63 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -314,7 +314,8 @@
}
// TODO: use android::base::ScopeGuard.
- if (int ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK)) {
+ if (int ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK
+ | POSIX_SPAWN_CLOEXEC_DEFAULT)) {
posix_spawnattr_destroy(&attr);
throwIOException(env, "posix_spawnattr_setflags failed", ret);
return -1;
diff --git a/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java
new file mode 100644
index 0000000..2b99d0a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 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;
+
+/** Interface for monitoring connectivity changes. */
+public interface ConnectivityMonitor {
+ /**
+ * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+ * network interfaces available for multi-cast messaging has changed.
+ */
+ void startWatchingConnectivityChanges();
+
+ /** Stops monitoring changes of connectivity. */
+ void stopWatchingConnectivityChanges();
+
+ void notifyConnectivityChange();
+
+ /** Listener interface for receiving connectivity changes. */
+ interface Listener {
+ void onConnectivityChanged();
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java
new file mode 100644
index 0000000..3563d61
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManager.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 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.annotation.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+/** Class for monitoring connectivity changes using {@link ConnectivityManager}. */
+public class ConnectivityMonitorWithConnectivityManager implements ConnectivityMonitor {
+ private static final String TAG = "ConnMntrWConnMgr";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+
+ private final Listener listener;
+ private final ConnectivityManager.NetworkCallback networkCallback;
+ private final ConnectivityManager connectivityManager;
+ // TODO(b/71901993): Ideally we shouldn't need this flag. However we still don't have clues why
+ // the receiver is unregistered twice yet.
+ private boolean isCallbackRegistered = false;
+
+ @SuppressWarnings({"nullness:assignment", "nullness:method.invocation"})
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public ConnectivityMonitorWithConnectivityManager(Context context, Listener listener) {
+ this.listener = listener;
+
+ connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ networkCallback =
+ new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(Network network) {
+ LOGGER.log("network available.");
+ notifyConnectivityChange();
+ }
+
+ @Override
+ public void onLost(Network network) {
+ LOGGER.log("network lost.");
+ notifyConnectivityChange();
+ }
+
+ @Override
+ public void onUnavailable() {
+ LOGGER.log("network unavailable.");
+ notifyConnectivityChange();
+ }
+ };
+ }
+
+ @Override
+ public void notifyConnectivityChange() {
+ listener.onConnectivityChanged();
+ }
+
+ /**
+ * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+ * network interfaces available for multi-cast messaging has changed.
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void startWatchingConnectivityChanges() {
+ LOGGER.log("Start watching connectivity changes");
+ if (isCallbackRegistered) {
+ return;
+ }
+
+ connectivityManager.registerNetworkCallback(
+ new NetworkRequest.Builder().addTransportType(
+ NetworkCapabilities.TRANSPORT_WIFI).build(),
+ networkCallback);
+ isCallbackRegistered = true;
+ }
+
+ /** Stops monitoring changes of connectivity. */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public void stopWatchingConnectivityChanges() {
+ LOGGER.log("Stop watching connectivity changes");
+ if (!isCallbackRegistered) {
+ return;
+ }
+
+ connectivityManager.unregisterNetworkCallback(networkCallback);
+ isCallbackRegistered = false;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service/mdns/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
new file mode 100644
index 0000000..3db1b22
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 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.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+/**
+ * A {@link Callable} that builds and enqueues a mDNS query to send over the multicast socket. If a
+ * query is built and enqueued successfully, then call to {@link #call()} returns the transaction ID
+ * and the list of the subtypes in the query as a {@link Pair}. If a query is failed to build, or if
+ * it can not be enqueued, then call to {@link #call()} returns {@code null}.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class EnqueueMdnsQueryCallable implements Callable<Pair<Integer, List<String>>> {
+
+ private static final String TAG = "MdnsQueryCallable";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+ private static final List<Integer> castShellEmulatorMdnsPorts;
+
+ static {
+ castShellEmulatorMdnsPorts = new ArrayList<>();
+ String[] stringPorts = MdnsConfigs.castShellEmulatorMdnsPorts();
+
+ for (String port : stringPorts) {
+ try {
+ castShellEmulatorMdnsPorts.add(Integer.parseInt(port));
+ } catch (NumberFormatException e) {
+ // Ignore.
+ }
+ }
+ }
+
+ private final WeakReference<MdnsSocketClient> weakRequestSender;
+ private final MdnsPacketWriter packetWriter;
+ private final String[] serviceTypeLabels;
+ private final List<String> subtypes;
+ private final boolean expectUnicastResponse;
+ private final int transactionId;
+
+ EnqueueMdnsQueryCallable(
+ @NonNull MdnsSocketClient requestSender,
+ @NonNull MdnsPacketWriter packetWriter,
+ @NonNull String serviceType,
+ @NonNull Collection<String> subtypes,
+ boolean expectUnicastResponse,
+ int transactionId) {
+ weakRequestSender = new WeakReference<>(requestSender);
+ this.packetWriter = packetWriter;
+ serviceTypeLabels = TextUtils.split(serviceType, "\\.");
+ this.subtypes = new ArrayList<>(subtypes);
+ this.expectUnicastResponse = expectUnicastResponse;
+ this.transactionId = transactionId;
+ }
+
+ @Override
+ public Pair<Integer, List<String>> call() {
+ try {
+ MdnsSocketClient requestSender = weakRequestSender.get();
+ if (requestSender == null) {
+ return null;
+ }
+
+ int numQuestions = 1;
+ if (!subtypes.isEmpty()) {
+ numQuestions += subtypes.size();
+ }
+
+ // Header.
+ packetWriter.writeUInt16(transactionId); // transaction ID
+ packetWriter.writeUInt16(MdnsConstants.FLAGS_QUERY); // flags
+ packetWriter.writeUInt16(numQuestions); // number of questions
+ packetWriter.writeUInt16(0); // number of answers (not yet known; will be written later)
+ packetWriter.writeUInt16(0); // number of authority entries
+ packetWriter.writeUInt16(0); // number of additional records
+
+ // Question(s). There will be one question for each (fqdn+subtype, recordType)
+ // combination,
+ // as well as one for each (fqdn, recordType) combination.
+
+ for (String subtype : subtypes) {
+ String[] labels = new String[serviceTypeLabels.length + 2];
+ labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
+ labels[1] = MdnsConstants.SUBTYPE_LABEL;
+ System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
+
+ packetWriter.writeLabels(labels);
+ packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
+ packetWriter.writeUInt16(
+ MdnsConstants.QCLASS_INTERNET
+ | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
+ }
+
+ packetWriter.writeLabels(serviceTypeLabels);
+ packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
+ packetWriter.writeUInt16(
+ MdnsConstants.QCLASS_INTERNET
+ | (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
+
+ InetAddress mdnsAddress = MdnsConstants.getMdnsIPv4Address();
+ if (requestSender.isOnIPv6OnlyNetwork()) {
+ mdnsAddress = MdnsConstants.getMdnsIPv6Address();
+ }
+
+ sendPacketTo(requestSender,
+ new InetSocketAddress(mdnsAddress, MdnsConstants.MDNS_PORT));
+ for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
+ sendPacketTo(requestSender, new InetSocketAddress(mdnsAddress, emulatorPort));
+ }
+ return Pair.create(transactionId, subtypes);
+ } catch (IOException e) {
+ LOGGER.e(String.format("Failed to create mDNS packet for subtype: %s.",
+ TextUtils.join(",", subtypes)), e);
+ return null;
+ }
+ }
+
+ private void sendPacketTo(MdnsSocketClient requestSender, InetSocketAddress address)
+ throws IOException {
+ DatagramPacket packet = packetWriter.getPacket(address);
+ if (expectUnicastResponse) {
+ requestSender.sendUnicastPacket(packet);
+ } else {
+ requestSender.sendMulticastPacket(packet);
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/ExecutorProvider.java b/service/mdns/com/android/server/connectivity/mdns/ExecutorProvider.java
new file mode 100644
index 0000000..72b65e0
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/ExecutorProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2021 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.util.ArraySet;
+
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+/**
+ * This class provides {@link ScheduledExecutorService} instances to {@link MdnsServiceTypeClient}
+ * instances, and provides method to shutdown all the created executors.
+ */
+public class ExecutorProvider {
+
+ private final Set<ScheduledExecutorService> serviceTypeClientSchedulerExecutors =
+ new ArraySet<>();
+
+ /** Returns a new {@link ScheduledExecutorService} instance. */
+ public ScheduledExecutorService newServiceTypeClientSchedulerExecutor() {
+ // TODO: actually use a pool ?
+ ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1);
+ serviceTypeClientSchedulerExecutors.add(executor);
+ return executor;
+ }
+
+ /** Shuts down all the created {@link ScheduledExecutorService} instances. */
+ public void shutdownAll() {
+ for (ScheduledExecutorService executor : serviceTypeClientSchedulerExecutors) {
+ executor.shutdownNow();
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java b/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java
new file mode 100644
index 0000000..922037b
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsConfigs.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 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;
+
+/**
+ * mDNS configuration values.
+ *
+ * TODO: consider making some of these adjustable via flags.
+ */
+public class MdnsConfigs {
+ public static String[] castShellEmulatorMdnsPorts() {
+ return new String[0];
+ }
+
+ public static long initialTimeBetweenBurstsMs() {
+ return 5000L;
+ }
+
+ public static long timeBetweenBurstsMs() {
+ return 20_000L;
+ }
+
+ public static int queriesPerBurst() {
+ return 3;
+ }
+
+ public static long timeBetweenQueriesInBurstMs() {
+ return 1000L;
+ }
+
+ public static int queriesPerBurstPassive() {
+ return 1;
+ }
+
+ public static boolean alwaysAskForUnicastResponseInEachBurst() {
+ return false;
+ }
+
+ public static boolean useSessionIdToScheduleMdnsTask() {
+ return false;
+ }
+
+ public static boolean shouldCancelScanTaskWhenFutureIsNull() {
+ return false;
+ }
+
+ public static long sleepTimeForSocketThreadMs() {
+ return 20_000L;
+ }
+
+ public static boolean checkMulticastResponse() {
+ return false;
+ }
+
+ public static boolean useSeparateSocketToSendUnicastQuery() {
+ return false;
+ }
+
+ public static long checkMulticastResponseIntervalMs() {
+ return 10_000L;
+ }
+
+ public static boolean clearMdnsPacketQueueAfterDiscoveryStops() {
+ return true;
+ }
+
+ public static boolean allowAddMdnsPacketAfterDiscoveryStops() {
+ return false;
+ }
+
+ public static int mdnsPacketQueueMaxSize() {
+ return Integer.MAX_VALUE;
+ }
+
+ public static boolean preferIpv6() {
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsConstants.java b/service/mdns/com/android/server/connectivity/mdns/MdnsConstants.java
new file mode 100644
index 0000000..ed28700
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsConstants.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 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.annotation.TargetApi;
+import android.os.Build;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/** mDNS-related constants. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public final class MdnsConstants {
+ public static final int MDNS_PORT = 5353;
+ // Flags word format is:
+ // 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
+ // QR [ Opcode ] AA TC RD RA Z AD CD [ Rcode ]
+ // See http://www.networksorcery.com/enp/protocol/dns.htm
+ // For responses, QR bit should be 1, AA - CD bits should be ignored, and all other bits
+ // should be 0.
+ public static final int FLAGS_QUERY = 0x0000;
+ public static final int FLAGS_RESPONSE_MASK = 0xF80F;
+ public static final int FLAGS_RESPONSE = 0x8000;
+ public static final int QCLASS_INTERNET = 0x0001;
+ public static final int QCLASS_UNICAST = 0x8000;
+ public static final String SUBTYPE_LABEL = "_sub";
+ public static final String SUBTYPE_PREFIX = "_";
+ private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
+ private static final String MDNS_IPV6_HOST_ADDRESS = "FF02::FB";
+ private static InetAddress mdnsAddress;
+ private static Charset utf8Charset;
+ private MdnsConstants() {
+ }
+
+ public static InetAddress getMdnsIPv4Address() {
+ synchronized (MdnsConstants.class) {
+ InetAddress addr = null;
+ try {
+ addr = InetAddress.getByName(MDNS_IPV4_HOST_ADDRESS);
+ } catch (UnknownHostException e) {
+ /* won't happen */
+ }
+ mdnsAddress = addr;
+ return mdnsAddress;
+ }
+ }
+
+ public static InetAddress getMdnsIPv6Address() {
+ synchronized (MdnsConstants.class) {
+ InetAddress addr = null;
+ try {
+ addr = InetAddress.getByName(MDNS_IPV6_HOST_ADDRESS);
+ } catch (UnknownHostException e) {
+ /* won't happen */
+ }
+ mdnsAddress = addr;
+ return mdnsAddress;
+ }
+ }
+
+ public static Charset getUtf8Charset() {
+ synchronized (MdnsConstants.class) {
+ if (utf8Charset == null) {
+ utf8Charset = getUtf8CharsetOnKitKat();
+ }
+ return utf8Charset;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private static Charset getUtf8CharsetOnKitKat() {
+ return StandardCharsets.UTF_8;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
new file mode 100644
index 0000000..1faa6ce
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2021 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.Manifest.permission;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
+ * notify them when a mDNS service instance is found, updated, or removed?
+ */
+public class MdnsDiscoveryManager implements MdnsSocketClient.Callback {
+
+ private static final MdnsLogger LOGGER = new MdnsLogger("MdnsDiscoveryManager");
+
+ private final ExecutorProvider executorProvider;
+ private final MdnsSocketClient socketClient;
+
+ private final Map<String, MdnsServiceTypeClient> serviceTypeClients = new ArrayMap<>();
+
+ public MdnsDiscoveryManager(
+ @NonNull ExecutorProvider executorProvider, @NonNull MdnsSocketClient socketClient) {
+ this.executorProvider = executorProvider;
+ this.socketClient = socketClient;
+ }
+
+ /**
+ * Starts (or continue) to discovery mDNS services with given {@code serviceType}, and registers
+ * {@code listener} for receiving mDNS service discovery responses.
+ *
+ * @param serviceType The type of the service to discover.
+ * @param listener The {@link MdnsServiceBrowserListener} listener.
+ * @param searchOptions The {@link MdnsSearchOptions} to be used for discovering {@code
+ * serviceType}.
+ */
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ public synchronized void registerListener(
+ @NonNull String serviceType,
+ @NonNull MdnsServiceBrowserListener listener,
+ @NonNull MdnsSearchOptions searchOptions) {
+ LOGGER.log(
+ "Registering listener for subtypes: %s",
+ TextUtils.join(",", searchOptions.getSubtypes()));
+ if (serviceTypeClients.isEmpty()) {
+ // First listener. Starts the socket client.
+ try {
+ socketClient.startDiscovery();
+ } catch (IOException e) {
+ LOGGER.e("Failed to start discover.", e);
+ return;
+ }
+ }
+ // All listeners of the same service types shares the same MdnsServiceTypeClient.
+ MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
+ if (serviceTypeClient == null) {
+ serviceTypeClient = createServiceTypeClient(serviceType);
+ serviceTypeClients.put(serviceType, serviceTypeClient);
+ }
+ serviceTypeClient.startSendAndReceive(listener, searchOptions);
+ }
+
+ /**
+ * Unregister {@code listener} for receiving mDNS service discovery responses. IF no listener is
+ * registered for the given service type, stops discovery for the service type.
+ *
+ * @param serviceType The type of the service to discover.
+ * @param listener The {@link MdnsServiceBrowserListener} listener.
+ */
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ public synchronized void unregisterListener(
+ @NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
+ LOGGER.log("Unregistering listener for service type: %s", serviceType);
+ MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
+ if (serviceTypeClient == null) {
+ return;
+ }
+ if (serviceTypeClient.stopSendAndReceive(listener)) {
+ // No listener is registered for the service type anymore, remove it from the list of
+ // the
+ // service type clients.
+ serviceTypeClients.remove(serviceType);
+ if (serviceTypeClients.isEmpty()) {
+ // No discovery request. Stops the socket client.
+ socketClient.stopDiscovery();
+ }
+ }
+ }
+
+ @Override
+ public synchronized void onResponseReceived(@NonNull MdnsResponse response) {
+ String[] name =
+ response.getPointerRecords().isEmpty()
+ ? null
+ : response.getPointerRecords().get(0).getName();
+ if (name != null) {
+ for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
+ String[] serviceType = serviceTypeClient.getServiceTypeLabels();
+ if ((Arrays.equals(name, serviceType)
+ || ((name.length == (serviceType.length + 2))
+ && name[1].equals(MdnsConstants.SUBTYPE_LABEL)
+ && MdnsRecord.labelsAreSuffix(serviceType, name)))) {
+ serviceTypeClient.processResponse(response);
+ return;
+ }
+ }
+ }
+ }
+
+ @Override
+ public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
+ for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
+ serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
+ }
+ }
+
+ @VisibleForTesting
+ MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType) {
+ return new MdnsServiceTypeClient(
+ serviceType, socketClient,
+ executorProvider.newServiceTypeClientSchedulerExecutor());
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
new file mode 100644
index 0000000..e35743c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Locale;
+import java.util.Objects;
+
+/** An mDNS "AAAA" or "A" record, which holds an IPv6 or IPv4 address. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsInetAddressRecord extends MdnsRecord {
+ private Inet6Address inet6Address;
+ private Inet4Address inet4Address;
+
+ /**
+ * Constructs the {@link MdnsRecord}
+ *
+ * @param name the service host name
+ * @param type the type of record (either Type 'AAAA' or Type 'A')
+ * @param reader the reader to read the record from.
+ */
+ public MdnsInetAddressRecord(String[] name, int type, MdnsPacketReader reader)
+ throws IOException {
+ super(name, type, reader);
+ }
+
+ /** Returns the IPv6 address. */
+ public Inet6Address getInet6Address() {
+ return inet6Address;
+ }
+
+ /** Returns the IPv4 address. */
+ public Inet4Address getInet4Address() {
+ return inet4Address;
+ }
+
+ @Override
+ protected void readData(MdnsPacketReader reader) throws IOException {
+ int size = 4;
+ if (super.getType() == MdnsRecord.TYPE_AAAA) {
+ size = 16;
+ }
+ byte[] buf = new byte[size];
+ reader.readBytes(buf);
+ try {
+ InetAddress address = InetAddress.getByAddress(buf);
+ if (address instanceof Inet4Address) {
+ inet4Address = (Inet4Address) address;
+ inet6Address = null;
+ } else if (address instanceof Inet6Address) {
+ inet4Address = null;
+ inet6Address = (Inet6Address) address;
+ } else {
+ inet4Address = null;
+ inet6Address = null;
+ }
+ } catch (UnknownHostException e) {
+ // Ignore exception
+ }
+ }
+
+ @Override
+ protected void writeData(MdnsPacketWriter writer) throws IOException {
+ byte[] buf = null;
+ if (inet4Address != null) {
+ buf = inet4Address.getAddress();
+ } else if (inet6Address != null) {
+ buf = inet6Address.getAddress();
+ }
+ if (buf != null) {
+ writer.writeBytes(buf);
+ }
+ }
+
+ @Override
+ public String toString() {
+ String type = "AAAA";
+ if (super.getType() == MdnsRecord.TYPE_A) {
+ type = "A";
+ }
+ return String.format(
+ Locale.ROOT, "%s: Inet4Address: %s Inet6Address: %s", type, inet4Address,
+ inet6Address);
+ }
+
+ @Override
+ public int hashCode() {
+ return (super.hashCode() * 31)
+ + Objects.hashCode(inet4Address)
+ + Objects.hashCode(inet6Address);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MdnsInetAddressRecord)) {
+ return false;
+ }
+
+ return super.equals(other)
+ && Objects.equals(inet4Address, ((MdnsInetAddressRecord) other).inet4Address)
+ && Objects.equals(inet6Address, ((MdnsInetAddressRecord) other).inet6Address);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java
new file mode 100644
index 0000000..61c5f5a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2021 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.util.SparseArray;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/** Simple decoder for mDNS packets. */
+public class MdnsPacketReader {
+ private final byte[] buf;
+ private final int count;
+ private final SparseArray<LabelEntry> labelDictionary;
+ private int pos;
+ private int limit;
+
+ /** Constructs a reader for the given packet. */
+ public MdnsPacketReader(DatagramPacket packet) {
+ buf = packet.getData();
+ count = packet.getLength();
+ pos = 0;
+ limit = -1;
+ labelDictionary = new SparseArray<>(16);
+ }
+
+ /**
+ * Sets a temporary limit (from the current read position) for subsequent reads. Any attempt to
+ * read past this limit will result in an EOFException.
+ *
+ * @param limit The new limit.
+ * @throws IOException If there is insufficient data for the new limit.
+ */
+ public void setLimit(int limit) throws IOException {
+ if (limit >= 0) {
+ if (pos + limit <= count) {
+ this.limit = pos + limit;
+ } else {
+ throw new IOException(
+ String.format(
+ Locale.ROOT,
+ "attempt to set limit beyond available data: %d exceeds %d",
+ pos + limit,
+ count));
+ }
+ }
+ }
+
+ /** Clears the limit set by {@link #setLimit}. */
+ public void clearLimit() {
+ limit = -1;
+ }
+
+ /**
+ * Returns the number of bytes left to read, between the current read position and either the
+ * limit (if set) or the end of the packet.
+ */
+ public int getRemaining() {
+ return (limit >= 0 ? limit : count) - pos;
+ }
+
+ /**
+ * Reads an unsigned 8-bit integer.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public int readUInt8() throws EOFException {
+ checkRemaining(1);
+ byte val = buf[pos++];
+ return val & 0xFF;
+ }
+
+ /**
+ * Reads an unsigned 16-bit integer.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public int readUInt16() throws EOFException {
+ checkRemaining(2);
+ int val = (buf[pos++] & 0xFF) << 8;
+ val |= (buf[pos++]) & 0xFF;
+ return val;
+ }
+
+ /**
+ * Reads an unsigned 32-bit integer.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public long readUInt32() throws EOFException {
+ checkRemaining(4);
+ long val = (long) (buf[pos++] & 0xFF) << 24;
+ val |= (long) (buf[pos++] & 0xFF) << 16;
+ val |= (long) (buf[pos++] & 0xFF) << 8;
+ val |= buf[pos++] & 0xFF;
+ return val;
+ }
+
+ /**
+ * Reads a sequence of labels and returns them as an array of strings. A sequence of labels is
+ * either a sequence of strings terminated by a NUL byte, a sequence of strings terminated by a
+ * pointer, or a pointer.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ * @throws IOException If invalid data is read.
+ */
+ public String[] readLabels() throws IOException {
+ List<String> result = new ArrayList<>(5);
+ LabelEntry previousEntry = null;
+
+ while (getRemaining() > 0) {
+ byte nextByte = peekByte();
+
+ if (nextByte == 0) {
+ // A NUL byte terminates a sequence of labels.
+ skip(1);
+ break;
+ }
+
+ int currentOffset = pos;
+
+ boolean isLabelPointer = (nextByte & 0xC0) == 0xC0;
+ if (isLabelPointer) {
+ // A pointer terminates a sequence of labels. Store the pointer value in the
+ // previous label entry.
+ int labelOffset = ((readUInt8() & 0x3F) << 8) | (readUInt8() & 0xFF);
+ if (previousEntry != null) {
+ previousEntry.nextOffset = labelOffset;
+ }
+
+ // Follow the chain of labels starting at this pointer, adding all of them onto the
+ // result.
+ while (labelOffset != 0) {
+ LabelEntry entry = labelDictionary.get(labelOffset);
+ if (entry == null) {
+ throw new IOException(
+ String.format(Locale.ROOT, "Invalid label pointer: %04X",
+ labelOffset));
+ }
+ result.add(entry.label);
+ labelOffset = entry.nextOffset;
+ }
+ break;
+ } else {
+ // It's an ordinary label. Chain it onto the previous label entry (if any), and add
+ // it onto the result.
+ String val = readString();
+ LabelEntry newEntry = new LabelEntry(val);
+ labelDictionary.put(currentOffset, newEntry);
+
+ if (previousEntry != null) {
+ previousEntry.nextOffset = currentOffset;
+ }
+ previousEntry = newEntry;
+ result.add(val);
+ }
+ }
+
+ return result.toArray(new String[result.size()]);
+ }
+
+ /**
+ * Reads a length-prefixed string.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public String readString() throws EOFException {
+ int len = readUInt8();
+ checkRemaining(len);
+ String val = new String(buf, pos, len, MdnsConstants.getUtf8Charset());
+ pos += len;
+ return val;
+ }
+
+ /**
+ * Reads a specific number of bytes.
+ *
+ * @param bytes The array to fill.
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public void readBytes(byte[] bytes) throws EOFException {
+ checkRemaining(bytes.length);
+ System.arraycopy(buf, pos, bytes, 0, bytes.length);
+ pos += bytes.length;
+ }
+
+ /**
+ * Skips over the given number of bytes.
+ *
+ * @param count The number of bytes to read and discard.
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public void skip(int count) throws EOFException {
+ checkRemaining(count);
+ pos += count;
+ }
+
+ /**
+ * Peeks at and returns the next byte in the packet, without advancing the read position.
+ *
+ * @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
+ * read.
+ */
+ public byte peekByte() throws EOFException {
+ checkRemaining(1);
+ return buf[pos];
+ }
+
+ /** Returns the current byte position of the reader for the data packet. */
+ public int getPosition() {
+ return pos;
+ }
+
+ // Checks if the number of remaining bytes to be read in the packet is at least |count|.
+ private void checkRemaining(int count) throws EOFException {
+ if (getRemaining() < count) {
+ throw new EOFException();
+ }
+ }
+
+ private static class LabelEntry {
+ public final String label;
+ public int nextOffset = 0;
+
+ public LabelEntry(String label) {
+ this.label = label;
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
new file mode 100644
index 0000000..2fed36d
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2021 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 java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Simple encoder for mDNS packets. */
+public class MdnsPacketWriter {
+ private static final int MDNS_POINTER_MASK = 0xC000;
+ private final byte[] data;
+ private final Map<Integer, String[]> labelDictionary;
+ private int pos = 0;
+ private int savedWritePos = -1;
+
+ /**
+ * Constructs a writer for a new packet.
+ *
+ * @param maxSize The maximum size of a packet.
+ */
+ public MdnsPacketWriter(int maxSize) {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("invalid size");
+ }
+
+ data = new byte[maxSize];
+ labelDictionary = new HashMap<>();
+ }
+
+ /** Returns the current write position. */
+ public int getWritePosition() {
+ return pos;
+ }
+
+ /**
+ * Saves the current write position and then rewinds the write position by the given number of
+ * bytes. This is useful for updating length fields earlier in the packet. Rewinds cannot be
+ * nested.
+ *
+ * @param position The position to rewind to.
+ * @throws IOException If the count would go beyond the beginning of the packet, or if there is
+ * already a rewind in effect.
+ */
+ public void rewind(int position) throws IOException {
+ if ((savedWritePos != -1) || (position > pos) || (position < 0)) {
+ throw new IOException("invalid rewind");
+ }
+
+ savedWritePos = pos;
+ pos = position;
+ }
+
+ /**
+ * Sets the current write position to what it was prior to the last rewind.
+ *
+ * @throws IOException If there was no rewind in effect.
+ */
+ public void unrewind() throws IOException {
+ if (savedWritePos == -1) {
+ throw new IOException("no rewind is in effect");
+ }
+ pos = savedWritePos;
+ savedWritePos = -1;
+ }
+
+ /** Clears any rewind state. */
+ public void clearRewind() {
+ savedWritePos = -1;
+ }
+
+ /**
+ * Writes an unsigned 8-bit integer.
+ *
+ * @param value The value to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeUInt8(int value) throws IOException {
+ checkRemaining(1);
+ data[pos++] = (byte) (value & 0xFF);
+ }
+
+ /**
+ * Writes an unsigned 16-bit integer.
+ *
+ * @param value The value to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeUInt16(int value) throws IOException {
+ checkRemaining(2);
+ data[pos++] = (byte) ((value >>> 8) & 0xFF);
+ data[pos++] = (byte) (value & 0xFF);
+ }
+
+ /**
+ * Writes an unsigned 32-bit integer.
+ *
+ * @param value The value to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeUInt32(long value) throws IOException {
+ checkRemaining(4);
+ data[pos++] = (byte) ((value >>> 24) & 0xFF);
+ data[pos++] = (byte) ((value >>> 16) & 0xFF);
+ data[pos++] = (byte) ((value >>> 8) & 0xFF);
+ data[pos++] = (byte) (value & 0xFF);
+ }
+
+ /**
+ * Writes a specific number of bytes.
+ *
+ * @param data The array to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeBytes(byte[] data) throws IOException {
+ checkRemaining(data.length);
+ System.arraycopy(data, 0, this.data, pos, data.length);
+ pos += data.length;
+ }
+
+ /**
+ * Writes a string.
+ *
+ * @param value The string to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeString(String value) throws IOException {
+ byte[] utf8 = value.getBytes(MdnsConstants.getUtf8Charset());
+ writeUInt8(utf8.length);
+ writeBytes(utf8);
+ }
+
+ /**
+ * Writes a series of labels. Uses name compression.
+ *
+ * @param labels The labels to write.
+ * @throws IOException If there is not enough space remaining in the packet.
+ */
+ public void writeLabels(String[] labels) throws IOException {
+ // See section 4.1.4 of RFC 1035 (http://tools.ietf.org/html/rfc1035) for a description
+ // of the name compression method used here.
+
+ int suffixLength = 0;
+ int suffixPointer = 0;
+
+ for (Map.Entry<Integer, String[]> entry : labelDictionary.entrySet()) {
+ int existingOffset = entry.getKey();
+ String[] existingLabels = entry.getValue();
+
+ if (Arrays.equals(existingLabels, labels)) {
+ writePointer(existingOffset);
+ return;
+ } else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
+ // Keep track of the longest matching suffix so far.
+ if (existingLabels.length > suffixLength) {
+ suffixLength = existingLabels.length;
+ suffixPointer = existingOffset;
+ }
+ }
+ }
+
+ if (suffixLength > 0) {
+ for (int i = 0; i < (labels.length - suffixLength); ++i) {
+ writeString(labels[i]);
+ }
+ writePointer(suffixPointer);
+ } else {
+ int[] offsets = new int[labels.length];
+ for (int i = 0; i < labels.length; ++i) {
+ offsets[i] = getWritePosition();
+ writeString(labels[i]);
+ }
+ writeUInt8(0); // NUL terminator
+
+ // Add entries to the label dictionary for each suffix of the label list, including
+ // the whole list itself.
+ for (int i = 0, len = labels.length; i < labels.length; ++i, --len) {
+ String[] value = new String[len];
+ System.arraycopy(labels, i, value, 0, len);
+ labelDictionary.put(offsets[i], value);
+ }
+ }
+ }
+
+ /** Returns the number of bytes that can still be written. */
+ public int getRemaining() {
+ return data.length - pos;
+ }
+
+ // Writes a pointer to a label.
+ private void writePointer(int offset) throws IOException {
+ writeUInt16(MDNS_POINTER_MASK | offset);
+ }
+
+ // Checks if the remaining space in the packet is at least |count|.
+ private void checkRemaining(int count) throws IOException {
+ if (getRemaining() < count) {
+ throw new IOException();
+ }
+ }
+
+ /** Builds and returns the packet. */
+ public DatagramPacket getPacket(SocketAddress destAddress) throws IOException {
+ return new DatagramPacket(data, pos, destAddress);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPointerRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPointerRecord.java
new file mode 100644
index 0000000..2b36a3c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/** An mDNS "PTR" record, which holds a name (the "pointer"). */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsPointerRecord extends MdnsRecord {
+ private String[] pointer;
+
+ public MdnsPointerRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, TYPE_PTR, reader);
+ }
+
+ /** Returns the pointer as an array of labels. */
+ public String[] getPointer() {
+ return pointer;
+ }
+
+ @Override
+ protected void readData(MdnsPacketReader reader) throws IOException {
+ pointer = reader.readLabels();
+ }
+
+ @Override
+ protected void writeData(MdnsPacketWriter writer) throws IOException {
+ writer.writeLabels(pointer);
+ }
+
+ public boolean hasSubtype() {
+ return (name != null) && (name.length > 2) && name[1].equals(MdnsConstants.SUBTYPE_LABEL);
+ }
+
+ public String getSubtype() {
+ return hasSubtype() ? name[0] : null;
+ }
+
+ @Override
+ public String toString() {
+ return "PTR: " + labelsToString(name) + " -> " + labelsToString(pointer);
+ }
+
+ @Override
+ public int hashCode() {
+ return (super.hashCode() * 31) + Arrays.hashCode(pointer);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MdnsPointerRecord)) {
+ return false;
+ }
+
+ return super.equals(other) && Arrays.equals(pointer, ((MdnsPointerRecord) other).pointer);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsRecord.java
new file mode 100644
index 0000000..4bfdb2c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsRecord.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2021 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.SystemClock;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Abstract base class for mDNS records. Stores the header fields and provides methods for reading
+ * the record from and writing it to a packet.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public abstract class MdnsRecord {
+ public static final int TYPE_A = 0x0001;
+ public static final int TYPE_AAAA = 0x001C;
+ public static final int TYPE_PTR = 0x000C;
+ public static final int TYPE_SRV = 0x0021;
+ public static final int TYPE_TXT = 0x0010;
+
+ /** Status indicating that the record is current. */
+ public static final int STATUS_OK = 0;
+ /** Status indicating that the record has expired (TTL reached 0). */
+ public static final int STATUS_EXPIRED = 1;
+ /** Status indicating that the record should be refreshed (Less than half of TTL remains.) */
+ public static final int STATUS_NEEDS_REFRESH = 2;
+
+ protected final String[] name;
+ private final int type;
+ private final int cls;
+ private final long receiptTimeMillis;
+ private final long ttlMillis;
+ private Object key;
+
+ /**
+ * Constructs a new record with the given name and type.
+ *
+ * @param reader The reader to read the record from.
+ * @throws IOException If an error occurs while reading the packet.
+ */
+ protected MdnsRecord(String[] name, int type, MdnsPacketReader reader) throws IOException {
+ this.name = name;
+ this.type = type;
+ cls = reader.readUInt16();
+ ttlMillis = TimeUnit.SECONDS.toMillis(reader.readUInt32());
+ int dataLength = reader.readUInt16();
+
+ receiptTimeMillis = SystemClock.elapsedRealtime();
+
+ reader.setLimit(dataLength);
+ readData(reader);
+ reader.clearLimit();
+ }
+
+ /**
+ * Converts an array of labels into their dot-separated string representation. This method
+ * should
+ * be used for logging purposes only.
+ */
+ public static String labelsToString(String[] labels) {
+ if (labels == null) {
+ return null;
+ }
+ return TextUtils.join(".", labels);
+ }
+
+ /** Tests if |list1| is a suffix of |list2|. */
+ public static boolean labelsAreSuffix(String[] list1, String[] list2) {
+ int offset = list2.length - list1.length;
+
+ if (offset < 1) {
+ return false;
+ }
+
+ for (int i = 0; i < list1.length; ++i) {
+ if (!list1[i].equals(list2[i + offset])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /** Returns the record's receipt (creation) time. */
+ public final long getReceiptTime() {
+ return receiptTimeMillis;
+ }
+
+ /** Returns the record's name. */
+ public String[] getName() {
+ return name;
+ }
+
+ /** Returns the record's original TTL, in milliseconds. */
+ public final long getTtl() {
+ return ttlMillis;
+ }
+
+ /** Returns the record's type. */
+ public final int getType() {
+ return type;
+ }
+
+ /**
+ * Returns the record's remaining TTL.
+ *
+ * @param now The current system time.
+ * @return The remaning TTL, in milliseconds.
+ */
+ public long getRemainingTTL(final long now) {
+ long age = now - receiptTimeMillis;
+ if (age > ttlMillis) {
+ return 0;
+ }
+
+ return ttlMillis - age;
+ }
+
+ /**
+ * Reads the record's payload from a packet.
+ *
+ * @param reader The reader to use.
+ * @throws IOException If an I/O error occurs.
+ */
+ protected abstract void readData(MdnsPacketReader reader) throws IOException;
+
+ /**
+ * Writes the record to a packet.
+ *
+ * @param writer The writer to use.
+ * @param now The current system time. This is used when writing the updated TTL.
+ */
+ @VisibleForTesting
+ public final void write(MdnsPacketWriter writer, long now) throws IOException {
+ writer.writeLabels(name);
+ writer.writeUInt16(type);
+ writer.writeUInt16(cls);
+
+ writer.writeUInt32(TimeUnit.MILLISECONDS.toSeconds(getRemainingTTL(now)));
+
+ int dataLengthPos = writer.getWritePosition();
+ writer.writeUInt16(0); // data length
+ int dataPos = writer.getWritePosition();
+
+ writeData(writer);
+
+ // Calculate amount of data written, and overwrite the data field earlier in the packet.
+ int endPos = writer.getWritePosition();
+ int dataLength = endPos - dataPos;
+ writer.rewind(dataLengthPos);
+ writer.writeUInt16(dataLength);
+ writer.unrewind();
+ }
+
+ /**
+ * Writes the record's payload to a packet.
+ *
+ * @param writer The writer to use.
+ * @throws IOException If an I/O error occurs.
+ */
+ protected abstract void writeData(MdnsPacketWriter writer) throws IOException;
+
+ /** Gets the status of the record. */
+ public int getStatus(final long now) {
+ final long age = now - receiptTimeMillis;
+ if (age > ttlMillis) {
+ return STATUS_EXPIRED;
+ }
+ if (age > (ttlMillis / 2)) {
+ return STATUS_NEEDS_REFRESH;
+ }
+ return STATUS_OK;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof MdnsRecord)) {
+ return false;
+ }
+
+ MdnsRecord otherRecord = (MdnsRecord) other;
+
+ return Arrays.equals(name, otherRecord.name) && (type == otherRecord.type);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(Arrays.hashCode(name), type);
+ }
+
+ /**
+ * Returns an opaque object that uniquely identifies this record through a combination of its
+ * type
+ * and name. Suitable for use as a key in caches.
+ */
+ public final Object getKey() {
+ if (key == null) {
+ key = new Key(type, name);
+ }
+ return key;
+ }
+
+ private static final class Key {
+ private final int recordType;
+ private final String[] recordName;
+
+ public Key(int recordType, String[] recordName) {
+ this.recordType = recordType;
+ this.recordName = recordName;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Key)) {
+ return false;
+ }
+
+ Key otherKey = (Key) other;
+
+ return (recordType == otherKey.recordType) && Arrays.equals(recordName,
+ otherKey.recordName);
+ }
+
+ @Override
+ public int hashCode() {
+ return (recordType * 31) + Arrays.hashCode(recordName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java
new file mode 100644
index 0000000..1305e07
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/** An mDNS response. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsResponse {
+ private final List<MdnsRecord> records;
+ private final List<MdnsPointerRecord> pointerRecords;
+ private MdnsServiceRecord serviceRecord;
+ private MdnsTextRecord textRecord;
+ private MdnsInetAddressRecord inet4AddressRecord;
+ private MdnsInetAddressRecord inet6AddressRecord;
+ private long lastUpdateTime;
+
+ /** Constructs a new, empty response. */
+ public MdnsResponse(long now) {
+ lastUpdateTime = now;
+ records = new LinkedList<>();
+ pointerRecords = new LinkedList<>();
+ }
+
+ // This generic typed helper compares records for equality.
+ // Returns True if records are the same.
+ private <T> boolean recordsAreSame(T a, T b) {
+ return ((a == null) && (b == null)) || ((a != null) && (b != null) && a.equals(b));
+ }
+
+ /**
+ * Adds a pointer record.
+ *
+ * @return <code>true</code> if the record was added, or <code>false</code> if a matching
+ * pointer
+ * record is already present in the response.
+ */
+ public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
+ if (!pointerRecords.contains(pointerRecord)) {
+ pointerRecords.add(pointerRecord);
+ records.add(pointerRecord);
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Gets the pointer records. */
+ public synchronized List<MdnsPointerRecord> getPointerRecords() {
+ // Returns a shallow copy.
+ return new LinkedList<>(pointerRecords);
+ }
+
+ public synchronized boolean hasPointerRecords() {
+ return !pointerRecords.isEmpty();
+ }
+
+ @VisibleForTesting
+ /* package */ synchronized void clearPointerRecords() {
+ pointerRecords.clear();
+ }
+
+ public synchronized boolean hasSubtypes() {
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ if (pointerRecord.hasSubtype()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public synchronized List<String> getSubtypes() {
+ List<String> subtypes = null;
+
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ if (pointerRecord.hasSubtype()) {
+ if (subtypes == null) {
+ subtypes = new LinkedList<>();
+ }
+ subtypes.add(pointerRecord.getSubtype());
+ }
+ }
+
+ return subtypes;
+ }
+
+ @VisibleForTesting
+ public synchronized void removeSubtypes() {
+ Iterator<MdnsPointerRecord> iter = pointerRecords.iterator();
+ while (iter.hasNext()) {
+ MdnsPointerRecord pointerRecord = iter.next();
+ if (pointerRecord.hasSubtype()) {
+ iter.remove();
+ }
+ }
+ }
+
+ /** Sets the service record. */
+ public synchronized boolean setServiceRecord(MdnsServiceRecord serviceRecord) {
+ if (recordsAreSame(this.serviceRecord, serviceRecord)) {
+ return false;
+ }
+ if (this.serviceRecord != null) {
+ records.remove(this.serviceRecord);
+ }
+ this.serviceRecord = serviceRecord;
+ if (this.serviceRecord != null) {
+ records.add(this.serviceRecord);
+ }
+ return true;
+ }
+
+ /** Gets the service record. */
+ public synchronized MdnsServiceRecord getServiceRecord() {
+ return serviceRecord;
+ }
+
+ public synchronized boolean hasServiceRecord() {
+ return serviceRecord != null;
+ }
+
+ /** Sets the text record. */
+ public synchronized boolean setTextRecord(MdnsTextRecord textRecord) {
+ if (recordsAreSame(this.textRecord, textRecord)) {
+ return false;
+ }
+ if (this.textRecord != null) {
+ records.remove(this.textRecord);
+ }
+ this.textRecord = textRecord;
+ if (this.textRecord != null) {
+ records.add(this.textRecord);
+ }
+ return true;
+ }
+
+ /** Gets the text record. */
+ public synchronized MdnsTextRecord getTextRecord() {
+ return textRecord;
+ }
+
+ public synchronized boolean hasTextRecord() {
+ return textRecord != null;
+ }
+
+ /** Sets the IPv4 address record. */
+ public synchronized boolean setInet4AddressRecord(MdnsInetAddressRecord newInet4AddressRecord) {
+ if (recordsAreSame(this.inet4AddressRecord, newInet4AddressRecord)) {
+ return false;
+ }
+ if (this.inet4AddressRecord != null) {
+ records.remove(this.inet4AddressRecord);
+ }
+ if (newInet4AddressRecord != null && newInet4AddressRecord.getInet4Address() != null) {
+ this.inet4AddressRecord = newInet4AddressRecord;
+ records.add(this.inet4AddressRecord);
+ }
+ return true;
+ }
+
+ /** Gets the IPv4 address record. */
+ public synchronized MdnsInetAddressRecord getInet4AddressRecord() {
+ return inet4AddressRecord;
+ }
+
+ public synchronized boolean hasInet4AddressRecord() {
+ return inet4AddressRecord != null;
+ }
+
+ /** Sets the IPv6 address record. */
+ public synchronized boolean setInet6AddressRecord(MdnsInetAddressRecord newInet6AddressRecord) {
+ if (recordsAreSame(this.inet6AddressRecord, newInet6AddressRecord)) {
+ return false;
+ }
+ if (this.inet6AddressRecord != null) {
+ records.remove(this.inet6AddressRecord);
+ }
+ if (newInet6AddressRecord != null && newInet6AddressRecord.getInet6Address() != null) {
+ this.inet6AddressRecord = newInet6AddressRecord;
+ records.add(this.inet6AddressRecord);
+ }
+ return true;
+ }
+
+
+ /** Gets the IPv6 address record. */
+ public synchronized MdnsInetAddressRecord getInet6AddressRecord() {
+ return inet6AddressRecord;
+ }
+
+ public synchronized boolean hasInet6AddressRecord() {
+ return inet6AddressRecord != null;
+ }
+
+ /** Gets all of the records. */
+ public synchronized List<MdnsRecord> getRecords() {
+ return new LinkedList<>(records);
+ }
+
+ /**
+ * Merges any records that are present in another response into this one.
+ *
+ * @return <code>true</code> if any records were added or updated.
+ */
+ public synchronized boolean mergeRecordsFrom(MdnsResponse other) {
+ lastUpdateTime = other.lastUpdateTime;
+
+ boolean updated = false;
+
+ List<MdnsPointerRecord> pointerRecords = other.getPointerRecords();
+ if (pointerRecords != null) {
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ if (addPointerRecord(pointerRecord)) {
+ updated = true;
+ }
+ }
+ }
+
+ MdnsServiceRecord serviceRecord = other.getServiceRecord();
+ if (serviceRecord != null) {
+ if (setServiceRecord(serviceRecord)) {
+ updated = true;
+ }
+ }
+
+ MdnsTextRecord textRecord = other.getTextRecord();
+ if (textRecord != null) {
+ if (setTextRecord(textRecord)) {
+ updated = true;
+ }
+ }
+
+ MdnsInetAddressRecord otherInet4AddressRecord = other.getInet4AddressRecord();
+ if (otherInet4AddressRecord != null && otherInet4AddressRecord.getInet4Address() != null) {
+ if (setInet4AddressRecord(otherInet4AddressRecord)) {
+ updated = true;
+ }
+ }
+
+ MdnsInetAddressRecord otherInet6AddressRecord = other.getInet6AddressRecord();
+ if (otherInet6AddressRecord != null && otherInet6AddressRecord.getInet6Address() != null) {
+ if (setInet6AddressRecord(otherInet6AddressRecord)) {
+ updated = true;
+ }
+ }
+
+ // If the hostname in the service record no longer matches the hostname in either of the
+ // address records, then drop the address records.
+ if (this.serviceRecord != null) {
+ boolean dropAddressRecords = false;
+
+ if (this.inet4AddressRecord != null) {
+ if (!Arrays.equals(
+ this.serviceRecord.getServiceHost(), this.inet4AddressRecord.getName())) {
+ dropAddressRecords = true;
+ }
+ }
+ if (this.inet6AddressRecord != null) {
+ if (!Arrays.equals(
+ this.serviceRecord.getServiceHost(), this.inet6AddressRecord.getName())) {
+ dropAddressRecords = true;
+ }
+ }
+
+ if (dropAddressRecords) {
+ setInet4AddressRecord(null);
+ setInet6AddressRecord(null);
+ updated = true;
+ }
+ }
+
+ return updated;
+ }
+
+ /**
+ * Tests if the response is complete. A response is considered complete if it contains PTR, SRV,
+ * TXT, and A (for IPv4) or AAAA (for IPv6) records.
+ */
+ public synchronized boolean isComplete() {
+ return !pointerRecords.isEmpty()
+ && (serviceRecord != null)
+ && (textRecord != null)
+ && (inet4AddressRecord != null || inet6AddressRecord != null);
+ }
+
+ /**
+ * Returns the key for this response. The key uniquely identifies the response by its service
+ * name.
+ */
+ public synchronized String getServiceInstanceName() {
+ if (pointerRecords.isEmpty()) {
+ return null;
+ }
+ String[] pointers = pointerRecords.get(0).getPointer();
+ return ((pointers != null) && (pointers.length > 0)) ? pointers[0] : null;
+ }
+
+ /**
+ * Tests if this response is a goodbye message. This will be true if a service record is present
+ * and any of the records have a TTL of 0.
+ */
+ public synchronized boolean isGoodbye() {
+ if (getServiceInstanceName() != null) {
+ for (MdnsRecord record : records) {
+ // Expiring PTR records with subtypes just signal a change in known supported
+ // criteria, not the device itself going offline, so ignore those.
+ if ((record instanceof MdnsPointerRecord)
+ && ((MdnsPointerRecord) record).hasSubtype()) {
+ continue;
+ }
+
+ if (record.getTtl() == 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Writes the response to a packet.
+ *
+ * @param writer The writer to use.
+ * @param now The current time. This is used to write updated TTLs that reflect the remaining
+ * TTL
+ * since the response was received.
+ * @return The number of records that were written.
+ * @throws IOException If an error occurred while writing (typically indicating overflow).
+ */
+ public synchronized int write(MdnsPacketWriter writer, long now) throws IOException {
+ int count = 0;
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ pointerRecord.write(writer, now);
+ ++count;
+ }
+
+ if (serviceRecord != null) {
+ serviceRecord.write(writer, now);
+ ++count;
+ }
+
+ if (textRecord != null) {
+ textRecord.write(writer, now);
+ ++count;
+ }
+
+ if (inet4AddressRecord != null) {
+ inet4AddressRecord.write(writer, now);
+ ++count;
+ }
+
+ if (inet6AddressRecord != null) {
+ inet6AddressRecord.write(writer, now);
+ ++count;
+ }
+
+ return count;
+ }
+}
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
new file mode 100644
index 0000000..72c3156
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2021 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemClock;
+
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/** A class that decodes mDNS responses from UDP packets. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsResponseDecoder {
+
+ public static final int SUCCESS = 0;
+ private static final String TAG = "MdnsResponseDecoder";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+ private final String[] serviceType;
+ private final Clock clock;
+
+ /** Constructs a new decoder that will extract responses for the given service type. */
+ public MdnsResponseDecoder(@NonNull Clock clock, @Nullable String[] serviceType) {
+ this.clock = clock;
+ this.serviceType = serviceType;
+ }
+
+ private static void skipMdnsRecord(MdnsPacketReader reader) throws IOException {
+ reader.skip(2 + 4); // skip the class and TTL
+ int dataLength = reader.readUInt16();
+ reader.skip(dataLength);
+ }
+
+ private static MdnsResponse findResponseWithPointer(
+ List<MdnsResponse> responses, String[] pointer) {
+ if (responses != null) {
+ for (MdnsResponse response : responses) {
+ List<MdnsPointerRecord> pointerRecords = response.getPointerRecords();
+ if (pointerRecords == null) {
+ continue;
+ }
+ for (MdnsPointerRecord pointerRecord : pointerRecords) {
+ if (Arrays.equals(pointerRecord.getPointer(), pointer)) {
+ return response;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private static MdnsResponse findResponseWithHostName(
+ List<MdnsResponse> responses, String[] hostName) {
+ if (responses != null) {
+ for (MdnsResponse response : responses) {
+ MdnsServiceRecord serviceRecord = response.getServiceRecord();
+ if (serviceRecord == null) {
+ continue;
+ }
+ if (Arrays.equals(serviceRecord.getServiceHost(), hostName)) {
+ return response;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Decodes all mDNS responses for the desired service type from a packet. The class does not
+ * check
+ * the responses for completeness; the caller should do that.
+ *
+ * @param packet The packet to read from.
+ * @return A list of mDNS responses, or null if the packet contained no appropriate responses.
+ */
+ public int decode(@NonNull DatagramPacket packet, @NonNull List<MdnsResponse> responses) {
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ List<MdnsRecord> records;
+ try {
+ reader.readUInt16(); // transaction ID (not used)
+ int flags = reader.readUInt16();
+ if ((flags & MdnsConstants.FLAGS_RESPONSE_MASK) != MdnsConstants.FLAGS_RESPONSE) {
+ return MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE;
+ }
+
+ int numQuestions = reader.readUInt16();
+ int numAnswers = reader.readUInt16();
+ int numAuthority = reader.readUInt16();
+ int numRecords = reader.readUInt16();
+
+ LOGGER.log(String.format(
+ "num questions: %d, num answers: %d, num authority: %d, num records: %d",
+ numQuestions, numAnswers, numAuthority, numRecords));
+
+ if (numAnswers < 1) {
+ return MdnsResponseErrorCode.ERROR_NO_ANSWERS;
+ }
+
+ records = new LinkedList<>();
+
+ for (int i = 0; i < (numAnswers + numAuthority + numRecords); ++i) {
+ String[] name;
+ try {
+ name = reader.readLabels();
+ } catch (IOException e) {
+ LOGGER.e("Failed to read labels from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_RECORD_NAME;
+ }
+ int type = reader.readUInt16();
+
+ switch (type) {
+ case MdnsRecord.TYPE_A: {
+ try {
+ records.add(new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader));
+ } catch (IOException e) {
+ LOGGER.e("Failed to read A record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_A_RDATA;
+ }
+ break;
+ }
+
+ case MdnsRecord.TYPE_AAAA: {
+ try {
+ // AAAA should only contain the IPv6 address.
+ MdnsInetAddressRecord record =
+ new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader);
+ if (record.getInet6Address() != null) {
+ records.add(record);
+ }
+ } catch (IOException e) {
+ LOGGER.e("Failed to read AAAA record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_AAAA_RDATA;
+ }
+ break;
+ }
+
+ case MdnsRecord.TYPE_PTR: {
+ try {
+ records.add(new MdnsPointerRecord(name, reader));
+ } catch (IOException e) {
+ LOGGER.e("Failed to read PTR record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_PTR_RDATA;
+ }
+ break;
+ }
+
+ case MdnsRecord.TYPE_SRV: {
+ if (name.length == 4) {
+ try {
+ records.add(new MdnsServiceRecord(name, reader));
+ } catch (IOException e) {
+ LOGGER.e("Failed to read SRV record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_SRV_RDATA;
+ }
+ } else {
+ try {
+ skipMdnsRecord(reader);
+ } catch (IOException e) {
+ LOGGER.e("Failed to skip SVR record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_SKIPPING_SRV_RDATA;
+ }
+ }
+ break;
+ }
+
+ case MdnsRecord.TYPE_TXT: {
+ try {
+ records.add(new MdnsTextRecord(name, reader));
+ } catch (IOException e) {
+ LOGGER.e("Failed to read TXT record from mDNS response.", e);
+ return MdnsResponseErrorCode.ERROR_READING_TXT_RDATA;
+ }
+ break;
+ }
+
+ default: {
+ try {
+ skipMdnsRecord(reader);
+ } catch (IOException e) {
+ LOGGER.e("Failed to skip mDNS record.", e);
+ return MdnsResponseErrorCode.ERROR_SKIPPING_UNKNOWN_RECORD;
+ }
+ }
+ }
+ }
+ } catch (EOFException e) {
+ LOGGER.e("Reached the end of the mDNS response unexpectedly.", e);
+ return MdnsResponseErrorCode.ERROR_END_OF_FILE;
+ }
+
+ // The response records are structured in a hierarchy, where some records reference
+ // others, as follows:
+ //
+ // PTR
+ // / \
+ // / \
+ // TXT SRV
+ // / \
+ // / \
+ // A AAAA
+ //
+ // But the order in which these records appear in the response packet is completely
+ // arbitrary. This means that we need to rescan the record list to construct each level of
+ // this hierarchy.
+ //
+ // PTR: service type -> service instance name
+ //
+ // SRV: service instance name -> host name (priority, weight)
+ //
+ // TXT: service instance name -> machine readable txt entries.
+ //
+ // A: host name -> IP address
+
+ // Loop 1: find PTR records, which identify distinct service instances.
+ long now = SystemClock.elapsedRealtime();
+ 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))) {
+ MdnsPointerRecord pointerRecord = (MdnsPointerRecord) record;
+ // Group PTR records that refer to the same service instance name into a single
+ // response.
+ MdnsResponse response = findResponseWithPointer(responses,
+ pointerRecord.getPointer());
+ if (response == null) {
+ response = new MdnsResponse(now);
+ responses.add(response);
+ }
+ response.addPointerRecord((MdnsPointerRecord) record);
+ }
+ }
+ }
+
+ // Loop 2: find SRV and TXT records, which reference the pointer in the PTR record.
+ for (MdnsRecord record : records) {
+ if (record instanceof MdnsServiceRecord) {
+ MdnsServiceRecord serviceRecord = (MdnsServiceRecord) record;
+ MdnsResponse response = findResponseWithPointer(responses, serviceRecord.getName());
+ if (response != null) {
+ response.setServiceRecord(serviceRecord);
+ }
+ } else if (record instanceof MdnsTextRecord) {
+ MdnsTextRecord textRecord = (MdnsTextRecord) record;
+ MdnsResponse response = findResponseWithPointer(responses, textRecord.getName());
+ if (response != null) {
+ response.setTextRecord(textRecord);
+ }
+ }
+ }
+
+ // Loop 3: find A and AAAA records, which reference the host name in the SRV record.
+ for (MdnsRecord record : records) {
+ if (record instanceof MdnsInetAddressRecord) {
+ MdnsInetAddressRecord inetRecord = (MdnsInetAddressRecord) record;
+ MdnsResponse response = findResponseWithHostName(responses, inetRecord.getName());
+ if (inetRecord.getInet4Address() != null && response != null) {
+ response.setInet4AddressRecord(inetRecord);
+ } else if (inetRecord.getInet6Address() != null && response != null) {
+ response.setInet6AddressRecord(inetRecord);
+ }
+ }
+ }
+
+ return SUCCESS;
+ }
+
+ public static class Clock {
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java
new file mode 100644
index 0000000..fcf9058
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsResponseErrorCode.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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;
+
+/**
+ * The list of error code for parsing mDNS response.
+ *
+ * @hide
+ */
+public class MdnsResponseErrorCode {
+ public static final int SUCCESS = 0;
+ public static final int ERROR_NOT_RESPONSE_MESSAGE = 1;
+ public static final int ERROR_NO_ANSWERS = 2;
+ public static final int ERROR_READING_RECORD_NAME = 3;
+ public static final int ERROR_READING_A_RDATA = 4;
+ public static final int ERROR_READING_AAAA_RDATA = 5;
+ public static final int ERROR_READING_PTR_RDATA = 6;
+ public static final int ERROR_SKIPPING_PTR_RDATA = 7;
+ public static final int ERROR_READING_SRV_RDATA = 8;
+ public static final int ERROR_SKIPPING_SRV_RDATA = 9;
+ public static final int ERROR_READING_TXT_RDATA = 10;
+ public static final int ERROR_SKIPPING_UNKNOWN_RECORD = 11;
+ public static final int ERROR_END_OF_FILE = 12;
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java
new file mode 100644
index 0000000..6e90d2c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 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.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * API configuration parameters for searching the mDNS service.
+ *
+ * <p>Use {@link MdnsSearchOptions.Builder} to create {@link MdnsSearchOptions}.
+ *
+ * @hide
+ */
+public class MdnsSearchOptions implements Parcelable {
+
+ /** @hide */
+ public static final Parcelable.Creator<MdnsSearchOptions> CREATOR =
+ new Parcelable.Creator<MdnsSearchOptions>() {
+ @Override
+ public MdnsSearchOptions createFromParcel(Parcel source) {
+ return new MdnsSearchOptions(source.createStringArrayList(),
+ source.readBoolean());
+ }
+
+ @Override
+ public MdnsSearchOptions[] newArray(int size) {
+ return new MdnsSearchOptions[size];
+ }
+ };
+ private static MdnsSearchOptions defaultOptions;
+ private final List<String> subtypes;
+
+ private final boolean isPassiveMode;
+
+ /** Parcelable constructs for a {@link MdnsServiceInfo}. */
+ MdnsSearchOptions(List<String> subtypes, boolean isPassiveMode) {
+ this.subtypes = new ArrayList<>();
+ if (subtypes != null) {
+ this.subtypes.addAll(subtypes);
+ }
+ this.isPassiveMode = isPassiveMode;
+ }
+
+ /** Returns a {@link Builder} for {@link MdnsSearchOptions}. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /** Returns a default search options. */
+ public static synchronized MdnsSearchOptions getDefaultOptions() {
+ if (defaultOptions == null) {
+ defaultOptions = newBuilder().build();
+ }
+ return defaultOptions;
+ }
+
+ /** @return the list of subtypes to search. */
+ public List<String> getSubtypes() {
+ return subtypes;
+ }
+
+ /**
+ * @return {@code true} if the passive mode is used. The passive mode scans less frequently in
+ * order to conserve battery and produce less network traffic.
+ */
+ public boolean isPassiveMode() {
+ return isPassiveMode;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeStringList(subtypes);
+ out.writeBoolean(isPassiveMode);
+ }
+
+ /** A builder to create {@link MdnsSearchOptions}. */
+ public static final class Builder {
+ private final Set<String> subtypes;
+ private boolean isPassiveMode = true;
+
+ private Builder() {
+ subtypes = new ArraySet<>();
+ }
+
+ /**
+ * Adds a subtype to search.
+ *
+ * @param subtype the subtype to add.
+ */
+ public Builder addSubtype(@NonNull String subtype) {
+ if (TextUtils.isEmpty(subtype)) {
+ throw new IllegalArgumentException("Empty subtype");
+ }
+ subtypes.add(subtype);
+ return this;
+ }
+
+ /**
+ * Adds a set of subtypes to search.
+ *
+ * @param subtypes The list of subtypes to add.
+ */
+ public Builder addSubtypes(@NonNull Collection<String> subtypes) {
+ this.subtypes.addAll(Objects.requireNonNull(subtypes));
+ return this;
+ }
+
+ /**
+ * Sets if the passive mode scan should be used. The passive mode scans less frequently in
+ * order
+ * to conserve battery and produce less network traffic.
+ *
+ * @param isPassiveMode If set to {@code true}, passive mode will be used. If set to {@code
+ * false}, active mode will be used.
+ */
+ public Builder setIsPassiveMode(boolean isPassiveMode) {
+ this.isPassiveMode = isPassiveMode;
+ return this;
+ }
+
+ /** Builds a {@link MdnsSearchOptions} with the arguments supplied to this builder. */
+ public MdnsSearchOptions build() {
+ return new MdnsSearchOptions(new ArrayList<>(subtypes), isPassiveMode);
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
new file mode 100644
index 0000000..53e58d1
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceBrowserListener.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 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.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Listener interface for mDNS service instance discovery events.
+ *
+ * @hide
+ */
+public interface MdnsServiceBrowserListener {
+
+ /**
+ * Called when an mDNS service instance is found.
+ *
+ * @param serviceInfo The found mDNS service instance.
+ */
+ void onServiceFound(@NonNull MdnsServiceInfo serviceInfo);
+
+ /**
+ * Called when an mDNS service instance is updated.
+ *
+ * @param serviceInfo The updated mDNS service instance.
+ */
+ void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo);
+
+ /**
+ * Called when an mDNS service instance is no longer valid and removed.
+ *
+ * @param serviceInstanceName The service instance name of the removed mDNS service.
+ */
+ void onServiceRemoved(@NonNull String serviceInstanceName);
+
+ /**
+ * Called when searching for mDNS service has stopped because of an error.
+ *
+ * TODO (changed when importing code): define error constants
+ *
+ * @param error The error code of the stop reason.
+ */
+ void onSearchStoppedWithError(int error);
+
+ /** Called when it failed to start an mDNS service discovery process. */
+ void onSearchFailedToStart();
+
+ /**
+ * Called when a mDNS service discovery query has been sent.
+ *
+ * @param subtypes The list of subtypes in the discovery query.
+ * @param transactionId The transaction ID of the query.
+ */
+ void onDiscoveryQuerySent(@NonNull List<String> subtypes, int transactionId);
+
+ /**
+ * Called when an error has happened when parsing a received mDNS response packet.
+ *
+ * @param receivedPacketNumber The packet sequence number of the received packet.
+ * @param errorCode The error code, defined in {@link MdnsResponseErrorCode}.
+ */
+ void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
new file mode 100644
index 0000000..2e4a4e5
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2021 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.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * A class representing a discovered mDNS service instance.
+ *
+ * @hide
+ */
+public class MdnsServiceInfo implements Parcelable {
+
+ /** @hide */
+ public static final Parcelable.Creator<MdnsServiceInfo> CREATOR =
+ new Parcelable.Creator<MdnsServiceInfo>() {
+
+ @Override
+ public MdnsServiceInfo createFromParcel(Parcel source) {
+ return new MdnsServiceInfo(
+ source.readString(),
+ source.createStringArray(),
+ source.createStringArrayList(),
+ source.createStringArray(),
+ source.readInt(),
+ source.readString(),
+ source.readString(),
+ source.createStringArrayList());
+ }
+
+ @Override
+ public MdnsServiceInfo[] newArray(int size) {
+ return new MdnsServiceInfo[size];
+ }
+ };
+
+ private final String serviceInstanceName;
+ private final String[] serviceType;
+ private final List<String> subtypes;
+ private final String[] hostName;
+ private final int port;
+ private final String ipv4Address;
+ private final String ipv6Address;
+ private final Map<String, String> attributes = new HashMap<>();
+ List<String> textStrings;
+
+ /**
+ * Constructs a {@link MdnsServiceInfo} object with default values.
+ *
+ * @hide
+ */
+ public MdnsServiceInfo(
+ String serviceInstanceName,
+ String[] serviceType,
+ List<String> subtypes,
+ String[] hostName,
+ int port,
+ String ipv4Address,
+ String ipv6Address,
+ List<String> textStrings) {
+ this.serviceInstanceName = serviceInstanceName;
+ this.serviceType = serviceType;
+ this.subtypes = new ArrayList<>();
+ if (subtypes != null) {
+ this.subtypes.addAll(subtypes);
+ }
+ this.hostName = hostName;
+ this.port = port;
+ this.ipv4Address = ipv4Address;
+ this.ipv6Address = ipv6Address;
+ if (textStrings != null) {
+ for (String text : textStrings) {
+ int pos = text.indexOf('=');
+ if (pos < 1) {
+ continue;
+ }
+ attributes.put(text.substring(0, pos).toLowerCase(Locale.ENGLISH),
+ text.substring(++pos));
+ }
+ }
+ }
+
+ /** @return the name of this service instance. */
+ public String getServiceInstanceName() {
+ return serviceInstanceName;
+ }
+
+ /** @return the type of this service instance. */
+ public String[] getServiceType() {
+ return serviceType;
+ }
+
+ /** @return the list of subtypes supported by this service instance. */
+ public List<String> getSubtypes() {
+ return new ArrayList<>(subtypes);
+ }
+
+ /**
+ * @return {@code true} if this service instance supports any subtypes.
+ * @return {@code false} if this service instance does not support any subtypes.
+ */
+ public boolean hasSubtypes() {
+ return !subtypes.isEmpty();
+ }
+
+ /** @return the host name of this service instance. */
+ public String[] getHostName() {
+ return hostName;
+ }
+
+ /** @return the port number of this service instance. */
+ public int getPort() {
+ return port;
+ }
+
+ /** @return the IPV4 address of this service instance. */
+ public String getIpv4Address() {
+ return ipv4Address;
+ }
+
+ /** @return the IPV6 address of this service instance. */
+ public String getIpv6Address() {
+ return ipv6Address;
+ }
+
+ /**
+ * @return the attribute value for {@code key}.
+ * @return {@code null} if no attribute value exists for {@code key}.
+ */
+ public String getAttributeByKey(@NonNull String key) {
+ return attributes.get(key.toLowerCase(Locale.ENGLISH));
+ }
+
+ /** @return an immutable map of all attributes. */
+ public Map<String, String> getAttributes() {
+ return Collections.unmodifiableMap(attributes);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ if (textStrings == null) {
+ // Lazily initialize the parcelable field mTextStrings.
+ textStrings = new ArrayList<>(attributes.size());
+ for (Map.Entry<String, String> kv : attributes.entrySet()) {
+ textStrings.add(String.format(Locale.ROOT, "%s=%s", kv.getKey(), kv.getValue()));
+ }
+ }
+
+ out.writeString(serviceInstanceName);
+ out.writeStringArray(serviceType);
+ out.writeStringList(subtypes);
+ out.writeStringArray(hostName);
+ out.writeInt(port);
+ out.writeString(ipv4Address);
+ out.writeString(ipv6Address);
+ out.writeStringList(textStrings);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.ROOT,
+ "Name: %s, subtypes: %s, ip: %s, port: %d",
+ serviceInstanceName,
+ TextUtils.join(",", subtypes),
+ ipv4Address,
+ port);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceRecord.java
new file mode 100644
index 0000000..51de3b2
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Objects;
+
+/** An mDNS "SRV" record, which contains service information. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsServiceRecord extends MdnsRecord {
+ public static final int PROTO_NONE = 0;
+ public static final int PROTO_TCP = 1;
+ public static final int PROTO_UDP = 2;
+ private static final String PROTO_TOKEN_TCP = "_tcp";
+ private static final String PROTO_TOKEN_UDP = "_udp";
+ private int servicePriority;
+ private int serviceWeight;
+ private int servicePort;
+ private String[] serviceHost;
+
+ public MdnsServiceRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, TYPE_SRV, reader);
+ }
+
+ /** Returns the service's port number. */
+ public int getServicePort() {
+ return servicePort;
+ }
+
+ /** Returns the service's host name. */
+ public String[] getServiceHost() {
+ return serviceHost;
+ }
+
+ /** Returns the service's priority. */
+ public int getServicePriority() {
+ return servicePriority;
+ }
+
+ /** Returns the service's weight. */
+ public int getServiceWeight() {
+ return serviceWeight;
+ }
+
+ // Format of name is <instance-name>.<service-name>.<protocol>.<domain>
+
+ /** Returns the service's instance name, which uniquely identifies the service instance. */
+ public String getServiceInstanceName() {
+ if (name.length < 1) {
+ return null;
+ }
+ return name[0];
+ }
+
+ /** Returns the service's name. */
+ public String getServiceName() {
+ if (name.length < 2) {
+ return null;
+ }
+ return name[1];
+ }
+
+ /** Returns the service's protocol. */
+ public int getServiceProtocol() {
+ if (name.length < 3) {
+ return PROTO_NONE;
+ }
+
+ String protocol = name[2];
+ if (protocol.equals(PROTO_TOKEN_TCP)) {
+ return PROTO_TCP;
+ }
+ if (protocol.equals(PROTO_TOKEN_UDP)) {
+ return PROTO_UDP;
+ }
+ return PROTO_NONE;
+ }
+
+ @Override
+ protected void readData(MdnsPacketReader reader) throws IOException {
+ servicePriority = reader.readUInt16();
+ serviceWeight = reader.readUInt16();
+ servicePort = reader.readUInt16();
+ serviceHost = reader.readLabels();
+ }
+
+ @Override
+ protected void writeData(MdnsPacketWriter writer) throws IOException {
+ writer.writeUInt16(servicePriority);
+ writer.writeUInt16(serviceWeight);
+ writer.writeUInt16(servicePort);
+ writer.writeLabels(serviceHost);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.ROOT,
+ "SRV: %s:%d (prio=%d, weight=%d)",
+ labelsToString(serviceHost),
+ servicePort,
+ servicePriority,
+ serviceWeight);
+ }
+
+ @Override
+ public int hashCode() {
+ return (super.hashCode() * 31)
+ + Objects.hash(servicePriority, serviceWeight, Arrays.hashCode(serviceHost),
+ servicePort);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MdnsServiceRecord)) {
+ return false;
+ }
+ MdnsServiceRecord otherRecord = (MdnsServiceRecord) other;
+
+ return super.equals(other)
+ && (servicePriority == otherRecord.servicePriority)
+ && (serviceWeight == otherRecord.serviceWeight)
+ && Objects.equals(serviceHost, otherRecord.serviceHost)
+ && (servicePort == otherRecord.servicePort);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
new file mode 100644
index 0000000..c3a86e3
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2021 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.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Instance of this class sends and receives mDNS packets of a given service type and invoke
+ * registered {@link MdnsServiceBrowserListener} instances.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsServiceTypeClient {
+
+ private static final int DEFAULT_MTU = 1500;
+ private static final MdnsLogger LOGGER = new MdnsLogger("MdnsServiceTypeClient");
+
+ private final String serviceType;
+ private final String[] serviceTypeLabels;
+ private final MdnsSocketClient socketClient;
+ private final ScheduledExecutorService executor;
+ private final Object lock = new Object();
+ private final Set<MdnsServiceBrowserListener> listeners = new ArraySet<>();
+ private final Map<String, MdnsResponse> instanceNameToResponse = new HashMap<>();
+
+ // The session ID increases when startSendAndReceive() is called where we schedule a
+ // QueryTask for
+ // new subtypes. It stays the same between packets for same subtypes.
+ private long currentSessionId = 0;
+
+ @GuardedBy("lock")
+ private Future<?> requestTaskFuture;
+
+ /**
+ * Constructor of {@link MdnsServiceTypeClient}.
+ *
+ * @param socketClient Sends and receives mDNS packet.
+ * @param executor A {@link ScheduledExecutorService} used to schedule query tasks.
+ */
+ public MdnsServiceTypeClient(
+ @NonNull String serviceType,
+ @NonNull MdnsSocketClient socketClient,
+ @NonNull ScheduledExecutorService executor) {
+ this.serviceType = serviceType;
+ this.socketClient = socketClient;
+ this.executor = executor;
+ serviceTypeLabels = TextUtils.split(serviceType, "\\.");
+ }
+
+ private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
+ @NonNull MdnsResponse response, @NonNull String[] serviceTypeLabels) {
+ String[] hostName = response.getServiceRecord().getServiceHost();
+ int port = response.getServiceRecord().getServicePort();
+
+ String ipv4Address = null;
+ String ipv6Address = null;
+ if (response.hasInet4AddressRecord()) {
+ ipv4Address = response.getInet4AddressRecord().getInet4Address().getHostAddress();
+ }
+ if (response.hasInet6AddressRecord()) {
+ ipv6Address = response.getInet6AddressRecord().getInet6Address().getHostAddress();
+ }
+ // TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
+ return new MdnsServiceInfo(
+ response.getServiceInstanceName(),
+ serviceTypeLabels,
+ response.getSubtypes(),
+ hostName,
+ port,
+ ipv4Address,
+ ipv6Address,
+ response.getTextRecord().getStrings());
+ }
+
+ /**
+ * Registers {@code listener} for receiving discovery event of mDNS service instances, and
+ * starts
+ * (or continue) to send mDNS queries periodically.
+ *
+ * @param listener The {@link MdnsServiceBrowserListener} to register.
+ * @param searchOptions {@link MdnsSearchOptions} contains the list of subtypes to discover.
+ */
+ public void startSendAndReceive(
+ @NonNull MdnsServiceBrowserListener listener,
+ @NonNull MdnsSearchOptions searchOptions) {
+ synchronized (lock) {
+ if (!listeners.contains(listener)) {
+ listeners.add(listener);
+ for (MdnsResponse existingResponse : instanceNameToResponse.values()) {
+ if (existingResponse.isComplete()) {
+ listener.onServiceFound(
+ buildMdnsServiceInfoFromResponse(existingResponse,
+ serviceTypeLabels));
+ }
+ }
+ }
+ // Cancel the next scheduled periodical task.
+ if (requestTaskFuture != null) {
+ requestTaskFuture.cancel(true);
+ }
+ // 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)));
+ }
+ }
+
+ /**
+ * Unregisters {@code listener} from receiving discovery event of mDNS service instances.
+ *
+ * @param listener The {@link MdnsServiceBrowserListener} to unregister.
+ * @return {@code true} if no listener is registered with this client after unregistering {@code
+ * listener}. Otherwise returns {@code false}.
+ */
+ public boolean stopSendAndReceive(@NonNull MdnsServiceBrowserListener listener) {
+ synchronized (lock) {
+ listeners.remove(listener);
+ if (listeners.isEmpty() && requestTaskFuture != null) {
+ requestTaskFuture.cancel(true);
+ requestTaskFuture = null;
+ }
+ return listeners.isEmpty();
+ }
+ }
+
+ public String[] getServiceTypeLabels() {
+ return serviceTypeLabels;
+ }
+
+ public synchronized void processResponse(@NonNull MdnsResponse response) {
+ if (response.isGoodbye()) {
+ onGoodbyeReceived(response.getServiceInstanceName());
+ } else {
+ onResponseReceived(response);
+ }
+ }
+
+ public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
+ for (MdnsServiceBrowserListener listener : listeners) {
+ listener.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
+ }
+ }
+
+ private void onResponseReceived(@NonNull MdnsResponse response) {
+ MdnsResponse currentResponse;
+ currentResponse = instanceNameToResponse.get(response.getServiceInstanceName());
+
+ boolean newServiceFound = false;
+ boolean existingServiceChanged = false;
+ if (currentResponse == null) {
+ newServiceFound = true;
+ currentResponse = response;
+ instanceNameToResponse.put(response.getServiceInstanceName(), currentResponse);
+ } else if (currentResponse.mergeRecordsFrom(response)) {
+ existingServiceChanged = true;
+ }
+ if (!currentResponse.isComplete() || (!newServiceFound && !existingServiceChanged)) {
+ return;
+ }
+ MdnsServiceInfo serviceInfo =
+ buildMdnsServiceInfoFromResponse(currentResponse, serviceTypeLabels);
+
+ for (MdnsServiceBrowserListener listener : listeners) {
+ if (newServiceFound) {
+ listener.onServiceFound(serviceInfo);
+ } else {
+ listener.onServiceUpdated(serviceInfo);
+ }
+ }
+ }
+
+ private void onGoodbyeReceived(@NonNull String serviceInstanceName) {
+ instanceNameToResponse.remove(serviceInstanceName);
+ for (MdnsServiceBrowserListener listener : listeners) {
+ listener.onServiceRemoved(serviceInstanceName);
+ }
+ }
+
+ @VisibleForTesting
+ MdnsPacketWriter createMdnsPacketWriter() {
+ return new MdnsPacketWriter(DEFAULT_MTU);
+ }
+
+ // A configuration for the PeriodicalQueryTask that contains parameters to build a query packet.
+ // Call to getConfigForNextRun returns a config that can be used to build the next query task.
+ @VisibleForTesting
+ static class QueryTaskConfig {
+
+ private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
+ (int) MdnsConfigs.initialTimeBetweenBurstsMs();
+ private static final int TIME_BETWEEN_BURSTS_MS = (int) MdnsConfigs.timeBetweenBurstsMs();
+ private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
+ private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
+ (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
+ private static final int QUERIES_PER_BURST_PASSIVE_MODE =
+ (int) MdnsConfigs.queriesPerBurstPassive();
+ private static final int UNSIGNED_SHORT_MAX_VALUE = 65536;
+ // The following fields are used by QueryTask so we need to test them.
+ @VisibleForTesting
+ final List<String> subtypes;
+ private final boolean alwaysAskForUnicastResponse =
+ MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
+ private final boolean usePassiveMode;
+ private final long sessionId;
+ @VisibleForTesting
+ int transactionId;
+ @VisibleForTesting
+ boolean expectUnicastResponse;
+ private int queriesPerBurst;
+ private int timeBetweenBurstsInMs;
+ private int burstCounter;
+ private int timeToRunNextTaskInMs;
+ private boolean isFirstBurst;
+
+ QueryTaskConfig(@NonNull Collection<String> subtypes, boolean usePassiveMode,
+ long sessionId) {
+ this.usePassiveMode = usePassiveMode;
+ this.subtypes = new ArrayList<>(subtypes);
+ this.queriesPerBurst = QUERIES_PER_BURST;
+ this.burstCounter = 0;
+ this.transactionId = 1;
+ this.expectUnicastResponse = true;
+ this.isFirstBurst = true;
+ this.sessionId = sessionId;
+ // Config the scan frequency based on the scan mode.
+ if (this.usePassiveMode) {
+ // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
+ // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
+ // queries.
+ this.timeBetweenBurstsInMs = TIME_BETWEEN_BURSTS_MS;
+ } else {
+ // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
+ // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
+ // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
+ // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
+ this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
+ }
+ }
+
+ QueryTaskConfig getConfigForNextRun() {
+ if (++transactionId > UNSIGNED_SHORT_MAX_VALUE) {
+ transactionId = 1;
+ }
+ // Only the first query expects uni-cast response.
+ expectUnicastResponse = false;
+ if (++burstCounter == queriesPerBurst) {
+ burstCounter = 0;
+
+ if (alwaysAskForUnicastResponse) {
+ expectUnicastResponse = true;
+ }
+ // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and
+ // then in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
+ // queries.
+ if (isFirstBurst) {
+ isFirstBurst = false;
+ if (usePassiveMode) {
+ queriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
+ }
+ }
+ // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
+ // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
+ // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
+ // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
+ timeToRunNextTaskInMs = timeBetweenBurstsInMs;
+ if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) {
+ timeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2,
+ TIME_BETWEEN_BURSTS_MS);
+ }
+ } else {
+ timeToRunNextTaskInMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+ }
+ return this;
+ }
+ }
+
+ // A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task.
+ private class QueryTask implements Runnable {
+
+ private final QueryTaskConfig config;
+
+ QueryTask(@NonNull QueryTaskConfig config) {
+ this.config = config;
+ }
+
+ @Override
+ public void run() {
+ Pair<Integer, List<String>> result;
+ try {
+ result =
+ new EnqueueMdnsQueryCallable(
+ socketClient,
+ createMdnsPacketWriter(),
+ serviceType,
+ config.subtypes,
+ config.expectUnicastResponse,
+ config.transactionId)
+ .call();
+ } catch (Exception e) {
+ LOGGER.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
+ TextUtils.join(",", config.subtypes)), e);
+ result = null;
+ }
+ synchronized (lock) {
+ if (MdnsConfigs.useSessionIdToScheduleMdnsTask()) {
+ // In case that the task is not canceled successfully, use session ID to check
+ // if this task should continue to schedule more.
+ if (config.sessionId != currentSessionId) {
+ return;
+ }
+ }
+
+ if (MdnsConfigs.shouldCancelScanTaskWhenFutureIsNull()) {
+ if (requestTaskFuture == null) {
+ // If requestTaskFuture is set to null, the task is cancelled. We can't use
+ // isCancelled() here because this QueryTask is different from the future
+ // that is returned from executor.schedule(). See b/71646910.
+ return;
+ }
+ }
+ if ((result != null)) {
+ for (MdnsServiceBrowserListener listener : listeners) {
+ listener.onDiscoveryQuerySent(result.second, result.first);
+ }
+ }
+ QueryTaskConfig config = this.config.getConfigForNextRun();
+ requestTaskFuture =
+ executor.schedule(
+ new QueryTask(config), config.timeToRunNextTaskInMs,
+ TimeUnit.MILLISECONDS);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java
new file mode 100644
index 0000000..241a52a
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSocket.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 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.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.util.List;
+
+/**
+ * {@link MdnsSocket} provides a similar interface to {@link MulticastSocket} and binds to all
+ * available multi-cast network interfaces.
+ *
+ * @see MulticastSocket for javadoc of each public method.
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsSocket {
+ private static final InetSocketAddress MULTICAST_IPV4_ADDRESS =
+ new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
+ private static final InetSocketAddress MULTICAST_IPV6_ADDRESS =
+ new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
+ private static boolean isOnIPv6OnlyNetwork = false;
+ private final MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider;
+ private final MulticastSocket multicastSocket;
+
+ public MdnsSocket(
+ @NonNull MulticastNetworkInterfaceProvider multicastNetworkInterfaceProvider, int port)
+ throws IOException {
+ this.multicastNetworkInterfaceProvider = multicastNetworkInterfaceProvider;
+ this.multicastNetworkInterfaceProvider.startWatchingConnectivityChanges();
+ multicastSocket = createMulticastSocket(port);
+ // RFC Spec: https://tools.ietf.org/html/rfc6762
+ // Time to live is set 255, which is similar to the jMDNS implementation.
+ multicastSocket.setTimeToLive(255);
+
+ // TODO (changed when importing code): consider tagging the socket for data usage
+ isOnIPv6OnlyNetwork = false;
+ }
+
+ public void send(DatagramPacket packet) throws IOException {
+ List<NetworkInterfaceWrapper> networkInterfaces =
+ multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+ for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+ multicastSocket.setNetworkInterface(networkInterface.getNetworkInterface());
+ multicastSocket.send(packet);
+ }
+ }
+
+ public void receive(DatagramPacket packet) throws IOException {
+ multicastSocket.receive(packet);
+ }
+
+ public void joinGroup() throws IOException {
+ List<NetworkInterfaceWrapper> networkInterfaces =
+ multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+ InetSocketAddress multicastAddress = MULTICAST_IPV4_ADDRESS;
+ if (multicastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(networkInterfaces)) {
+ isOnIPv6OnlyNetwork = true;
+ multicastAddress = MULTICAST_IPV6_ADDRESS;
+ } else {
+ isOnIPv6OnlyNetwork = false;
+ }
+ for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+ multicastSocket.joinGroup(multicastAddress, networkInterface.getNetworkInterface());
+ }
+ }
+
+ public void leaveGroup() throws IOException {
+ List<NetworkInterfaceWrapper> networkInterfaces =
+ multicastNetworkInterfaceProvider.getMulticastNetworkInterfaces();
+ InetSocketAddress multicastAddress = MULTICAST_IPV4_ADDRESS;
+ if (multicastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(networkInterfaces)) {
+ multicastAddress = MULTICAST_IPV6_ADDRESS;
+ }
+ for (NetworkInterfaceWrapper networkInterface : networkInterfaces) {
+ multicastSocket.leaveGroup(multicastAddress, networkInterface.getNetworkInterface());
+ }
+ }
+
+ public void close() {
+ // This is a race with the use of the file descriptor (b/27403984).
+ multicastSocket.close();
+ multicastNetworkInterfaceProvider.stopWatchingConnectivityChanges();
+ }
+
+ @VisibleForTesting
+ MulticastSocket createMulticastSocket(int port) throws IOException {
+ return new MulticastSocket(port);
+ }
+
+ public boolean isOnIPv6OnlyNetwork() {
+ return isOnIPv6OnlyNetwork;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java
new file mode 100644
index 0000000..e689d6c
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2021 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.Manifest.permission;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.net.wifi.WifiManager.MulticastLock;
+import android.os.SystemClock;
+import android.text.format.DateUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * The {@link MdnsSocketClient} maintains separate threads to send and receive mDNS packets for all
+ * the requested service types.
+ *
+ * <p>See https://tools.ietf.org/html/rfc6763 (namely sections 4 and 5).
+ */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class MdnsSocketClient {
+
+ private static final String TAG = "MdnsClient";
+ // TODO: The following values are copied from cast module. We need to think about the
+ // better way to share those.
+ private static final String CAST_SENDER_LOG_SOURCE = "CAST_SENDER_SDK";
+ private static final String CAST_PREFS_NAME = "google_cast";
+ private static final String PREF_CAST_SENDER_ID = "PREF_CAST_SENDER_ID";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+ private static final String MULTICAST_TYPE = "multicast";
+ private static final String UNICAST_TYPE = "unicast";
+
+ private static final long SLEEP_TIME_FOR_SOCKET_THREAD_MS =
+ MdnsConfigs.sleepTimeForSocketThreadMs();
+ // A value of 0 leads to an infinite wait.
+ private static final long THREAD_JOIN_TIMEOUT_MS = DateUtils.SECOND_IN_MILLIS;
+ private static final int RECEIVER_BUFFER_SIZE = 2048;
+ @VisibleForTesting
+ final Queue<DatagramPacket> multicastPacketQueue = new ArrayDeque<>();
+ @VisibleForTesting
+ final Queue<DatagramPacket> unicastPacketQueue = new ArrayDeque<>();
+ private final Context context;
+ private final byte[] multicastReceiverBuffer = new byte[RECEIVER_BUFFER_SIZE];
+ private final byte[] unicastReceiverBuffer;
+ private final MdnsResponseDecoder responseDecoder;
+ private final MulticastLock multicastLock;
+ private final boolean useSeparateSocketForUnicast =
+ MdnsConfigs.useSeparateSocketToSendUnicastQuery();
+ private final boolean checkMulticastResponse = MdnsConfigs.checkMulticastResponse();
+ private final long checkMulticastResponseIntervalMs =
+ MdnsConfigs.checkMulticastResponseIntervalMs();
+ private final Object socketLock = new Object();
+ private final Object timerObject = new Object();
+ // If multicast response was received in the current session. The value is reset in the
+ // beginning of each session.
+ @VisibleForTesting
+ boolean receivedMulticastResponse;
+ // If unicast response was received in the current session. The value is reset in the beginning
+ // of each session.
+ @VisibleForTesting
+ boolean receivedUnicastResponse;
+ // If the phone is the bad state where it can't receive any multicast response.
+ @VisibleForTesting
+ AtomicBoolean cannotReceiveMulticastResponse = new AtomicBoolean(false);
+ @VisibleForTesting
+ volatile Thread sendThread;
+ @VisibleForTesting
+ Thread multicastReceiveThread;
+ @VisibleForTesting
+ Thread unicastReceiveThread;
+ private volatile boolean shouldStopSocketLoop;
+ private Callback callback;
+ private MdnsSocket multicastSocket;
+ private MdnsSocket unicastSocket;
+ private int receivedPacketNumber = 0;
+ private Timer logMdnsPacketTimer;
+ private AtomicInteger packetsCount;
+ private Timer checkMulticastResponseTimer;
+
+ public MdnsSocketClient(@NonNull Context context, @NonNull MulticastLock multicastLock) {
+ this.context = context;
+ this.multicastLock = multicastLock;
+ responseDecoder = new MdnsResponseDecoder(new MdnsResponseDecoder.Clock(), null);
+ if (useSeparateSocketForUnicast) {
+ unicastReceiverBuffer = new byte[RECEIVER_BUFFER_SIZE];
+ } else {
+ unicastReceiverBuffer = null;
+ }
+ }
+
+ public synchronized void setCallback(@Nullable Callback callback) {
+ this.callback = callback;
+ }
+
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ public synchronized void startDiscovery() throws IOException {
+ if (multicastSocket != null) {
+ LOGGER.w("Discovery is already in progress.");
+ return;
+ }
+
+ receivedMulticastResponse = false;
+ receivedUnicastResponse = false;
+ cannotReceiveMulticastResponse.set(false);
+
+ shouldStopSocketLoop = false;
+ try {
+ // TODO (changed when importing code): consider setting thread stats tag
+ multicastSocket = createMdnsSocket(MdnsConstants.MDNS_PORT);
+ multicastSocket.joinGroup();
+ if (useSeparateSocketForUnicast) {
+ // For unicast, use port 0 and the system will assign it with any available port.
+ unicastSocket = createMdnsSocket(0);
+ }
+ multicastLock.acquire();
+ } catch (IOException e) {
+ multicastLock.release();
+ if (multicastSocket != null) {
+ multicastSocket.close();
+ multicastSocket = null;
+ }
+ if (unicastSocket != null) {
+ unicastSocket.close();
+ unicastSocket = null;
+ }
+ throw e;
+ } finally {
+ // TODO (changed when importing code): consider resetting thread stats tag
+ }
+ createAndStartSendThread();
+ createAndStartReceiverThreads();
+ }
+
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ public void stopDiscovery() {
+ LOGGER.log("Stop discovery.");
+ if (multicastSocket == null && unicastSocket == null) {
+ return;
+ }
+
+ if (MdnsConfigs.clearMdnsPacketQueueAfterDiscoveryStops()) {
+ synchronized (multicastPacketQueue) {
+ multicastPacketQueue.clear();
+ }
+ synchronized (unicastPacketQueue) {
+ unicastPacketQueue.clear();
+ }
+ }
+
+ multicastLock.release();
+
+ shouldStopSocketLoop = true;
+ waitForSendThreadToStop();
+ waitForReceiverThreadsToStop();
+
+ synchronized (socketLock) {
+ multicastSocket = null;
+ unicastSocket = null;
+ }
+
+ synchronized (timerObject) {
+ if (checkMulticastResponseTimer != null) {
+ checkMulticastResponseTimer.cancel();
+ checkMulticastResponseTimer = null;
+ }
+ }
+ }
+
+ /** Sends a mDNS request packet that asks for multicast response. */
+ public void sendMulticastPacket(@NonNull DatagramPacket packet) {
+ sendMdnsPacket(packet, multicastPacketQueue);
+ }
+
+ /** Sends a mDNS request packet that asks for unicast response. */
+ public void sendUnicastPacket(DatagramPacket packet) {
+ if (useSeparateSocketForUnicast) {
+ sendMdnsPacket(packet, unicastPacketQueue);
+ } else {
+ sendMdnsPacket(packet, multicastPacketQueue);
+ }
+ }
+
+ private void sendMdnsPacket(DatagramPacket packet, Queue<DatagramPacket> packetQueueToUse) {
+ if (shouldStopSocketLoop && !MdnsConfigs.allowAddMdnsPacketAfterDiscoveryStops()) {
+ LOGGER.w("sendMdnsPacket() is called after discovery already stopped");
+ return;
+ }
+ synchronized (packetQueueToUse) {
+ while (packetQueueToUse.size() >= MdnsConfigs.mdnsPacketQueueMaxSize()) {
+ packetQueueToUse.remove();
+ }
+ packetQueueToUse.add(packet);
+ }
+ triggerSendThread();
+ }
+
+ private void createAndStartSendThread() {
+ if (sendThread != null) {
+ LOGGER.w("A socket thread already exists.");
+ return;
+ }
+ sendThread = new Thread(this::sendThreadMain);
+ sendThread.setName("mdns-send");
+ sendThread.start();
+ }
+
+ private void createAndStartReceiverThreads() {
+ if (multicastReceiveThread != null) {
+ LOGGER.w("A multicast receiver thread already exists.");
+ return;
+ }
+ multicastReceiveThread =
+ new Thread(() -> receiveThreadMain(multicastReceiverBuffer, multicastSocket));
+ multicastReceiveThread.setName("mdns-multicast-receive");
+ multicastReceiveThread.start();
+
+ if (useSeparateSocketForUnicast) {
+ unicastReceiveThread =
+ new Thread(() -> receiveThreadMain(unicastReceiverBuffer, unicastSocket));
+ unicastReceiveThread.setName("mdns-unicast-receive");
+ unicastReceiveThread.start();
+ }
+ }
+
+ private void triggerSendThread() {
+ LOGGER.log("Trigger send thread.");
+ Thread sendThread = this.sendThread;
+ if (sendThread != null) {
+ sendThread.interrupt();
+ } else {
+ LOGGER.w("Socket thread is null");
+ }
+ }
+
+ private void waitForReceiverThreadsToStop() {
+ if (multicastReceiveThread != null) {
+ waitForThread(multicastReceiveThread);
+ multicastReceiveThread = null;
+ }
+
+ if (unicastReceiveThread != null) {
+ waitForThread(unicastReceiveThread);
+ unicastReceiveThread = null;
+ }
+ }
+
+ private void waitForSendThreadToStop() {
+ LOGGER.log("wait For Send Thread To Stop");
+ if (sendThread == null) {
+ LOGGER.w("socket thread is already dead.");
+ return;
+ }
+ waitForThread(sendThread);
+ sendThread = null;
+ }
+
+ private void waitForThread(Thread thread) {
+ long startMs = SystemClock.elapsedRealtime();
+ long waitMs = THREAD_JOIN_TIMEOUT_MS;
+ while (thread.isAlive() && (waitMs > 0)) {
+ try {
+ thread.interrupt();
+ thread.join(waitMs);
+ if (thread.isAlive()) {
+ LOGGER.w("Failed to join thread: " + thread);
+ }
+ break;
+ } catch (InterruptedException e) {
+ // Compute remaining time after at least a single join call, in case the clock
+ // resolution is poor.
+ waitMs = THREAD_JOIN_TIMEOUT_MS - (SystemClock.elapsedRealtime() - startMs);
+ }
+ }
+ }
+
+ private void sendThreadMain() {
+ List<DatagramPacket> multicastPacketsToSend = new ArrayList<>();
+ List<DatagramPacket> unicastPacketsToSend = new ArrayList<>();
+ boolean shouldThreadSleep;
+ try {
+ while (!shouldStopSocketLoop) {
+ try {
+ // Make a local copy of all packets, and clear the queue.
+ // Send packets that ask for multicast response.
+ multicastPacketsToSend.clear();
+ synchronized (multicastPacketQueue) {
+ multicastPacketsToSend.addAll(multicastPacketQueue);
+ multicastPacketQueue.clear();
+ }
+
+ // Send packets that ask for unicast response.
+ if (useSeparateSocketForUnicast) {
+ unicastPacketsToSend.clear();
+ synchronized (unicastPacketQueue) {
+ unicastPacketsToSend.addAll(unicastPacketQueue);
+ unicastPacketQueue.clear();
+ }
+ }
+
+ // Send all the packets.
+ sendPackets(multicastPacketsToSend, multicastSocket);
+ sendPackets(unicastPacketsToSend, unicastSocket);
+
+ // Sleep ONLY if no more packets have been added to the queue, while packets
+ // were being sent.
+ synchronized (multicastPacketQueue) {
+ synchronized (unicastPacketQueue) {
+ shouldThreadSleep =
+ multicastPacketQueue.isEmpty() && unicastPacketQueue.isEmpty();
+ }
+ }
+ if (shouldThreadSleep) {
+ Thread.sleep(SLEEP_TIME_FOR_SOCKET_THREAD_MS);
+ }
+ } catch (InterruptedException e) {
+ // Don't log the interruption as it's expected.
+ }
+ }
+ } finally {
+ LOGGER.log("Send thread stopped.");
+ try {
+ multicastSocket.leaveGroup();
+ } catch (Exception t) {
+ LOGGER.e("Failed to leave the group.", t);
+ }
+
+ // Close the socket first. This is the only way to interrupt a blocking receive.
+ try {
+ // This is a race with the use of the file descriptor (b/27403984).
+ multicastSocket.close();
+ if (unicastSocket != null) {
+ unicastSocket.close();
+ }
+ } catch (Exception t) {
+ LOGGER.e("Failed to close the mdns socket.", t);
+ }
+ }
+ }
+
+ private void receiveThreadMain(byte[] receiverBuffer, MdnsSocket socket) {
+ DatagramPacket packet = new DatagramPacket(receiverBuffer, receiverBuffer.length);
+
+ while (!shouldStopSocketLoop) {
+ try {
+ // This is a race with the use of the file descriptor (b/27403984).
+ synchronized (socketLock) {
+ // This checks is to make sure the socket was not set to null.
+ if (socket != null && (socket == multicastSocket || socket == unicastSocket)) {
+ socket.receive(packet);
+ }
+ }
+
+ if (!shouldStopSocketLoop) {
+ String responseType = socket == multicastSocket ? MULTICAST_TYPE : UNICAST_TYPE;
+ processResponsePacket(packet, responseType);
+ }
+ } catch (IOException e) {
+ if (!shouldStopSocketLoop) {
+ LOGGER.e("Failed to receive mDNS packets.", e);
+ }
+ }
+ }
+ LOGGER.log("Receive thread stopped.");
+ }
+
+ private int processResponsePacket(@NonNull DatagramPacket packet, String responseType)
+ throws IOException {
+ int packetNumber = ++receivedPacketNumber;
+
+ List<MdnsResponse> responses = new LinkedList<>();
+ int errorCode = responseDecoder.decode(packet, responses);
+ if (errorCode == MdnsResponseDecoder.SUCCESS) {
+ if (responseType.equals(MULTICAST_TYPE)) {
+ receivedMulticastResponse = true;
+ if (cannotReceiveMulticastResponse.getAndSet(false)) {
+ // If we are already in the bad state, receiving a multicast response means
+ // we are recovered.
+ LOGGER.e(
+ "Recovered from the state where the phone can't receive any multicast"
+ + " response");
+ }
+ } else {
+ receivedUnicastResponse = true;
+ }
+ for (MdnsResponse response : responses) {
+ String serviceInstanceName = response.getServiceInstanceName();
+ LOGGER.log("mDNS %s response received: %s", responseType, serviceInstanceName);
+ if (callback != null) {
+ callback.onResponseReceived(response);
+ }
+ }
+ } else if (errorCode != MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE) {
+ LOGGER.w(String.format("Error while decoding %s packet (%d): %d",
+ responseType, packetNumber, errorCode));
+ if (callback != null) {
+ callback.onFailedToParseMdnsResponse(packetNumber, errorCode);
+ }
+ }
+ return errorCode;
+ }
+
+ @VisibleForTesting
+ MdnsSocket createMdnsSocket(int port) throws IOException {
+ return new MdnsSocket(new MulticastNetworkInterfaceProvider(context), port);
+ }
+
+ private void sendPackets(List<DatagramPacket> packets, MdnsSocket socket) {
+ String requestType = socket == multicastSocket ? "multicast" : "unicast";
+ for (DatagramPacket packet : packets) {
+ if (shouldStopSocketLoop) {
+ break;
+ }
+ try {
+ LOGGER.log("Sending a %s mDNS packet...", requestType);
+ socket.send(packet);
+
+ // Start the timer task to monitor the response.
+ synchronized (timerObject) {
+ if (socket == multicastSocket) {
+ if (cannotReceiveMulticastResponse.get()) {
+ // Don't schedule the timer task if we are already in the bad state.
+ return;
+ }
+ if (checkMulticastResponseTimer != null) {
+ // Don't schedule the timer task if it's already scheduled.
+ return;
+ }
+ if (checkMulticastResponse && useSeparateSocketForUnicast) {
+ // Only when useSeparateSocketForUnicast is true, we can tell if we
+ // received a multicast or unicast response.
+ checkMulticastResponseTimer = new Timer();
+ checkMulticastResponseTimer.schedule(
+ new TimerTask() {
+ @Override
+ public void run() {
+ synchronized (timerObject) {
+ if (checkMulticastResponseTimer == null) {
+ // Discovery already stopped.
+ return;
+ }
+ if ((!receivedMulticastResponse)
+ && receivedUnicastResponse) {
+ LOGGER.e(String.format(
+ "Haven't received multicast response"
+ + " in the last %d ms.",
+ checkMulticastResponseIntervalMs));
+ cannotReceiveMulticastResponse.set(true);
+ }
+ checkMulticastResponseTimer = null;
+ }
+ }
+ },
+ checkMulticastResponseIntervalMs);
+ }
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.e(String.format("Failed to send a %s mDNS packet.", requestType), e);
+ }
+ }
+ packets.clear();
+ }
+
+ public boolean isOnIPv6OnlyNetwork() {
+ return multicastSocket.isOnIPv6OnlyNetwork();
+ }
+
+ /** Callback for {@link MdnsSocketClient}. */
+ public interface Callback {
+ void onResponseReceived(@NonNull MdnsResponse response);
+
+ void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java
new file mode 100644
index 0000000..a5b5595
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2021 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 com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/** An mDNS "TXT" record, which contains a list of text strings. */
+// TODO(b/177655645): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+@VisibleForTesting
+public class MdnsTextRecord extends MdnsRecord {
+ private List<String> strings;
+
+ public MdnsTextRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, TYPE_TXT, reader);
+ }
+
+ /** Returns the list of strings. */
+ public List<String> getStrings() {
+ return Collections.unmodifiableList(strings);
+ }
+
+ @Override
+ protected void readData(MdnsPacketReader reader) throws IOException {
+ strings = new ArrayList<>();
+ while (reader.getRemaining() > 0) {
+ strings.add(reader.readString());
+ }
+ }
+
+ @Override
+ protected void writeData(MdnsPacketWriter writer) throws IOException {
+ if (strings != null) {
+ for (String string : strings) {
+ writer.writeString(string);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("TXT: {");
+ if (strings != null) {
+ for (String string : strings) {
+ sb.append(' ').append(string);
+ }
+ }
+ sb.append("}");
+
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return (super.hashCode() * 31) + Objects.hash(strings);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof MdnsTextRecord)) {
+ return false;
+ }
+
+ return super.equals(other) && Objects.equals(strings, ((MdnsTextRecord) other).strings);
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java b/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java
new file mode 100644
index 0000000..e0d8fa6
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProvider.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2021 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsLogger;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * This class is used by the {@link MdnsSocket} to monitor the list of {@link NetworkInterface}
+ * instances that are currently available for multi-cast messaging.
+ */
+public class MulticastNetworkInterfaceProvider {
+
+ private static final String TAG = "MdnsNIProvider";
+ private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
+ private static final boolean PREFER_IPV6 = MdnsConfigs.preferIpv6();
+
+ private final List<NetworkInterfaceWrapper> multicastNetworkInterfaces = new ArrayList<>();
+ // Only modifiable from tests.
+ @VisibleForTesting
+ ConnectivityMonitor connectivityMonitor;
+ private volatile boolean connectivityChanged = true;
+
+ @SuppressWarnings("nullness:methodref.receiver.bound")
+ public MulticastNetworkInterfaceProvider(@NonNull Context context) {
+ // IMPORT CHANGED
+ this.connectivityMonitor = new ConnectivityMonitorWithConnectivityManager(
+ context, this::onConnectivityChanged);
+ }
+
+ private void onConnectivityChanged() {
+ connectivityChanged = true;
+ }
+
+ /**
+ * Starts monitoring changes of connectivity of this device, which may indicate that the list of
+ * network interfaces available for multi-cast messaging has changed.
+ */
+ public void startWatchingConnectivityChanges() {
+ connectivityMonitor.startWatchingConnectivityChanges();
+ }
+
+ /** Stops monitoring changes of connectivity. */
+ public void stopWatchingConnectivityChanges() {
+ connectivityMonitor.stopWatchingConnectivityChanges();
+ }
+
+ /**
+ * Returns the list of {@link NetworkInterfaceWrapper} instances available for multi-cast
+ * messaging.
+ */
+ public synchronized List<NetworkInterfaceWrapper> getMulticastNetworkInterfaces() {
+ if (connectivityChanged) {
+ connectivityChanged = false;
+ updateMulticastNetworkInterfaces();
+ if (multicastNetworkInterfaces.isEmpty()) {
+ LOGGER.log("No network interface available for mDNS scanning.");
+ }
+ }
+ return new ArrayList<>(multicastNetworkInterfaces);
+ }
+
+ private void updateMulticastNetworkInterfaces() {
+ multicastNetworkInterfaces.clear();
+ List<NetworkInterfaceWrapper> networkInterfaceWrappers = getNetworkInterfaces();
+ for (NetworkInterfaceWrapper interfaceWrapper : networkInterfaceWrappers) {
+ if (canScanOnInterface(interfaceWrapper)) {
+ multicastNetworkInterfaces.add(interfaceWrapper);
+ }
+ }
+ }
+
+ public boolean isOnIpV6OnlyNetwork(List<NetworkInterfaceWrapper> networkInterfaces) {
+ if (networkInterfaces.isEmpty()) {
+ return false;
+ }
+
+ // TODO(b/79866499): Remove this when the bug is resolved.
+ if (PREFER_IPV6) {
+ return true;
+ }
+ boolean hasAtleastOneIPv6Address = false;
+ for (NetworkInterfaceWrapper interfaceWrapper : networkInterfaces) {
+ for (InterfaceAddress ifAddr : interfaceWrapper.getInterfaceAddresses()) {
+ if (!(ifAddr.getAddress() instanceof Inet6Address)) {
+ return false;
+ } else {
+ hasAtleastOneIPv6Address = true;
+ }
+ }
+ }
+ return hasAtleastOneIPv6Address;
+ }
+
+ @VisibleForTesting
+ List<NetworkInterfaceWrapper> getNetworkInterfaces() {
+ List<NetworkInterfaceWrapper> networkInterfaceWrappers = new ArrayList<>();
+ try {
+ Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+ if (interfaces != null) {
+ while (interfaces.hasMoreElements()) {
+ networkInterfaceWrappers.add(
+ new NetworkInterfaceWrapper(interfaces.nextElement()));
+ }
+ }
+ } catch (SocketException e) {
+ LOGGER.e("Failed to get network interfaces.", e);
+ } catch (NullPointerException e) {
+ // Android R has a bug that could lead to a NPE. See b/159277702.
+ LOGGER.e("Failed to call getNetworkInterfaces API", e);
+ }
+
+ return networkInterfaceWrappers;
+ }
+
+ private boolean canScanOnInterface(@Nullable NetworkInterfaceWrapper networkInterface) {
+ try {
+ if ((networkInterface == null)
+ || networkInterface.isLoopback()
+ || networkInterface.isPointToPoint()
+ || networkInterface.isVirtual()
+ || !networkInterface.isUp()
+ || !networkInterface.supportsMulticast()) {
+ return false;
+ }
+ return hasInet4Address(networkInterface) || hasInet6Address(networkInterface);
+ } catch (IOException e) {
+ LOGGER.e(String.format("Failed to check interface %s.",
+ networkInterface.getNetworkInterface().getDisplayName()), e);
+ }
+
+ return false;
+ }
+
+ private boolean hasInet4Address(@NonNull NetworkInterfaceWrapper networkInterface) {
+ for (InterfaceAddress ifAddr : networkInterface.getInterfaceAddresses()) {
+ if (ifAddr.getAddress() instanceof Inet4Address) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasInet6Address(@NonNull NetworkInterfaceWrapper networkInterface) {
+ for (InterfaceAddress ifAddr : networkInterface.getInterfaceAddresses()) {
+ if (ifAddr.getAddress() instanceof Inet6Address) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java b/service/mdns/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java
new file mode 100644
index 0000000..0ecae48
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/NetworkInterfaceWrapper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 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 java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.List;
+
+/** A wrapper class of {@link NetworkInterface} to be mocked in unit tests. */
+public class NetworkInterfaceWrapper {
+ private final NetworkInterface networkInterface;
+
+ public NetworkInterfaceWrapper(NetworkInterface networkInterface) {
+ this.networkInterface = networkInterface;
+ }
+
+ public NetworkInterface getNetworkInterface() {
+ return networkInterface;
+ }
+
+ public boolean isUp() throws SocketException {
+ return networkInterface.isUp();
+ }
+
+ public boolean isLoopback() throws SocketException {
+ return networkInterface.isLoopback();
+ }
+
+ public boolean isPointToPoint() throws SocketException {
+ return networkInterface.isPointToPoint();
+ }
+
+ public boolean isVirtual() {
+ return networkInterface.isVirtual();
+ }
+
+ public boolean supportsMulticast() throws SocketException {
+ return networkInterface.supportsMulticast();
+ }
+
+ public List<InterfaceAddress> getInterfaceAddresses() {
+ return networkInterface.getInterfaceAddresses();
+ }
+
+ @Override
+ public String toString() {
+ return networkInterface.toString();
+ }
+}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java b/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java
new file mode 100644
index 0000000..431f1fd
--- /dev/null
+++ b/service/mdns/com/android/server/connectivity/mdns/util/MdnsLogger.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 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.util;
+
+import android.text.TextUtils;
+
+import com.android.net.module.util.SharedLog;
+
+/**
+ * The logger used in mDNS.
+ */
+public class MdnsLogger {
+ // Make this logger public for other level logging than dogfood.
+ public final SharedLog mLog;
+
+ /**
+ * Constructs a new {@link MdnsLogger} with the given logging tag.
+ *
+ * @param tag The log tag that will be used by this logger
+ */
+ public MdnsLogger(String tag) {
+ mLog = new SharedLog(tag);
+ }
+
+ public void log(String message) {
+ mLog.log(message);
+ }
+
+ public void log(String message, Object... args) {
+ mLog.log(message + " ; " + TextUtils.join(" ; ", args));
+ }
+
+ public void d(String message) {
+ mLog.log(message);
+ }
+
+ public void e(String message) {
+ mLog.e(message);
+ }
+
+ public void e(String message, Throwable e) {
+ mLog.e(message, e);
+ }
+
+ public void w(String message) {
+ mLog.w(message);
+ }
+}
diff --git a/service/native/Android.bp b/service/native/Android.bp
index cb26bc3..697fcbd 100644
--- a/service/native/Android.bp
+++ b/service/native/Android.bp
@@ -52,7 +52,8 @@
cc_test {
name: "traffic_controller_unit_test",
- test_suites: ["general-tests"],
+ test_suites: ["general-tests", "mts-tethering"],
+ test_config_template: ":net_native_test_config_template",
require_root: true,
local_include_dirs: ["include"],
header_libs: [
@@ -71,4 +72,13 @@
"libnetd_updatable",
"netd_aidl_interface-lateststable-ndk",
],
+ compile_multilib: "both",
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
}
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index 3e98edb..9331548 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -56,7 +56,6 @@
using bpf::BpfMap;
using bpf::synchronizeKernelRCU;
using netdutils::DumpWriter;
-using netdutils::getIfaceList;
using netdutils::NetlinkListener;
using netdutils::NetlinkListenerInterface;
using netdutils::ScopedIndent;
@@ -74,6 +73,9 @@
const char* TrafficController::LOCAL_POWERSAVE = "fw_powersave";
const char* TrafficController::LOCAL_RESTRICTED = "fw_restricted";
const char* TrafficController::LOCAL_LOW_POWER_STANDBY = "fw_low_power_standby";
+const char* TrafficController::LOCAL_OEM_DENY_1 = "fw_oem_deny_1";
+const char* TrafficController::LOCAL_OEM_DENY_2 = "fw_oem_deny_2";
+const char* TrafficController::LOCAL_OEM_DENY_3 = "fw_oem_deny_3";
static_assert(BPF_PERMISSION_INTERNET == INetd::PERMISSION_INTERNET,
"Mismatch between BPF and AIDL permissions: PERMISSION_INTERNET");
@@ -88,7 +90,7 @@
} \
} while (0)
-const std::string uidMatchTypeToString(uint8_t match) {
+const std::string uidMatchTypeToString(uint32_t match) {
std::string matchType;
FLAG_MSG_TRANS(matchType, HAPPY_BOX_MATCH, match);
FLAG_MSG_TRANS(matchType, PENALTY_BOX_MATCH, match);
@@ -98,20 +100,16 @@
FLAG_MSG_TRANS(matchType, RESTRICTED_MATCH, match);
FLAG_MSG_TRANS(matchType, LOW_POWER_STANDBY_MATCH, match);
FLAG_MSG_TRANS(matchType, IIF_MATCH, match);
+ FLAG_MSG_TRANS(matchType, LOCKDOWN_VPN_MATCH, match);
+ FLAG_MSG_TRANS(matchType, OEM_DENY_1_MATCH, match);
+ FLAG_MSG_TRANS(matchType, OEM_DENY_2_MATCH, match);
+ FLAG_MSG_TRANS(matchType, OEM_DENY_3_MATCH, match);
if (match) {
return StringPrintf("Unknown match: %u", match);
}
return matchType;
}
-bool TrafficController::hasUpdateDeviceStatsPermission(uid_t uid) {
- // This implementation is the same logic as method ActivityManager#checkComponentPermission.
- // It implies that the calling uid can never be the same as PER_USER_RANGE.
- uint32_t appId = uid % PER_USER_RANGE;
- return ((appId == AID_ROOT) || (appId == AID_SYSTEM) ||
- mPrivilegedUser.find(appId) != mPrivilegedUser.end());
-}
-
const std::string UidPermissionTypeToString(int permission) {
if (permission == INetd::PERMISSION_NONE) {
return "PERMISSION_NONE";
@@ -183,6 +181,7 @@
RETURN_IF_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
RETURN_IF_NOT_OK(mUidOwnerMap.clear());
RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
+ ALOGI("%s successfully", __func__);
return netdutils::status::ok;
}
@@ -190,16 +189,6 @@
Status TrafficController::start() {
RETURN_IF_NOT_OK(initMaps());
- // Fetch the list of currently-existing interfaces. At this point NetlinkHandler is
- // already running, so it will call addInterface() when any new interface appears.
- // TODO: Clean-up addInterface() after interface monitoring is in
- // NetworkStatsService.
- std::map<std::string, uint32_t> ifacePairs;
- ASSIGN_OR_RETURN(ifacePairs, getIfaceList());
- for (const auto& ifacePair:ifacePairs) {
- addInterface(ifacePair.first.c_str(), ifacePair.second);
- }
-
auto result = makeSkDestroyListener();
if (!isOk(result)) {
ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str());
@@ -237,22 +226,6 @@
return netdutils::status::ok;
}
-int TrafficController::addInterface(const char* name, uint32_t ifaceIndex) {
- IfaceValue iface;
- if (ifaceIndex == 0) {
- ALOGE("Unknown interface %s(%d)", name, ifaceIndex);
- return -1;
- }
-
- strlcpy(iface.name, name, sizeof(IfaceValue));
- Status res = mIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY);
- if (!isOk(res)) {
- ALOGE("Failed to add iface %s(%d): %s", name, ifaceIndex, strerror(res.code()));
- return -res.code();
- }
- return 0;
-}
-
Status TrafficController::updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
FirewallType type) {
std::lock_guard guard(mMutex);
@@ -272,7 +245,7 @@
if (oldMatch.ok()) {
UidOwnerValue newMatch = {
.iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif,
- .rule = static_cast<uint8_t>(oldMatch.value().rule & ~match),
+ .rule = oldMatch.value().rule & ~match,
};
if (newMatch.rule == 0) {
RETURN_IF_NOT_OK(mUidOwnerMap.deleteValue(uid));
@@ -286,23 +259,20 @@
}
Status TrafficController::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
- // iif should be non-zero if and only if match == MATCH_IIF
- if (match == IIF_MATCH && iif == 0) {
- return statusFromErrno(EINVAL, "Interface match must have nonzero interface index");
- } else if (match != IIF_MATCH && iif != 0) {
+ if (match != IIF_MATCH && iif != 0) {
return statusFromErrno(EINVAL, "Non-interface match must have zero interface index");
}
auto oldMatch = mUidOwnerMap.readValue(uid);
if (oldMatch.ok()) {
UidOwnerValue newMatch = {
- .iif = iif ? iif : oldMatch.value().iif,
- .rule = static_cast<uint8_t>(oldMatch.value().rule | match),
+ .iif = (match == IIF_MATCH) ? iif : oldMatch.value().iif,
+ .rule = oldMatch.value().rule | match,
};
RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
} else {
UidOwnerValue newMatch = {
.iif = iif,
- .rule = static_cast<uint8_t>(match),
+ .rule = match,
};
RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
}
@@ -335,6 +305,12 @@
return ALLOWLIST;
case LOW_POWER_STANDBY:
return ALLOWLIST;
+ case OEM_DENY_1:
+ return DENYLIST;
+ case OEM_DENY_2:
+ return DENYLIST;
+ case OEM_DENY_3:
+ return DENYLIST;
case NONE:
default:
return DENYLIST;
@@ -360,6 +336,15 @@
case LOW_POWER_STANDBY:
res = updateOwnerMapEntry(LOW_POWER_STANDBY_MATCH, uid, rule, type);
break;
+ case OEM_DENY_1:
+ res = updateOwnerMapEntry(OEM_DENY_1_MATCH, uid, rule, type);
+ break;
+ case OEM_DENY_2:
+ res = updateOwnerMapEntry(OEM_DENY_2_MATCH, uid, rule, type);
+ break;
+ case OEM_DENY_3:
+ res = updateOwnerMapEntry(OEM_DENY_3_MATCH, uid, rule, type);
+ break;
case NONE:
default:
ALOGW("Unknown child chain: %d", chain);
@@ -399,9 +384,6 @@
Status TrafficController::addUidInterfaceRules(const int iif,
const std::vector<int32_t>& uidsToAdd) {
- if (!iif) {
- return statusFromErrno(EINVAL, "Interface rule must specify interface");
- }
std::lock_guard guard(mMutex);
for (auto uid : uidsToAdd) {
@@ -425,6 +407,18 @@
return netdutils::status::ok;
}
+Status TrafficController::updateUidLockdownRule(const uid_t uid, const bool add) {
+ std::lock_guard guard(mMutex);
+
+ netdutils::Status result = add ? addRule(uid, LOCKDOWN_VPN_MATCH)
+ : removeRule(uid, LOCKDOWN_VPN_MATCH);
+ if (!isOk(result)) {
+ ALOGW("%s Lockdown rule failed(%d): uid=%d",
+ (add ? "add": "remove"), result.code(), uid);
+ }
+ return result;
+}
+
int TrafficController::replaceUidOwnerMap(const std::string& name, bool isAllowlist __unused,
const std::vector<int32_t>& uids) {
// FirewallRule rule = isAllowlist ? ALLOW : DENY;
@@ -440,6 +434,12 @@
res = replaceRulesInMap(RESTRICTED_MATCH, uids);
} else if (!name.compare(LOCAL_LOW_POWER_STANDBY)) {
res = replaceRulesInMap(LOW_POWER_STANDBY_MATCH, uids);
+ } else if (!name.compare(LOCAL_OEM_DENY_1)) {
+ res = replaceRulesInMap(OEM_DENY_1_MATCH, uids);
+ } else if (!name.compare(LOCAL_OEM_DENY_2)) {
+ res = replaceRulesInMap(OEM_DENY_2_MATCH, uids);
+ } else if (!name.compare(LOCAL_OEM_DENY_3)) {
+ res = replaceRulesInMap(OEM_DENY_3_MATCH, uids);
} else {
ALOGE("unknown chain name: %s", name.c_str());
return -EINVAL;
@@ -451,61 +451,21 @@
return 0;
}
-int TrafficController::toggleUidOwnerMap(ChildChain chain, bool enable) {
- std::lock_guard guard(mMutex);
- uint32_t key = UID_RULES_CONFIGURATION_KEY;
- auto oldConfiguration = mConfigurationMap.readValue(key);
- if (!oldConfiguration.ok()) {
- ALOGE("Cannot read the old configuration from map: %s",
- oldConfiguration.error().message().c_str());
- return -oldConfiguration.error().code();
- }
- Status res;
- BpfConfig newConfiguration;
- uint8_t match;
- switch (chain) {
- case DOZABLE:
- match = DOZABLE_MATCH;
- break;
- case STANDBY:
- match = STANDBY_MATCH;
- break;
- case POWERSAVE:
- match = POWERSAVE_MATCH;
- break;
- case RESTRICTED:
- match = RESTRICTED_MATCH;
- break;
- case LOW_POWER_STANDBY:
- match = LOW_POWER_STANDBY_MATCH;
- break;
- default:
- return -EINVAL;
- }
- newConfiguration =
- enable ? (oldConfiguration.value() | match) : (oldConfiguration.value() & (~match));
- res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
- if (!isOk(res)) {
- ALOGE("Failed to toggleUidOwnerMap(%d): %s", chain, res.msg().c_str());
- }
- return -res.code();
-}
-
Status TrafficController::swapActiveStatsMap() {
std::lock_guard guard(mMutex);
uint32_t key = CURRENT_STATS_MAP_CONFIGURATION_KEY;
- auto oldConfiguration = mConfigurationMap.readValue(key);
- if (!oldConfiguration.ok()) {
+ auto oldConfigure = mConfigurationMap.readValue(key);
+ if (!oldConfigure.ok()) {
ALOGE("Cannot read the old configuration from map: %s",
- oldConfiguration.error().message().c_str());
- return Status(oldConfiguration.error().code(), oldConfiguration.error().message());
+ oldConfigure.error().message().c_str());
+ return Status(oldConfigure.error().code(), oldConfigure.error().message());
}
// Write to the configuration map to inform the kernel eBPF program to switch
// from using one map to the other. Use flag BPF_EXIST here since the map should
// be already populated in initMaps.
- uint8_t newConfigure = (oldConfiguration.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A;
+ uint32_t newConfigure = (oldConfigure.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A;
auto res = mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, newConfigure,
BPF_EXIST);
if (!res.ok()) {
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
index 9529cae..7730c13 100644
--- a/service/native/TrafficControllerTest.cpp
+++ b/service/native/TrafficControllerTest.cpp
@@ -30,12 +30,15 @@
#include <gtest/gtest.h>
+#include <android-base/file.h>
+#include <android-base/logging.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include <binder/Status.h>
#include <netdutils/MockSyscalls.h>
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
#include "TrafficController.h"
#include "bpf/BpfUtils.h"
#include "NetdUpdatablePublic.h"
@@ -48,6 +51,7 @@
using android::netdutils::Status;
using base::Result;
using netdutils::isOk;
+using netdutils::statusFromErrno;
constexpr int TEST_MAP_SIZE = 10;
constexpr uid_t TEST_UID = 10086;
@@ -55,8 +59,16 @@
constexpr uid_t TEST_UID3 = 98765;
constexpr uint32_t TEST_TAG = 42;
constexpr uint32_t TEST_COUNTERSET = 1;
+constexpr int TEST_COOKIE = 1;
+constexpr char TEST_IFNAME[] = "test0";
+constexpr int TEST_IFINDEX = 999;
+constexpr int RXPACKETS = 1;
+constexpr int RXBYTES = 100;
+constexpr int TXPACKETS = 0;
+constexpr int TXBYTES = 0;
#define ASSERT_VALID(x) ASSERT_TRUE((x).isValid())
+#define ASSERT_INVALID(x) ASSERT_FALSE((x).isValid())
class TrafficControllerTest : public ::testing::Test {
protected:
@@ -65,73 +77,90 @@
BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
- BpfMap<uint32_t, uint8_t> mFakeConfigurationMap;
+ BpfMap<StatsKey, StatsValue> mFakeStatsMapB; // makeTrafficControllerMapsInvalid only
+ BpfMap<uint32_t, StatsValue> mFakeIfaceStatsMap; ; // makeTrafficControllerMapsInvalid only
+ BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap;
BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
+ BpfMap<uint32_t, uint8_t> mFakeUidCounterSetMap;
+ BpfMap<uint32_t, IfaceValue> mFakeIfaceIndexNameMap;
void SetUp() {
std::lock_guard guard(mTc.mMutex);
ASSERT_EQ(0, setrlimitForTest());
- mFakeCookieTagMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint64_t), sizeof(UidTagValue),
- TEST_MAP_SIZE, 0));
+ mFakeCookieTagMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
ASSERT_VALID(mFakeCookieTagMap);
- mFakeAppUidStatsMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(StatsValue),
- TEST_MAP_SIZE, 0));
+ mFakeAppUidStatsMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
ASSERT_VALID(mFakeAppUidStatsMap);
- mFakeStatsMapA.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(StatsKey), sizeof(StatsValue),
- TEST_MAP_SIZE, 0));
+ mFakeStatsMapA.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
ASSERT_VALID(mFakeStatsMapA);
- mFakeConfigurationMap.reset(
- createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), 1, 0));
+ mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_ARRAY, CONFIGURATION_MAP_SIZE);
ASSERT_VALID(mFakeConfigurationMap);
- mFakeUidOwnerMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(UidOwnerValue),
- TEST_MAP_SIZE, 0));
+ mFakeUidOwnerMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
ASSERT_VALID(mFakeUidOwnerMap);
- mFakeUidPermissionMap.reset(
- createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0));
+ mFakeUidPermissionMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
ASSERT_VALID(mFakeUidPermissionMap);
- mTc.mCookieTagMap.reset(dupFd(mFakeCookieTagMap.getMap()));
+ mFakeUidCounterSetMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
+ ASSERT_VALID(mFakeUidCounterSetMap);
+
+ mFakeIfaceIndexNameMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
+ ASSERT_VALID(mFakeIfaceIndexNameMap);
+
+ mTc.mCookieTagMap = mFakeCookieTagMap;
ASSERT_VALID(mTc.mCookieTagMap);
- mTc.mAppUidStatsMap.reset(dupFd(mFakeAppUidStatsMap.getMap()));
+ mTc.mAppUidStatsMap = mFakeAppUidStatsMap;
ASSERT_VALID(mTc.mAppUidStatsMap);
- mTc.mStatsMapA.reset(dupFd(mFakeStatsMapA.getMap()));
+ mTc.mStatsMapA = mFakeStatsMapA;
ASSERT_VALID(mTc.mStatsMapA);
- mTc.mConfigurationMap.reset(dupFd(mFakeConfigurationMap.getMap()));
+ mTc.mConfigurationMap = mFakeConfigurationMap;
ASSERT_VALID(mTc.mConfigurationMap);
// Always write to stats map A by default.
- ASSERT_RESULT_OK(mTc.mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY,
- SELECT_MAP_A, BPF_ANY));
- mTc.mUidOwnerMap.reset(dupFd(mFakeUidOwnerMap.getMap()));
+ static_assert(SELECT_MAP_A == 0);
+
+ mTc.mUidOwnerMap = mFakeUidOwnerMap;
ASSERT_VALID(mTc.mUidOwnerMap);
- mTc.mUidPermissionMap.reset(dupFd(mFakeUidPermissionMap.getMap()));
+ mTc.mUidPermissionMap = mFakeUidPermissionMap;
ASSERT_VALID(mTc.mUidPermissionMap);
mTc.mPrivilegedUser.clear();
- }
- int dupFd(const android::base::unique_fd& mapFd) {
- return fcntl(mapFd.get(), F_DUPFD_CLOEXEC, 0);
+ mTc.mUidCounterSetMap = mFakeUidCounterSetMap;
+ ASSERT_VALID(mTc.mUidCounterSetMap);
+
+ mTc.mIfaceIndexNameMap = mFakeIfaceIndexNameMap;
+ ASSERT_VALID(mTc.mIfaceIndexNameMap);
}
void populateFakeStats(uint64_t cookie, uint32_t uid, uint32_t tag, StatsKey* key) {
UidTagValue cookieMapkey = {.uid = (uint32_t)uid, .tag = tag};
EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, cookieMapkey, BPF_ANY));
- *key = {.uid = uid, .tag = tag, .counterSet = TEST_COUNTERSET, .ifaceIndex = 1};
- StatsValue statsMapValue = {.rxPackets = 1, .rxBytes = 100};
- EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY));
- key->tag = 0;
+ *key = {.uid = uid, .tag = tag, .counterSet = TEST_COUNTERSET, .ifaceIndex = TEST_IFINDEX};
+ StatsValue statsMapValue = {.rxPackets = RXPACKETS, .rxBytes = RXBYTES,
+ .txPackets = TXPACKETS, .txBytes = TXBYTES};
EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY));
EXPECT_RESULT_OK(mFakeAppUidStatsMap.writeValue(uid, statsMapValue, BPF_ANY));
// put tag information back to statsKey
key->tag = tag;
}
+ void populateFakeCounterSet(uint32_t uid, uint32_t counterSet) {
+ EXPECT_RESULT_OK(mFakeUidCounterSetMap.writeValue(uid, counterSet, BPF_ANY));
+ }
+
+ void populateFakeIfaceIndexName(const char* name, uint32_t ifaceIndex) {
+ if (name == nullptr || ifaceIndex <= 0) return;
+
+ IfaceValue iface;
+ strlcpy(iface.name, name, sizeof(IfaceValue));
+ EXPECT_RESULT_OK(mFakeIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY));
+ }
+
void checkUidOwnerRuleForChain(ChildChain chain, UidOwnerMatchType match) {
uint32_t uid = TEST_UID;
EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, DENY, DENYLIST));
@@ -189,7 +218,7 @@
checkEachUidValue(uids, match);
}
- void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint8_t expectedRule,
+ void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint32_t expectedRule,
uint32_t expectedIif) {
for (uint32_t uid : appUids) {
Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
@@ -233,37 +262,6 @@
EXPECT_TRUE(mTc.mPrivilegedUser.empty());
}
- void addPrivilegedUid(uid_t uid) {
- std::vector privilegedUid = {uid};
- mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, privilegedUid);
- }
-
- void removePrivilegedUid(uid_t uid) {
- std::vector privilegedUid = {uid};
- mTc.setPermissionForUids(INetd::PERMISSION_NONE, privilegedUid);
- }
-
- void expectFakeStatsUnchanged(uint64_t cookie, uint32_t tag, uint32_t uid,
- StatsKey tagStatsMapKey) {
- Result<UidTagValue> cookieMapResult = mFakeCookieTagMap.readValue(cookie);
- EXPECT_RESULT_OK(cookieMapResult);
- EXPECT_EQ(uid, cookieMapResult.value().uid);
- EXPECT_EQ(tag, cookieMapResult.value().tag);
- Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
- EXPECT_RESULT_OK(statsMapResult);
- EXPECT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
- EXPECT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
- tagStatsMapKey.tag = 0;
- statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
- EXPECT_RESULT_OK(statsMapResult);
- EXPECT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
- EXPECT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
- auto appStatsResult = mFakeAppUidStatsMap.readValue(uid);
- EXPECT_RESULT_OK(appStatsResult);
- EXPECT_EQ((uint64_t)1, appStatsResult.value().rxPackets);
- EXPECT_EQ((uint64_t)100, appStatsResult.value().rxBytes);
- }
-
Status updateUidOwnerMaps(const std::vector<uint32_t>& appUids,
UidOwnerMatchType matchType, TrafficController::IptOp op) {
Status ret(0);
@@ -274,6 +272,108 @@
return ret;
}
+ Status dump(bool verbose, std::vector<std::string>& outputLines) {
+ if (!outputLines.empty()) return statusFromErrno(EUCLEAN, "Output buffer is not empty");
+
+ android::base::unique_fd localFd, remoteFd;
+ if (!Pipe(&localFd, &remoteFd)) return statusFromErrno(errno, "Failed on pipe");
+
+ // dump() blocks until another thread has consumed all its output.
+ std::thread dumpThread =
+ std::thread([this, remoteFd{std::move(remoteFd)}, verbose]() {
+ mTc.dump(remoteFd, verbose);
+ });
+
+ std::string dumpContent;
+ if (!android::base::ReadFdToString(localFd.get(), &dumpContent)) {
+ return statusFromErrno(errno, "Failed to read dump results from fd");
+ }
+ dumpThread.join();
+
+ std::stringstream dumpStream(std::move(dumpContent));
+ std::string line;
+ while (std::getline(dumpStream, line)) {
+ outputLines.push_back(line);
+ }
+
+ return netdutils::status::ok;
+ }
+
+ // Strings in the |expect| must exist in dump results in order. But no need to be consecutive.
+ bool expectDumpsysContains(std::vector<std::string>& expect) {
+ if (expect.empty()) return false;
+
+ std::vector<std::string> output;
+ Status result = dump(true, output);
+ if (!isOk(result)) {
+ GTEST_LOG_(ERROR) << "TrafficController dump failed: " << netdutils::toString(result);
+ return false;
+ }
+
+ int matched = 0;
+ auto it = expect.begin();
+ for (const auto& line : output) {
+ if (it == expect.end()) break;
+ if (std::string::npos != line.find(*it)) {
+ matched++;
+ ++it;
+ }
+ }
+
+ if (matched != expect.size()) {
+ // dump results for debugging
+ for (const auto& o : output) LOG(INFO) << "output: " << o;
+ for (const auto& e : expect) LOG(INFO) << "expect: " << e;
+ return false;
+ }
+ return true;
+ }
+
+ // Once called, the maps of TrafficController can't recover to valid maps which initialized
+ // in SetUp().
+ void makeTrafficControllerMapsInvalid() {
+ constexpr char INVALID_PATH[] = "invalid";
+
+ mFakeCookieTagMap.init(INVALID_PATH);
+ mTc.mCookieTagMap = mFakeCookieTagMap;
+ ASSERT_INVALID(mTc.mCookieTagMap);
+
+ mFakeAppUidStatsMap.init(INVALID_PATH);
+ mTc.mAppUidStatsMap = mFakeAppUidStatsMap;
+ ASSERT_INVALID(mTc.mAppUidStatsMap);
+
+ mFakeStatsMapA.init(INVALID_PATH);
+ mTc.mStatsMapA = mFakeStatsMapA;
+ ASSERT_INVALID(mTc.mStatsMapA);
+
+ mFakeStatsMapB.init(INVALID_PATH);
+ mTc.mStatsMapB = mFakeStatsMapB;
+ ASSERT_INVALID(mTc.mStatsMapB);
+
+ mFakeIfaceStatsMap.init(INVALID_PATH);
+ mTc.mIfaceStatsMap = mFakeIfaceStatsMap;
+ ASSERT_INVALID(mTc.mIfaceStatsMap);
+
+ mFakeConfigurationMap.init(INVALID_PATH);
+ mTc.mConfigurationMap = mFakeConfigurationMap;
+ ASSERT_INVALID(mTc.mConfigurationMap);
+
+ mFakeUidOwnerMap.init(INVALID_PATH);
+ mTc.mUidOwnerMap = mFakeUidOwnerMap;
+ ASSERT_INVALID(mTc.mUidOwnerMap);
+
+ mFakeUidPermissionMap.init(INVALID_PATH);
+ mTc.mUidPermissionMap = mFakeUidPermissionMap;
+ ASSERT_INVALID(mTc.mUidPermissionMap);
+
+ mFakeUidCounterSetMap.init(INVALID_PATH);
+ mTc.mUidCounterSetMap = mFakeUidCounterSetMap;
+ ASSERT_INVALID(mTc.mUidCounterSetMap);
+
+ mFakeIfaceIndexNameMap.init(INVALID_PATH);
+ mTc.mIfaceIndexNameMap = mFakeIfaceIndexNameMap;
+ ASSERT_INVALID(mTc.mIfaceIndexNameMap);
+ }
};
TEST_F(TrafficControllerTest, TestUpdateOwnerMapEntry) {
@@ -307,6 +407,9 @@
checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
checkUidOwnerRuleForChain(LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
+ checkUidOwnerRuleForChain(OEM_DENY_1, OEM_DENY_1_MATCH);
+ checkUidOwnerRuleForChain(OEM_DENY_2, OEM_DENY_2_MATCH);
+ checkUidOwnerRuleForChain(OEM_DENY_3, OEM_DENY_3_MATCH);
ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(NONE, TEST_UID, ALLOW, ALLOWLIST));
ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(INVALID_CHAIN, TEST_UID, ALLOW, ALLOWLIST));
}
@@ -318,6 +421,9 @@
checkUidMapReplace("fw_powersave", uids, POWERSAVE_MATCH);
checkUidMapReplace("fw_restricted", uids, RESTRICTED_MATCH);
checkUidMapReplace("fw_low_power_standby", uids, LOW_POWER_STANDBY_MATCH);
+ checkUidMapReplace("fw_oem_deny_1", uids, OEM_DENY_1_MATCH);
+ checkUidMapReplace("fw_oem_deny_2", uids, OEM_DENY_2_MATCH);
+ checkUidMapReplace("fw_oem_deny_3", uids, OEM_DENY_3_MATCH);
ASSERT_EQ(-EINVAL, mTc.replaceUidOwnerMap("unknow", true, uids));
}
@@ -432,6 +538,21 @@
expectMapEmpty(mFakeUidOwnerMap);
}
+TEST_F(TrafficControllerTest, TestUpdateUidLockdownRule) {
+ // Add Lockdown rules
+ ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1000, true /* add */)));
+ ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1001, true /* add */)));
+ expectUidOwnerMapValues({1000, 1001}, LOCKDOWN_VPN_MATCH, 0);
+
+ // Remove one of Lockdown rules
+ ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1000, false /* add */)));
+ expectUidOwnerMapValues({1001}, LOCKDOWN_VPN_MATCH, 0);
+
+ // Remove remaining Lockdown rule
+ ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1001, false /* add */)));
+ expectMapEmpty(mFakeUidOwnerMap);
+}
+
TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithExistingMatches) {
// Set up existing PENALTY_BOX_MATCH rules
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000, 1001, 10012}, PENALTY_BOX_MATCH,
@@ -491,6 +612,70 @@
checkEachUidValue({10001, 10002}, IIF_MATCH);
}
+TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRulesWithWildcard) {
+ // iif=0 is a wildcard
+ int iif = 0;
+ // Add interface rule with wildcard to uids
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
+ expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
+}
+
+TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRulesWithWildcard) {
+ // iif=0 is a wildcard
+ int iif = 0;
+ // Add interface rule with wildcard to two uids
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
+ expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
+
+ // Remove interface rule from one of the uids
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+ expectUidOwnerMapValues({1001}, IIF_MATCH, iif);
+ checkEachUidValue({1001}, IIF_MATCH);
+
+ // Remove interface rule from the remaining uid
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001})));
+ expectMapEmpty(mFakeUidOwnerMap);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndExistingMatches) {
+ // Set up existing DOZABLE_MATCH and POWERSAVE_MATCH rule
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+ TrafficController::IptOpInsert)));
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+ TrafficController::IptOpInsert)));
+
+ // iif=0 is a wildcard
+ int iif = 0;
+ // Add interface rule with wildcard to the existing uid
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
+ expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
+
+ // Remove interface rule with wildcard from the existing uid
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+ expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndNewMatches) {
+ // iif=0 is a wildcard
+ int iif = 0;
+ // Set up existing interface rule with wildcard
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
+
+ // Add DOZABLE_MATCH and POWERSAVE_MATCH rule to the existing uid
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+ TrafficController::IptOpInsert)));
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+ TrafficController::IptOpInsert)));
+ expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
+
+ // Remove DOZABLE_MATCH and POWERSAVE_MATCH rule from the existing uid
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+ TrafficController::IptOpDelete)));
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+ TrafficController::IptOpDelete)));
+ expectUidOwnerMapValues({1000}, IIF_MATCH, iif);
+}
+
TEST_F(TrafficControllerTest, TestGrantInternetPermission) {
std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
@@ -585,6 +770,148 @@
expectPrivilegedUserSetEmpty();
}
+TEST_F(TrafficControllerTest, TestDumpsys) {
+ StatsKey tagStatsMapKey;
+ populateFakeStats(TEST_COOKIE, TEST_UID, TEST_TAG, &tagStatsMapKey);
+ populateFakeCounterSet(TEST_UID3, TEST_COUNTERSET);
+
+ // Expect: (part of this depends on hard-code values in populateFakeStats())
+ //
+ // mCookieTagMap:
+ // cookie=1 tag=0x2a uid=10086
+ //
+ // mUidCounterSetMap:
+ // 98765 1
+ //
+ // mAppUidStatsMap::
+ // uid rxBytes rxPackets txBytes txPackets
+ // 10086 100 1 0 0
+ //
+ // mStatsMapA:
+ // ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes rxPackets txBytes txPackets
+ // 999 test0 0x2a 10086 1 100 1 0 0
+ std::vector<std::string> expectedLines = {
+ "mCookieTagMap:",
+ fmt::format("cookie={} tag={:#x} uid={}", TEST_COOKIE, TEST_TAG, TEST_UID),
+ "mUidCounterSetMap:",
+ fmt::format("{} {}", TEST_UID3, TEST_COUNTERSET),
+ "mAppUidStatsMap::", // TODO@: fix double colon
+ "uid rxBytes rxPackets txBytes txPackets",
+ fmt::format("{} {} {} {} {}", TEST_UID, RXBYTES, RXPACKETS, TXBYTES, TXPACKETS),
+ "mStatsMapA",
+ "ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes rxPackets txBytes txPackets",
+ fmt::format("{} {} {:#x} {} {} {} {} {} {}",
+ TEST_IFINDEX, TEST_IFNAME, TEST_TAG, TEST_UID, TEST_COUNTERSET, RXBYTES,
+ RXPACKETS, TXBYTES, TXPACKETS)};
+
+ populateFakeIfaceIndexName(TEST_IFNAME, TEST_IFINDEX);
+ expectedLines.emplace_back("mIfaceIndexNameMap:");
+ expectedLines.emplace_back(fmt::format("ifaceIndex={} ifaceName={}",
+ TEST_IFINDEX, TEST_IFNAME));
+
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({TEST_UID}, HAPPY_BOX_MATCH,
+ TrafficController::IptOpInsert)));
+ expectedLines.emplace_back("mUidOwnerMap:");
+ expectedLines.emplace_back(fmt::format("{} HAPPY_BOX_MATCH", TEST_UID));
+
+ mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, {TEST_UID2});
+ expectedLines.emplace_back("mUidPermissionMap:");
+ expectedLines.emplace_back(fmt::format("{} BPF_PERMISSION_UPDATE_DEVICE_STATS", TEST_UID2));
+ expectedLines.emplace_back("mPrivilegedUser:");
+ expectedLines.emplace_back(fmt::format("{} ALLOW_UPDATE_DEVICE_STATS", TEST_UID2));
+ EXPECT_TRUE(expectDumpsysContains(expectedLines));
+}
+
+TEST_F(TrafficControllerTest, dumpsysInvalidMaps) {
+ makeTrafficControllerMapsInvalid();
+
+ const std::string kErrIterate = "print end with error: Get firstKey map -1 failed: "
+ "Bad file descriptor";
+ const std::string kErrReadRulesConfig = "read ownerMatch configure failed with error: "
+ "Read value of map -1 failed: Bad file descriptor";
+ const std::string kErrReadStatsMapConfig = "read stats map configure failed with error: "
+ "Read value of map -1 failed: Bad file descriptor";
+
+ std::vector<std::string> expectedLines = {
+ fmt::format("mCookieTagMap {}", kErrIterate),
+ fmt::format("mUidCounterSetMap {}", kErrIterate),
+ fmt::format("mAppUidStatsMap {}", kErrIterate),
+ fmt::format("mStatsMapA {}", kErrIterate),
+ fmt::format("mStatsMapB {}", kErrIterate),
+ fmt::format("mIfaceIndexNameMap {}", kErrIterate),
+ fmt::format("mIfaceStatsMap {}", kErrIterate),
+ fmt::format("mConfigurationMap {}", kErrReadRulesConfig),
+ fmt::format("mConfigurationMap {}", kErrReadStatsMapConfig),
+ fmt::format("mUidOwnerMap {}", kErrIterate),
+ fmt::format("mUidPermissionMap {}", kErrIterate)};
+ EXPECT_TRUE(expectDumpsysContains(expectedLines));
+}
+
+TEST_F(TrafficControllerTest, uidMatchTypeToString) {
+ // NO_MATCH(0) can't be verified because match type flag is added by OR operator.
+ // See TrafficController::addRule()
+ static const struct TestConfig {
+ UidOwnerMatchType uidOwnerMatchType;
+ std::string expected;
+ } testConfigs[] = {
+ // clang-format off
+ {HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"},
+ {DOZABLE_MATCH, "DOZABLE_MATCH"},
+ {STANDBY_MATCH, "STANDBY_MATCH"},
+ {POWERSAVE_MATCH, "POWERSAVE_MATCH"},
+ {HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"},
+ {RESTRICTED_MATCH, "RESTRICTED_MATCH"},
+ {LOW_POWER_STANDBY_MATCH, "LOW_POWER_STANDBY_MATCH"},
+ {IIF_MATCH, "IIF_MATCH"},
+ {LOCKDOWN_VPN_MATCH, "LOCKDOWN_VPN_MATCH"},
+ {OEM_DENY_1_MATCH, "OEM_DENY_1_MATCH"},
+ {OEM_DENY_2_MATCH, "OEM_DENY_2_MATCH"},
+ {OEM_DENY_3_MATCH, "OEM_DENY_3_MATCH"},
+ // clang-format on
+ };
+
+ for (const auto& config : testConfigs) {
+ SCOPED_TRACE(fmt::format("testConfig: [{}, {}]", config.uidOwnerMatchType,
+ config.expected));
+
+ // Test private function uidMatchTypeToString() via dumpsys.
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({TEST_UID}, config.uidOwnerMatchType,
+ TrafficController::IptOpInsert)));
+ std::vector<std::string> expectedLines;
+ expectedLines.emplace_back(fmt::format("{} {}", TEST_UID, config.expected));
+ EXPECT_TRUE(expectDumpsysContains(expectedLines));
+
+ // Clean up the stubs.
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({TEST_UID}, config.uidOwnerMatchType,
+ TrafficController::IptOpDelete)));
+ }
+}
+
+TEST_F(TrafficControllerTest, getFirewallType) {
+ static const struct TestConfig {
+ ChildChain childChain;
+ FirewallType firewallType;
+ } testConfigs[] = {
+ // clang-format off
+ {NONE, DENYLIST},
+ {DOZABLE, ALLOWLIST},
+ {STANDBY, DENYLIST},
+ {POWERSAVE, ALLOWLIST},
+ {RESTRICTED, ALLOWLIST},
+ {LOW_POWER_STANDBY, ALLOWLIST},
+ {OEM_DENY_1, DENYLIST},
+ {OEM_DENY_2, DENYLIST},
+ {OEM_DENY_3, DENYLIST},
+ {INVALID_CHAIN, DENYLIST},
+ // clang-format on
+ };
+
+ for (const auto& config : testConfigs) {
+ SCOPED_TRACE(fmt::format("testConfig: [{}, {}]", config.childChain, config.firewallType));
+ EXPECT_EQ(config.firewallType, mTc.getFirewallType(config.childChain));
+ }
+}
+
constexpr uint32_t SOCK_CLOSE_WAIT_US = 30 * 1000;
constexpr uint32_t ENOBUFS_POLL_WAIT_US = 10 * 1000;
@@ -608,7 +935,7 @@
BpfMap<uint64_t, UidTagValue> mCookieTagMap;
void SetUp() {
- mCookieTagMap.reset(android::bpf::mapRetrieveRW(COOKIE_TAG_MAP_PATH));
+ mCookieTagMap.init(COOKIE_TAG_MAP_PATH);
ASSERT_TRUE(mCookieTagMap.isValid());
}
@@ -620,7 +947,7 @@
if (res.ok() || (res.error().code() == ENOENT)) {
return Result<void>();
}
- ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s\n", key,
+ ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s", key,
strerror(res.error().code()));
}
// Move forward to next cookie in the map.
diff --git a/service/native/include/Common.h b/service/native/include/Common.h
index dc44845..03f449a 100644
--- a/service/native/include/Common.h
+++ b/service/native/include/Common.h
@@ -17,9 +17,12 @@
#pragma once
// TODO: deduplicate with the constants in NetdConstants.h.
#include <aidl/android/net/INetd.h>
+#include "clat_mark.h"
using aidl::android::net::INetd;
+static_assert(INetd::CLAT_MARK == CLAT_MARK, "must be 0xDEADC1A7");
+
enum FirewallRule { ALLOW = INetd::FIREWALL_RULE_ALLOW, DENY = INetd::FIREWALL_RULE_DENY };
// ALLOWLIST means the firewall denies all by default, uids must be explicitly ALLOWed
@@ -35,6 +38,9 @@
POWERSAVE = 3,
RESTRICTED = 4,
LOW_POWER_STANDBY = 5,
+ OEM_DENY_1 = 7,
+ OEM_DENY_2 = 8,
+ OEM_DENY_3 = 9,
INVALID_CHAIN
};
// LINT.ThenChange(packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java)
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
index 79e75ac..14c5eaf 100644
--- a/service/native/include/TrafficController.h
+++ b/service/native/include/TrafficController.h
@@ -45,11 +45,6 @@
*/
netdutils::Status swapActiveStatsMap() EXCLUDES(mMutex);
- /*
- * Add the interface name and index pair into the eBPF map.
- */
- int addInterface(const char* name, uint32_t ifaceIndex);
-
int changeUidOwnerRule(ChildChain chain, const uid_t uid, FirewallRule rule, FirewallType type);
int removeUidOwnerRule(const uid_t uid);
@@ -71,11 +66,11 @@
EXCLUDES(mMutex);
netdutils::Status removeUidInterfaceRules(const std::vector<int32_t>& uids) EXCLUDES(mMutex);
+ netdutils::Status updateUidLockdownRule(const uid_t uid, const bool add) EXCLUDES(mMutex);
+
netdutils::Status updateUidOwnerMap(const uint32_t uid,
UidOwnerMatchType matchType, IptOp op) EXCLUDES(mMutex);
- int toggleUidOwnerMap(ChildChain chain, bool enable) EXCLUDES(mMutex);
-
static netdutils::StatusOr<std::unique_ptr<netdutils::NetlinkListenerInterface>>
makeSkDestroyListener();
@@ -88,6 +83,9 @@
static const char* LOCAL_POWERSAVE;
static const char* LOCAL_RESTRICTED;
static const char* LOCAL_LOW_POWER_STANDBY;
+ static const char* LOCAL_OEM_DENY_1;
+ static const char* LOCAL_OEM_DENY_2;
+ static const char* LOCAL_OEM_DENY_3;
private:
/*
@@ -149,13 +147,13 @@
* the map right now:
* - Entry with UID_RULES_CONFIGURATION_KEY:
* Store the configuration for the current uid rules. It indicates the device
- * is in doze/powersave/standby/restricted/low power standby mode.
+ * is in doze/powersave/standby/restricted/low power standby/oem deny mode.
* - Entry with CURRENT_STATS_MAP_CONFIGURATION_KEY:
* Stores the current live stats map that kernel program is writing to.
* Userspace can do scraping and cleaning job on the other one depending on the
* current configs.
*/
- bpf::BpfMap<uint32_t, uint8_t> mConfigurationMap GUARDED_BY(mMutex);
+ bpf::BpfMap<uint32_t, uint32_t> mConfigurationMap GUARDED_BY(mMutex);
/*
* mUidOwnerMap: Store uids that are used for bandwidth control uid match.
@@ -182,8 +180,6 @@
// need to call back to system server for permission check.
std::set<uid_t> mPrivilegedUser GUARDED_BY(mMutex);
- bool hasUpdateDeviceStatsPermission(uid_t uid) REQUIRES(mMutex);
-
// For testing
friend class TrafficControllerTest;
};
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 68e4dc4..54d40ac 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -35,7 +35,8 @@
cc_test {
name: "libclat_test",
defaults: ["netd_defaults"],
- test_suites: ["device-tests"],
+ test_suites: ["general-tests", "mts-tethering"],
+ test_config_template: ":net_native_test_config_template",
srcs: [
"clatutils_test.cpp",
],
@@ -49,5 +50,14 @@
"liblog",
"libnetutils",
],
+ compile_multilib: "both",
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
require_root: true,
}
diff --git a/service/native/libs/libclat/clatutils_test.cpp b/service/native/libs/libclat/clatutils_test.cpp
index 4153e19..8cca1f4 100644
--- a/service/native/libs/libclat/clatutils_test.cpp
+++ b/service/native/libs/libclat/clatutils_test.cpp
@@ -19,6 +19,7 @@
#include <gtest/gtest.h>
#include <linux/if_packet.h>
#include <linux/if_tun.h>
+#include <netinet/in.h>
#include "tun_interface.h"
extern "C" {
@@ -182,6 +183,31 @@
v6Iface.destroy();
}
+// This is not a realistic test because we can't test generateIPv6Address here since it requires
+// manipulating routing, which we can't do without talking to the real netd on the system.
+// See test MakeChecksumNeutral.
+// TODO: remove this test once EthernetTetheringTest can test on mainline test coverage branch.
+TEST_F(ClatUtils, GenerateIpv6AddressFailWithUlaSocketAddress) {
+ // Create an interface for generateIpv6Address to connect to.
+ TunInterface tun;
+ ASSERT_EQ(0, tun.init());
+
+ // Unused because v6 address is ULA and makeChecksumNeutral has never called.
+ in_addr v4 = {inet_addr(kIPv4LocalAddr)};
+ // Not a NAT64 prefix because test can't connect to in generateIpv6Address.
+ // Used to be a fake address from the tun interface for generating an IPv6 socket address.
+ // nat64Prefix won't be used in MakeChecksumNeutral because MakeChecksumNeutral has never
+ // be called.
+ in6_addr nat64Prefix = tun.dstAddr(); // not realistic
+ in6_addr v6;
+ EXPECT_EQ(1, inet_pton(AF_INET6, "::", &v6)); // initialize as zero
+
+ EXPECT_EQ(-ENETUNREACH, generateIpv6Address(tun.name().c_str(), v4, nat64Prefix, &v6));
+ EXPECT_TRUE(IN6_IS_ADDR_ULA(&v6));
+
+ tun.destroy();
+}
+
} // namespace clat
} // namespace net
} // namespace android
diff --git a/service/proguard.flags b/service/proguard.flags
new file mode 100644
index 0000000..cffa490
--- /dev/null
+++ b/service/proguard.flags
@@ -0,0 +1,17 @@
+# Make sure proguard keeps all connectivity classes
+# TODO: instead of keeping everything, consider listing only "entry points"
+# (service loader, JNI registered methods, etc) and letting the optimizer do its job
+-keep class android.net.** { *; }
+-keep class com.android.connectivity.** { *; }
+-keep class com.android.net.** { *; }
+-keep class !com.android.server.nearby.**,com.android.server.** { *; }
+
+# Prevent proguard from stripping out any nearby-service and fast-pair-lite-protos fields.
+-keep class com.android.server.nearby.NearbyService { *; }
+
+# The lite proto runtime uses reflection to access fields based on the names in
+# the schema, keep all the fields.
+# This replicates the base proguard rule used by the build by default
+# (proguard_basic_keeps.flags), but needs to be specified here because the
+# com.google.protobuf package is jarjared to the below package.
+-keepclassmembers class * extends android.net.connectivity.com.google.protobuf.MessageLite { <fields>; }
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index c006bc6..6599c7f 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -16,15 +16,31 @@
package com.android.server;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+import static android.system.OsConstants.EINVAL;
+import static android.system.OsConstants.ENOENT;
import static android.system.OsConstants.EOPNOTSUPP;
import android.net.INetd;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
+import android.util.SparseLongArray;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.Struct.U32;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -35,22 +51,115 @@
* {@hide}
*/
public class BpfNetMaps {
+ private static final boolean PRE_T = !SdkLevel.isAtLeastT();
+ static {
+ if (!PRE_T) {
+ System.loadLibrary("service-connectivity");
+ }
+ }
+
private static final String TAG = "BpfNetMaps";
private final INetd mNetd;
// Use legacy netd for releases before T.
- private static final boolean USE_NETD = !SdkLevel.isAtLeastT();
private static boolean sInitialized = false;
+ // Lock for sConfigurationMap entry for UID_RULES_CONFIGURATION_KEY.
+ // This entry is not accessed by others.
+ // BpfNetMaps acquires this lock while sequence of read, modify, and write.
+ private static final Object sUidRulesConfigBpfMapLock = new Object();
+
+ private static final String CONFIGURATION_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_configuration_map";
+ private static final String UID_OWNER_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_uid_owner_map";
+ private static final U32 UID_RULES_CONFIGURATION_KEY = new U32(0);
+ private static BpfMap<U32, U32> sConfigurationMap = null;
+ // BpfMap for UID_OWNER_MAP_PATH. This map is not accessed by others.
+ private static BpfMap<U32, UidOwnerValue> sUidOwnerMap = null;
+
+ // LINT.IfChange(match_type)
+ @VisibleForTesting public static final long NO_MATCH = 0;
+ @VisibleForTesting public static final long HAPPY_BOX_MATCH = (1 << 0);
+ @VisibleForTesting public static final long PENALTY_BOX_MATCH = (1 << 1);
+ @VisibleForTesting public static final long DOZABLE_MATCH = (1 << 2);
+ @VisibleForTesting public static final long STANDBY_MATCH = (1 << 3);
+ @VisibleForTesting public static final long POWERSAVE_MATCH = (1 << 4);
+ @VisibleForTesting public static final long RESTRICTED_MATCH = (1 << 5);
+ @VisibleForTesting public static final long LOW_POWER_STANDBY_MATCH = (1 << 6);
+ @VisibleForTesting public static final long IIF_MATCH = (1 << 7);
+ @VisibleForTesting public static final long LOCKDOWN_VPN_MATCH = (1 << 8);
+ @VisibleForTesting public static final long OEM_DENY_1_MATCH = (1 << 9);
+ @VisibleForTesting public static final long OEM_DENY_2_MATCH = (1 << 10);
+ @VisibleForTesting public static final long OEM_DENY_3_MATCH = (1 << 11);
+ // LINT.ThenChange(packages/modules/Connectivity/bpf_progs/bpf_shared.h)
+
+ // TODO: Use Java BpfMap instead of JNI code (TrafficController) for map update.
+ // Currently, BpfNetMaps uses TrafficController for map update and TrafficController
+ // (changeUidOwnerRule and toggleUidOwnerMap) also does conversion from "firewall chain" to
+ // "match". Migrating map update from JNI to Java BpfMap will solve this duplication.
+ private static final SparseLongArray FIREWALL_CHAIN_TO_MATCH = new SparseLongArray();
+ static {
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_DOZABLE, DOZABLE_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_STANDBY, STANDBY_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_POWERSAVE, POWERSAVE_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_RESTRICTED, RESTRICTED_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_1, OEM_DENY_1_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_2, OEM_DENY_2_MATCH);
+ FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_3, OEM_DENY_3_MATCH);
+ }
+
+ /**
+ * Set configurationMap for test.
+ */
+ @VisibleForTesting
+ public static void setConfigurationMapForTest(BpfMap<U32, U32> configurationMap) {
+ sConfigurationMap = configurationMap;
+ }
+
+ /**
+ * Set uidOwnerMap for test.
+ */
+ @VisibleForTesting
+ public static void setUidOwnerMapForTest(BpfMap<U32, UidOwnerValue> uidOwnerMap) {
+ sUidOwnerMap = uidOwnerMap;
+ }
+
+ private static BpfMap<U32, U32> getConfigurationMap() {
+ try {
+ return new BpfMap<>(
+ CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDWR, U32.class, U32.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open netd configuration map", e);
+ }
+ }
+
+ private static BpfMap<U32, UidOwnerValue> getUidOwnerMap() {
+ try {
+ return new BpfMap<>(
+ UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDWR, U32.class, UidOwnerValue.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open uid owner map", e);
+ }
+ }
+
+ private static void setBpfMaps() {
+ if (sConfigurationMap == null) {
+ sConfigurationMap = getConfigurationMap();
+ }
+ if (sUidOwnerMap == null) {
+ sUidOwnerMap = getUidOwnerMap();
+ }
+ }
+
/**
* Initializes the class if it is not already initialized. This method will open maps but not
* cause any other effects. This method may be called multiple times on any thread.
*/
private static synchronized void ensureInitialized() {
if (sInitialized) return;
- if (!USE_NETD) {
- System.loadLibrary("service-connectivity");
- native_init();
- }
+ setBpfMaps();
+ native_init();
sInitialized = true;
}
@@ -58,20 +167,101 @@
public BpfNetMaps() {
this(null);
- if (USE_NETD) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
+ if (PRE_T) throw new IllegalArgumentException("BpfNetMaps need to use netd before T");
}
- public BpfNetMaps(INetd netd) {
- ensureInitialized();
+ public BpfNetMaps(final INetd netd) {
+ if (!PRE_T) {
+ ensureInitialized();
+ }
mNetd = netd;
}
+ /**
+ * Get corresponding match from firewall chain.
+ */
+ @VisibleForTesting
+ public long getMatchByFirewallChain(final int chain) {
+ final long match = FIREWALL_CHAIN_TO_MATCH.get(chain, NO_MATCH);
+ if (match == NO_MATCH) {
+ throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
+ }
+ return match;
+ }
+
private void maybeThrow(final int err, final String msg) {
if (err != 0) {
throw new ServiceSpecificException(err, msg + ": " + Os.strerror(err));
}
}
+ private void throwIfPreT(final String msg) {
+ if (PRE_T) {
+ throw new UnsupportedOperationException(msg);
+ }
+ }
+
+ private void removeRule(final int uid, final long match, final String caller) {
+ try {
+ synchronized (sUidOwnerMap) {
+ final UidOwnerValue oldMatch = sUidOwnerMap.getValue(new U32(uid));
+
+ if (oldMatch == null) {
+ throw new ServiceSpecificException(ENOENT,
+ "sUidOwnerMap does not have entry for uid: " + uid);
+ }
+
+ final UidOwnerValue newMatch = new UidOwnerValue(
+ (match == IIF_MATCH) ? 0 : oldMatch.iif,
+ oldMatch.rule & ~match
+ );
+
+ if (newMatch.rule == 0) {
+ sUidOwnerMap.deleteEntry(new U32(uid));
+ } else {
+ sUidOwnerMap.updateEntry(new U32(uid), newMatch);
+ }
+ }
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ caller + " failed to remove rule: " + Os.strerror(e.errno));
+ }
+ }
+
+ private void addRule(final int uid, final long match, final long iif, final String caller) {
+ if (match != IIF_MATCH && iif != 0) {
+ throw new ServiceSpecificException(EINVAL,
+ "Non-interface match must have zero interface index");
+ }
+
+ try {
+ synchronized (sUidOwnerMap) {
+ final UidOwnerValue oldMatch = sUidOwnerMap.getValue(new U32(uid));
+
+ final UidOwnerValue newMatch;
+ if (oldMatch != null) {
+ newMatch = new UidOwnerValue(
+ (match == IIF_MATCH) ? iif : oldMatch.iif,
+ oldMatch.rule | match
+ );
+ } else {
+ newMatch = new UidOwnerValue(
+ iif,
+ match
+ );
+ }
+ sUidOwnerMap.updateEntry(new U32(uid), newMatch);
+ }
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ caller + " failed to add rule: " + Os.strerror(e.errno));
+ }
+ }
+
+ private void addRule(final int uid, final long match, final String caller) {
+ addRule(uid, match, 0 /* iif */, caller);
+ }
+
/**
* Add naughty app bandwidth rule for specific app
*
@@ -80,8 +270,8 @@
* cause of the failure.
*/
public void addNaughtyApp(final int uid) {
- final int err = native_addNaughtyApp(uid);
- maybeThrow(err, "Unable to add naughty app");
+ throwIfPreT("addNaughtyApp is not available on pre-T devices");
+ addRule(uid, PENALTY_BOX_MATCH, "addNaughtyApp");
}
/**
@@ -92,8 +282,8 @@
* cause of the failure.
*/
public void removeNaughtyApp(final int uid) {
- final int err = native_removeNaughtyApp(uid);
- maybeThrow(err, "Unable to remove naughty app");
+ throwIfPreT("removeNaughtyApp is not available on pre-T devices");
+ removeRule(uid, PENALTY_BOX_MATCH, "removeNaughtyApp");
}
/**
@@ -104,8 +294,8 @@
* cause of the failure.
*/
public void addNiceApp(final int uid) {
- final int err = native_addNiceApp(uid);
- maybeThrow(err, "Unable to add nice app");
+ throwIfPreT("addNiceApp is not available on pre-T devices");
+ addRule(uid, HAPPY_BOX_MATCH, "addNiceApp");
}
/**
@@ -116,8 +306,8 @@
* cause of the failure.
*/
public void removeNiceApp(final int uid) {
- final int err = native_removeNiceApp(uid);
- maybeThrow(err, "Unable to remove nice app");
+ throwIfPreT("removeNiceApp is not available on pre-T devices");
+ removeRule(uid, HAPPY_BOX_MATCH, "removeNiceApp");
}
/**
@@ -125,12 +315,46 @@
*
* @param childChain target chain to enable
* @param enable whether to enable or disable child chain.
+ * @throws UnsupportedOperationException if called on pre-T devices.
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
public void setChildChain(final int childChain, final boolean enable) {
- final int err = native_setChildChain(childChain, enable);
- maybeThrow(err, "Unable to set child chain");
+ throwIfPreT("setChildChain is not available on pre-T devices");
+
+ final long match = getMatchByFirewallChain(childChain);
+ try {
+ synchronized (sUidRulesConfigBpfMapLock) {
+ final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+ final long newConfig = enable ? (config.val | match) : (config.val & ~match);
+ sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(newConfig));
+ }
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to set child chain: " + Os.strerror(e.errno));
+ }
+ }
+
+ /**
+ * Get the specified firewall chain's status.
+ *
+ * @param childChain target chain
+ * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public boolean isChainEnabled(final int childChain) {
+ throwIfPreT("isChainEnabled is not available on pre-T devices");
+
+ final long match = getMatchByFirewallChain(childChain);
+ try {
+ final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+ return (config.val & match) != 0;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to get firewall chain status: " + Os.strerror(e.errno));
+ }
}
/**
@@ -148,11 +372,13 @@
*/
public int replaceUidChain(final String chainName, final boolean isAllowlist,
final int[] uids) {
- final int err = native_replaceUidChain(chainName, isAllowlist, uids);
- if (err != 0) {
- Log.e(TAG, "replaceUidChain failed: " + Os.strerror(-err));
+ synchronized (sUidOwnerMap) {
+ final int err = native_replaceUidChain(chainName, isAllowlist, uids);
+ if (err != 0) {
+ Log.e(TAG, "replaceUidChain failed: " + Os.strerror(-err));
+ }
+ return -err;
}
- return -err;
}
/**
@@ -165,8 +391,10 @@
* cause of the failure.
*/
public void setUidRule(final int childChain, final int uid, final int firewallRule) {
- final int err = native_setUidRule(childChain, uid, firewallRule);
- maybeThrow(err, "Unable to set uid rule");
+ synchronized (sUidOwnerMap) {
+ final int err = native_setUidRule(childChain, uid, firewallRule);
+ maybeThrow(err, "Unable to set uid rule");
+ }
}
/**
@@ -187,12 +415,14 @@
* cause of the failure.
*/
public void addUidInterfaceRules(final String ifName, final int[] uids) throws RemoteException {
- if (USE_NETD) {
+ if (PRE_T) {
mNetd.firewallAddUidInterfaceRules(ifName, uids);
return;
}
- final int err = native_addUidInterfaceRules(ifName, uids);
- maybeThrow(err, "Unable to add uid interface rules");
+ synchronized (sUidOwnerMap) {
+ final int err = native_addUidInterfaceRules(ifName, uids);
+ maybeThrow(err, "Unable to add uid interface rules");
+ }
}
/**
@@ -207,12 +437,31 @@
* cause of the failure.
*/
public void removeUidInterfaceRules(final int[] uids) throws RemoteException {
- if (USE_NETD) {
+ if (PRE_T) {
mNetd.firewallRemoveUidInterfaceRules(uids);
return;
}
- final int err = native_removeUidInterfaceRules(uids);
- maybeThrow(err, "Unable to remove uid interface rules");
+ synchronized (sUidOwnerMap) {
+ final int err = native_removeUidInterfaceRules(uids);
+ maybeThrow(err, "Unable to remove uid interface rules");
+ }
+ }
+
+ /**
+ * Update lockdown rule for uid
+ *
+ * @param uid target uid to add/remove the rule
+ * @param add {@code true} to add the rule, {@code false} to remove the rule.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void updateUidLockdownRule(final int uid, final boolean add) {
+ throwIfPreT("updateUidLockdownRule is not available on pre-T devices");
+ if (add) {
+ addRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
+ } else {
+ removeRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
+ }
}
/**
@@ -237,7 +486,7 @@
* @throws RemoteException when netd has crashed.
*/
public void setNetPermForUids(final int permissions, final int[] uids) throws RemoteException {
- if (USE_NETD) {
+ if (PRE_T) {
mNetd.trafficSetNetPermForUids(permissions, uids);
return;
}
@@ -253,7 +502,7 @@
*/
public void dump(final FileDescriptor fd, boolean verbose)
throws IOException, ServiceSpecificException {
- if (USE_NETD) {
+ if (PRE_T) {
throw new ServiceSpecificException(
EOPNOTSUPP, "dumpsys connectivity trafficcontroller dump not available on pre-T"
+ " devices, use dumpsys netd trafficcontroller instead.");
@@ -262,15 +511,24 @@
}
private static native void native_init();
+ @GuardedBy("sUidOwnerMap")
private native int native_addNaughtyApp(int uid);
+ @GuardedBy("sUidOwnerMap")
private native int native_removeNaughtyApp(int uid);
+ @GuardedBy("sUidOwnerMap")
private native int native_addNiceApp(int uid);
+ @GuardedBy("sUidOwnerMap")
private native int native_removeNiceApp(int uid);
- private native int native_setChildChain(int childChain, boolean enable);
+ @GuardedBy("sUidOwnerMap")
private native int native_replaceUidChain(String name, boolean isAllowlist, int[] uids);
+ @GuardedBy("sUidOwnerMap")
private native int native_setUidRule(int childChain, int uid, int firewallRule);
+ @GuardedBy("sUidOwnerMap")
private native int native_addUidInterfaceRules(String ifName, int[] uids);
+ @GuardedBy("sUidOwnerMap")
private native int native_removeUidInterfaceRules(int[] uids);
+ @GuardedBy("sUidOwnerMap")
+ private native int native_updateUidLockdownRule(int uid, boolean add);
private native int native_swapActiveStatsMap();
private native void native_setPermissionForUids(int permissions, int[] uids);
private native void native_dump(FileDescriptor fd, boolean verbose);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index d79bdb8..37fc391 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -108,6 +108,7 @@
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
import android.app.usage.NetworkStatsManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -258,6 +259,7 @@
import com.android.net.module.util.netlink.InetDiagMessage;
import com.android.server.connectivity.AutodestructReference;
import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
+import com.android.server.connectivity.ClatCoordinator;
import com.android.server.connectivity.ConnectivityFlags;
import com.android.server.connectivity.DnsManager;
import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
@@ -608,13 +610,6 @@
// Handle private DNS validation status updates.
private static final int EVENT_PRIVATE_DNS_VALIDATION_UPDATE = 38;
- /**
- * used to remove a network request, either a listener or a real request and call unavailable
- * arg1 = UID of caller
- * obj = NetworkRequest
- */
- private static final int EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE = 39;
-
/**
* Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the network has
* been tested.
@@ -753,7 +748,7 @@
* The BPF program attached to the tc-police hook to account for to-be-dropped traffic.
*/
private static final String TC_POLICE_BPF_PROG_PATH =
- "/sys/fs/bpf/prog_netd_schedact_ingress_account";
+ "/sys/fs/bpf/netd_shared/prog_netd_schedact_ingress_account";
private static String eventName(int what) {
return sMagicDecoderRing.get(what, Integer.toString(what));
@@ -1192,6 +1187,7 @@
/**
* Keeps track of the number of requests made under different uids.
*/
+ // TODO: Remove the hack and use com.android.net.module.util.PerUidCounter instead.
public static class PerUidCounter {
private final int mMaxCountPerUid;
@@ -1405,6 +1401,19 @@
}
/**
+ * @see ClatCoordinator
+ */
+ public ClatCoordinator getClatCoordinator(INetd netd) {
+ return new ClatCoordinator(
+ new ClatCoordinator.Dependencies() {
+ @NonNull
+ public INetd getNetd() {
+ return netd;
+ }
+ });
+ }
+
+ /**
* Wraps {@link TcUtils#tcFilterAddDevIngressPolice}
*/
public void enableIngressRateLimit(String iface, long rateInBytesPerSecond) {
@@ -2613,7 +2622,7 @@
verifyCallingUidAndPackage(callingPackageName, mDeps.getCallingUid());
enforceChangePermission(callingPackageName, callingAttributionTag);
if (mProtectedNetworks.contains(networkType)) {
- enforceConnectivityRestrictedNetworksPermission();
+ enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
}
InetAddress addr;
@@ -2865,7 +2874,7 @@
private void enforceNetworkFactoryPermission() {
// TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
- if (getCallingUid() == Process.BLUETOOTH_UID) return;
+ if (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return;
enforceAnyPermissionOf(
android.Manifest.permission.NETWORK_FACTORY,
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
@@ -2873,7 +2882,7 @@
private void enforceNetworkFactoryOrSettingsPermission() {
// TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
- if (getCallingUid() == Process.BLUETOOTH_UID) return;
+ if (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return;
enforceAnyPermissionOf(
android.Manifest.permission.NETWORK_SETTINGS,
android.Manifest.permission.NETWORK_FACTORY,
@@ -2882,7 +2891,7 @@
private void enforceNetworkFactoryOrTestNetworksPermission() {
// TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface.
- if (getCallingUid() == Process.BLUETOOTH_UID) return;
+ if (UserHandle.getAppId(getCallingUid()) == Process.BLUETOOTH_UID) return;
enforceAnyPermissionOf(
android.Manifest.permission.MANAGE_TEST_NETWORKS,
android.Manifest.permission.NETWORK_FACTORY,
@@ -2896,7 +2905,7 @@
android.Manifest.permission.NETWORK_SETTINGS, pid, uid)
|| PERMISSION_GRANTED == mContext.checkPermission(
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, pid, uid)
- || uid == Process.BLUETOOTH_UID;
+ || UserHandle.getAppId(uid) == Process.BLUETOOTH_UID;
}
private boolean checkSettingsPermission() {
@@ -2967,18 +2976,35 @@
android.Manifest.permission.NETWORK_SETTINGS);
}
- private void enforceConnectivityRestrictedNetworksPermission() {
- try {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS,
- "ConnectivityService");
- return;
- } catch (SecurityException e) { /* fallback to ConnectivityInternalPermission */ }
- // TODO: Remove this fallback check after all apps have declared
- // CONNECTIVITY_USE_RESTRICTED_NETWORKS.
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.CONNECTIVITY_INTERNAL,
- "ConnectivityService");
+ private boolean checkConnectivityRestrictedNetworksPermission(int callingUid,
+ boolean checkUidsAllowedList) {
+ if (PermissionUtils.checkAnyPermissionOf(mContext,
+ android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS)) {
+ return true;
+ }
+
+ // fallback to ConnectivityInternalPermission
+ // TODO: Remove this fallback check after all apps have declared
+ // CONNECTIVITY_USE_RESTRICTED_NETWORKS.
+ if (PermissionUtils.checkAnyPermissionOf(mContext,
+ android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
+ return true;
+ }
+
+ // Check whether uid is in allowed on restricted networks list.
+ if (checkUidsAllowedList
+ && mPermissionMonitor.isUidAllowedOnRestrictedNetworks(callingUid)) {
+ return true;
+ }
+ return false;
+ }
+
+ private void enforceConnectivityRestrictedNetworksPermission(boolean checkUidsAllowedList) {
+ final int callingUid = mDeps.getCallingUid();
+ if (!checkConnectivityRestrictedNetworksPermission(callingUid, checkUidsAllowedList)) {
+ throw new SecurityException("ConnectivityService: user " + callingUid
+ + " has no permission to access restricted network.");
+ }
}
private void enforceKeepalivePermission() {
@@ -3266,11 +3292,12 @@
return;
}
- pw.print("NetworkProviders for:");
+ pw.println("NetworkProviders for:");
+ pw.increaseIndent();
for (NetworkProviderInfo npi : mNetworkProviderInfos.values()) {
- pw.print(" " + npi.name);
+ pw.println(npi.providerId + ": " + npi.name);
}
- pw.println();
+ pw.decreaseIndent();
pw.println();
final NetworkAgentInfo defaultNai = getDefaultNetwork();
@@ -3319,6 +3346,14 @@
pw.decreaseIndent();
pw.println();
+ pw.println("Network Offers:");
+ pw.increaseIndent();
+ for (final NetworkOfferInfo offerInfo : mNetworkOffers) {
+ pw.println(offerInfo.offer);
+ }
+ pw.decreaseIndent();
+ pw.println();
+
mLegacyTypeTracker.dump(pw);
pw.println();
@@ -3387,12 +3422,27 @@
pw.increaseIndent();
mNetworkActivityTracker.dump(pw);
pw.decreaseIndent();
+
+ // pre-T is logged by netd.
+ if (SdkLevel.isAtLeastT()) {
+ pw.println();
+ pw.println("BPF programs & maps:");
+ pw.increaseIndent();
+ // Flush is required. Otherwise, the traces in fd can interleave with traces in pw.
+ pw.flush();
+ dumpTrafficController(pw, fd, /*verbose=*/ true);
+ pw.decreaseIndent();
+ }
}
private void dumpNetworks(IndentingPrintWriter pw) {
for (NetworkAgentInfo nai : networksSortedById()) {
pw.println(nai.toString());
pw.increaseIndent();
+ pw.println("Nat464Xlat:");
+ pw.increaseIndent();
+ nai.dumpNat464Xlat(pw);
+ pw.decreaseIndent();
pw.println(String.format(
"Requests: REQUEST:%d LISTEN:%d BACKGROUND_REQUEST:%d total:%d",
nai.numForegroundNetworkRequests(),
@@ -3669,7 +3719,7 @@
}
case NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES: {
if (mDscpPolicyTracker != null) {
- mDscpPolicyTracker.removeAllDscpPolicies(nai);
+ mDscpPolicyTracker.removeAllDscpPolicies(nai, true);
}
break;
}
@@ -3833,7 +3883,6 @@
}
final boolean wasValidated = nai.lastValidated;
- final boolean wasDefault = isDefaultNetwork(nai);
final boolean wasPartial = nai.partialConnectivity;
nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
final boolean partialConnectivityChanged =
@@ -4410,6 +4459,9 @@
}
private void destroyNativeNetwork(@NonNull NetworkAgentInfo nai) {
+ if (mDscpPolicyTracker != null) {
+ mDscpPolicyTracker.removeAllDscpPolicies(nai, false);
+ }
try {
mNetd.networkDestroy(nai.network.getNetId());
} catch (RemoteException | ServiceSpecificException e) {
@@ -4464,7 +4516,7 @@
private boolean hasCarrierPrivilegeForNetworkCaps(final int callingUid,
@NonNull final NetworkCapabilities caps) {
- if (SdkLevel.isAtLeastT() && mCarrierPrivilegeAuthenticator != null) {
+ if (mCarrierPrivilegeAuthenticator != null) {
return mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
callingUid, caps);
}
@@ -4494,7 +4546,6 @@
private void handleRegisterNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) {
ensureRunningOnConnectivityServiceThread();
- NetworkRequest requestToBeReleased = null;
for (final NetworkRequestInfo nri : nris) {
mNetworkRequestInfoLogs.log("REGISTER " + nri);
checkNrisConsistency(nri);
@@ -4509,13 +4560,6 @@
}
}
}
- if (req.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
- if (!hasCarrierPrivilegeForNetworkCaps(nri.mUid, req.networkCapabilities)
- && !checkConnectivityRestrictedNetworksPermission(
- nri.mPid, nri.mUid)) {
- requestToBeReleased = req;
- }
- }
}
// If this NRI has a satisfier already, it is replacing an older request that
@@ -4527,11 +4571,6 @@
}
}
- if (requestToBeReleased != null) {
- releaseNetworkRequestAndCallOnUnavailable(requestToBeReleased);
- return;
- }
-
if (mFlags.noRematchAllRequestsOnRegister()) {
rematchNetworksAndRequests(nris);
} else {
@@ -5371,11 +5410,6 @@
/* callOnUnavailable */ false);
break;
}
- case EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE: {
- handleReleaseNetworkRequest((NetworkRequest) msg.obj, msg.arg1,
- /* callOnUnavailable */ true);
- break;
- }
case EVENT_SET_ACCEPT_UNVALIDATED: {
Network network = (Network) msg.obj;
handleSetAcceptUnvalidated(network, toBool(msg.arg1), toBool(msg.arg2));
@@ -5945,6 +5979,10 @@
+ Arrays.toString(ranges) + "): netd command failed: " + e);
}
+ if (SdkLevel.isAtLeastT()) {
+ mPermissionMonitor.updateVpnLockdownUidRanges(requireVpn, ranges);
+ }
+
for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
final boolean curMetered = nai.networkCapabilities.isMetered();
maybeNotifyNetworkBlocked(nai, curMetered, curMetered,
@@ -6325,7 +6363,7 @@
if (null != satisfier) {
// If the old NRI was satisfied by an NAI, then it may have had an active request.
// The active request is necessary to figure out what callbacks to send, in
- // particular then a network updates its capabilities.
+ // particular when a network updates its capabilities.
// As this code creates a new NRI with a new set of requests, figure out which of
// the list of requests should be the active request. It is always the first
// request of the list that can be satisfied by the satisfier since the order of
@@ -6594,7 +6632,7 @@
case REQUEST:
networkCapabilities = new NetworkCapabilities(networkCapabilities);
enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
- callingAttributionTag);
+ callingAttributionTag, callingUid);
// TODO: this is incorrect. We mark the request as metered or not depending on
// the state of the app when the request is filed, but we never change the
// request if the app changes network state. http://b/29964605
@@ -6684,26 +6722,19 @@
}
private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities,
- String callingPackageName, String callingAttributionTag) {
+ String callingPackageName, String callingAttributionTag, final int callingUid) {
if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) {
- if (!networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
- enforceConnectivityRestrictedNetworksPermission();
+ // For T+ devices, callers with carrier privilege could request with CBS capabilities.
+ if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)
+ && hasCarrierPrivilegeForNetworkCaps(callingUid, networkCapabilities)) {
+ return;
}
+ enforceConnectivityRestrictedNetworksPermission(true /* checkUidsAllowedList */);
} else {
enforceChangePermission(callingPackageName, callingAttributionTag);
}
}
- private boolean checkConnectivityRestrictedNetworksPermission(int callerPid, int callerUid) {
- if (checkAnyPermissionOf(callerPid, callerUid,
- android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS)
- || checkAnyPermissionOf(callerPid, callerUid,
- android.Manifest.permission.CONNECTIVITY_INTERNAL)) {
- return true;
- }
- return false;
- }
-
@Override
public boolean requestBandwidthUpdate(Network network) {
enforceAccessPermission();
@@ -6762,7 +6793,7 @@
final int callingUid = mDeps.getCallingUid();
networkCapabilities = new NetworkCapabilities(networkCapabilities);
enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
- callingAttributionTag);
+ callingAttributionTag, callingUid);
enforceMeteredApnPolicy(networkCapabilities);
ensureRequestableCapabilities(networkCapabilities);
ensureSufficientPermissionsForRequest(networkCapabilities,
@@ -6885,13 +6916,6 @@
EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest));
}
- private void releaseNetworkRequestAndCallOnUnavailable(NetworkRequest networkRequest) {
- ensureNetworkRequestHasType(networkRequest);
- mHandler.sendMessage(mHandler.obtainMessage(
- EVENT_RELEASE_NETWORK_REQUEST_AND_CALL_UNAVAILABLE, mDeps.getCallingUid(), 0,
- networkRequest));
- }
-
private void handleRegisterNetworkProvider(NetworkProviderInfo npi) {
if (mNetworkProviderInfos.containsKey(npi.messenger)) {
// Avoid creating duplicates. even if an app makes a direct AIDL call.
@@ -7722,10 +7746,10 @@
private void updateVpnFiltering(LinkProperties newLp, LinkProperties oldLp,
NetworkAgentInfo nai) {
- final String oldIface = oldLp != null ? oldLp.getInterfaceName() : null;
- final String newIface = newLp != null ? newLp.getInterfaceName() : null;
- final boolean wasFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, oldLp);
- final boolean needsFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, newLp);
+ final String oldIface = getVpnIsolationInterface(nai, nai.networkCapabilities, oldLp);
+ final String newIface = getVpnIsolationInterface(nai, nai.networkCapabilities, newLp);
+ final boolean wasFiltering = requiresVpnAllowRule(nai, oldLp, oldIface);
+ final boolean needsFiltering = requiresVpnAllowRule(nai, newLp, newIface);
if (!wasFiltering && !needsFiltering) {
// Nothing to do.
@@ -7738,6 +7762,10 @@
}
final Set<UidRange> ranges = nai.networkCapabilities.getUidRanges();
+ if (ranges == null || ranges.isEmpty()) {
+ return;
+ }
+
final int vpnAppUid = nai.networkCapabilities.getOwnerUid();
// TODO: this create a window of opportunity for apps to receive traffic between the time
// when the old rules are removed and the time when new rules are added. To fix this,
@@ -7803,6 +7831,7 @@
}
nai.declaredCapabilities = new NetworkCapabilities(nc);
NetworkAgentInfo.restrictCapabilitiesFromNetworkAgent(nc, nai.creatorUid,
+ mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE),
mCarrierPrivilegeAuthenticator);
}
@@ -8031,15 +8060,14 @@
}
/**
- * Returns whether VPN isolation (ingress interface filtering) should be applied on the given
- * network.
+ * Returns the interface which requires VPN isolation (ingress interface filtering).
*
* Ingress interface filtering enforces that all apps under the given network can only receive
* packets from the network's interface (and loopback). This is important for VPNs because
* apps that cannot bypass a fully-routed VPN shouldn't be able to receive packets from any
* non-VPN interfaces.
*
- * As a result, this method should return true iff
+ * As a result, this method should return Non-null interface iff
* 1. the network is an app VPN (not legacy VPN)
* 2. the VPN does not allow bypass
* 3. the VPN is fully-routed
@@ -8048,15 +8076,34 @@
* @see INetd#firewallAddUidInterfaceRules
* @see INetd#firewallRemoveUidInterfaceRules
*/
- private boolean requiresVpnIsolation(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
+ @Nullable
+ private String getVpnIsolationInterface(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
LinkProperties lp) {
- if (nc == null || lp == null) return false;
- return nai.isVPN()
+ if (nc == null || lp == null) return null;
+ if (nai.isVPN()
&& !nai.networkAgentConfig.allowBypass
&& nc.getOwnerUid() != Process.SYSTEM_UID
&& lp.getInterfaceName() != null
&& (lp.hasIpv4DefaultRoute() || lp.hasIpv4UnreachableDefaultRoute())
- && (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute());
+ && (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute())
+ && !lp.hasExcludeRoute()) {
+ return lp.getInterfaceName();
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether we need to set interface filtering rule or not
+ */
+ private boolean requiresVpnAllowRule(NetworkAgentInfo nai, LinkProperties lp,
+ String isolationIface) {
+ // Allow rules are always needed if VPN isolation is enabled.
+ if (isolationIface != null) return true;
+
+ // On T and above, allow rules are needed for all VPNs. Allow rule with null iface is a
+ // wildcard to allow apps to receive packets on all interfaces. This is required to accept
+ // incoming traffic in Lockdown mode by overriding the Lockdown blocking rule.
+ return SdkLevel.isAtLeastT() && nai.isVPN() && lp != null && lp.getInterfaceName() != null;
}
private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -8184,9 +8231,10 @@
if (!prevRanges.isEmpty()) {
updateVpnUidRanges(false, nai, prevRanges);
}
- final boolean wasFiltering = requiresVpnIsolation(nai, prevNc, nai.linkProperties);
- final boolean shouldFilter = requiresVpnIsolation(nai, newNc, nai.linkProperties);
- final String iface = nai.linkProperties.getInterfaceName();
+ final String oldIface = getVpnIsolationInterface(nai, prevNc, nai.linkProperties);
+ final String newIface = getVpnIsolationInterface(nai, newNc, nai.linkProperties);
+ final boolean wasFiltering = requiresVpnAllowRule(nai, nai.linkProperties, oldIface);
+ final boolean shouldFilter = requiresVpnAllowRule(nai, nai.linkProperties, newIface);
// For VPN uid interface filtering, old ranges need to be removed before new ranges can
// be added, due to the range being expanded and stored as individual UIDs. For example
// the UIDs might be updated from [0, 99999] to ([0, 10012], [10014, 99999]) which means
@@ -8199,10 +8247,11 @@
// TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range
// to be removed will never overlap with the new range to be added.
if (wasFiltering && !prevRanges.isEmpty()) {
- mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges, prevNc.getOwnerUid());
+ mPermissionMonitor.onVpnUidRangesRemoved(oldIface, prevRanges,
+ prevNc.getOwnerUid());
}
if (shouldFilter && !newRanges.isEmpty()) {
- mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges, newNc.getOwnerUid());
+ mPermissionMonitor.onVpnUidRangesAdded(newIface, newRanges, newNc.getOwnerUid());
}
} catch (Exception e) {
// Never crash!
@@ -9207,7 +9256,18 @@
params.networkCapabilities = networkAgent.networkCapabilities;
params.linkProperties = new LinkProperties(networkAgent.linkProperties,
true /* parcelSensitiveFields */);
- networkAgent.networkMonitor().notifyNetworkConnected(params);
+ // isAtLeastT() is conservative here, as recent versions of NetworkStack support the
+ // newer callback even before T. However getInterfaceVersion is a synchronized binder
+ // call that would cause a Log.wtf to be emitted from the system_server process, and
+ // in the absence of a satisfactory, scalable solution which follows an easy/standard
+ // process to check the interface version, just use an SDK check. NetworkStack will
+ // always be new enough when running on T+.
+ if (SdkLevel.isAtLeastT()) {
+ networkAgent.networkMonitor().notifyNetworkConnected(params);
+ } else {
+ networkAgent.networkMonitor().notifyNetworkConnected(params.linkProperties,
+ params.networkCapabilities);
+ }
scheduleUnvalidatedPrompt(networkAgent);
// Whether a particular NetworkRequest listen should cause signal strength thresholds to
@@ -10593,7 +10653,11 @@
if (callback == null) throw new IllegalArgumentException("callback must be non-null");
if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
- enforceConnectivityRestrictedNetworksPermission();
+ // TODO: Check allowed list here and ensure that either a) any QoS callback registered
+ // on this network is unregistered when the app loses permission or b) no QoS
+ // callbacks are sent for restricted networks unless the app currently has permission
+ // to access restricted networks.
+ enforceConnectivityRestrictedNetworksPermission(false /* checkUidsAllowedList */);
}
mQosCallbackTracker.registerCallback(callback, filter, nai);
}
@@ -10609,13 +10673,29 @@
mQosCallbackTracker.unregisterCallback(callback);
}
+ private boolean isNetworkPreferenceAllowedForProfile(@NonNull UserHandle profile) {
+ // UserManager.isManagedProfile returns true for all apps in managed user profiles.
+ // Enterprise device can be fully managed like device owner and such use case
+ // also should be supported. Calling app check for work profile and fully managed device
+ // is already done in DevicePolicyManager.
+ // This check is an extra caution to be sure device is fully managed or not.
+ final UserManager um = mContext.getSystemService(UserManager.class);
+ final DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
+ if (um.isManagedProfile(profile.getIdentifier())) {
+ return true;
+ }
+ if (SdkLevel.isAtLeastT() && dpm.getDeviceOwner() != null) return true;
+ return false;
+ }
+
/**
- * Request that a user profile is put by default on a network matching a given preference.
+ * Set a list of default network selection policies for a user profile or device owner.
*
* See the documentation for the individual preferences for a description of the supported
* behaviors.
*
- * @param profile the user profile for whih the preference is being set.
+ * @param profile If the device owner is set, any profile is allowed.
+ Otherwise, the given profile can only be managed profile.
* @param preferences the list of profile network preferences for the
* provided profile.
* @param listener an optional listener to listen for completion of the operation.
@@ -10629,7 +10709,10 @@
Objects.requireNonNull(profile);
if (preferences.size() == 0) {
- preferences.add((new ProfileNetworkPreference.Builder()).build());
+ final ProfileNetworkPreference pref = new ProfileNetworkPreference.Builder()
+ .setPreference(ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT)
+ .build();
+ preferences.add(pref);
}
PermissionUtils.enforceNetworkStackPermission(mContext);
@@ -10640,19 +10723,21 @@
throw new IllegalArgumentException("Must explicitly specify a user handle ("
+ "UserHandle.CURRENT not supported)");
}
- final UserManager um = mContext.getSystemService(UserManager.class);
- if (!um.isManagedProfile(profile.getIdentifier())) {
- throw new IllegalArgumentException("Profile must be a managed profile");
+ if (!isNetworkPreferenceAllowedForProfile(profile)) {
+ throw new IllegalArgumentException("Profile must be a managed profile "
+ + "or the device owner must be set. ");
}
final List<ProfileNetworkPreferenceList.Preference> preferenceList =
new ArrayList<ProfileNetworkPreferenceList.Preference>();
- boolean allowFallback = true;
+ boolean hasDefaultPreference = false;
for (final ProfileNetworkPreference preference : preferences) {
final NetworkCapabilities nc;
+ boolean allowFallback = true;
switch (preference.getPreference()) {
case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT:
nc = null;
+ hasDefaultPreference = true;
if (preference.getPreferenceEnterpriseId() != 0) {
throw new IllegalArgumentException(
"Invalid enterprise identifier in setProfileNetworkPreferences");
@@ -10662,6 +10747,14 @@
allowFallback = false;
// continue to process the enterprise preference.
case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE:
+ // This code is needed even though there is a check later on,
+ // because isRangeAlreadyInPreferenceList assumes that every preference
+ // has a UID list.
+ if (hasDefaultPreference) {
+ throw new IllegalArgumentException(
+ "Default profile preference should not be set along with other "
+ + "preference");
+ }
if (!isEnterpriseIdentifierValid(preference.getPreferenceEnterpriseId())) {
throw new IllegalArgumentException(
"Invalid enterprise identifier in setProfileNetworkPreferences");
@@ -10685,6 +10778,10 @@
}
preferenceList.add(new ProfileNetworkPreferenceList.Preference(
profile, nc, allowFallback));
+ if (hasDefaultPreference && preferenceList.size() > 1) {
+ throw new IllegalArgumentException(
+ "Default profile preference should not be set along with other preference");
+ }
}
mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_PROFILE_NETWORK_PREFERENCE,
new Pair<>(preferenceList, listener)));
@@ -10729,12 +10826,6 @@
return false;
}
- private void validateNetworkCapabilitiesOfProfileNetworkPreference(
- @Nullable final NetworkCapabilities nc) {
- if (null == nc) return; // Null caps are always allowed. It means to remove the setting.
- ensureRequestableCapabilities(nc);
- }
-
private ArraySet<NetworkRequestInfo> createNrisFromProfileNetworkPreferences(
@NonNull final ProfileNetworkPreferenceList prefs) {
final ArraySet<NetworkRequestInfo> result = new ArraySet<>();
@@ -10785,10 +10876,19 @@
private void handleSetProfileNetworkPreference(
@NonNull final List<ProfileNetworkPreferenceList.Preference> preferenceList,
@Nullable final IOnCompleteListener listener) {
+ /*
+ * handleSetProfileNetworkPreference is always called for single user.
+ * preferenceList only contains preferences for different uids within the same user
+ * (enforced by getUidListToBeAppliedForNetworkPreference).
+ * Clear all the existing preferences for the user before applying new preferences.
+ *
+ */
+ mProfileNetworkPreferences = mProfileNetworkPreferences.withoutUser(
+ preferenceList.get(0).user);
for (final ProfileNetworkPreferenceList.Preference preference : preferenceList) {
- validateNetworkCapabilitiesOfProfileNetworkPreference(preference.capabilities);
mProfileNetworkPreferences = mProfileNetworkPreferences.plus(preference);
}
+
removeDefaultNetworkRequestsForPreference(PREFERENCE_ORDER_PROFILE);
addPerAppDefaultNetworkRequests(
createNrisFromProfileNetworkPreferences(mProfileNetworkPreferences));
@@ -11264,6 +11364,9 @@
final int defaultRule;
switch (chain) {
case ConnectivityManager.FIREWALL_CHAIN_STANDBY:
+ case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1:
+ case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2:
+ case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3:
defaultRule = FIREWALL_RULE_ALLOW;
break;
case ConnectivityManager.FIREWALL_CHAIN_DOZABLE:
@@ -11292,6 +11395,13 @@
}
@Override
+ public boolean getFirewallChainEnabled(final int chain) {
+ enforceNetworkStackOrSettingsPermission();
+
+ return mBpfNetMaps.isChainEnabled(chain);
+ }
+
+ @Override
public void replaceFirewallChain(final int chain, final int[] uids) {
enforceNetworkStackOrSettingsPermission();
@@ -11313,6 +11423,15 @@
mBpfNetMaps.replaceUidChain("fw_low_power_standby", true /* isAllowList */,
uids);
break;
+ case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1:
+ mBpfNetMaps.replaceUidChain("fw_oem_deny_1", false /* isAllowList */, uids);
+ break;
+ case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2:
+ mBpfNetMaps.replaceUidChain("fw_oem_deny_2", false /* isAllowList */, uids);
+ break;
+ case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3:
+ mBpfNetMaps.replaceUidChain("fw_oem_deny_3", false /* isAllowList */, uids);
+ break;
default:
throw new IllegalArgumentException("replaceFirewallChain with invalid chain: "
+ chain);
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index ccc2776..1209579 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -16,6 +16,7 @@
package com.android.server;
+import static android.net.TestNetworkManager.CLAT_INTERFACE_PREFIX;
import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
import static android.net.TestNetworkManager.TEST_TUN_PREFIX;
@@ -49,6 +50,7 @@
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.NetworkStackConstants;
+import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
@@ -75,7 +77,11 @@
@NonNull private final NetworkProvider mNetworkProvider;
// Native method stubs
- private static native int jniCreateTunTap(boolean isTun, @NonNull String iface);
+ private static native int nativeCreateTunTap(boolean isTun, boolean hasCarrier,
+ @NonNull String iface);
+
+ private static native void nativeSetTunTapCarrierEnabled(@NonNull String iface, int tunFd,
+ boolean enabled);
@VisibleForTesting
protected TestNetworkService(@NonNull Context context) {
@@ -98,6 +104,14 @@
}
}
+ // TODO: find a way to allow the caller to pass in non-clat interface names, ensuring that
+ // those names do not conflict with names created by callers that do not pass in an interface
+ // name.
+ private static boolean isValidInterfaceName(@NonNull final String iface) {
+ return iface.startsWith(CLAT_INTERFACE_PREFIX + TEST_TUN_PREFIX)
+ || iface.startsWith(CLAT_INTERFACE_PREFIX + TEST_TAP_PREFIX);
+ }
+
/**
* Create a TUN or TAP interface with the specified parameters.
*
@@ -105,30 +119,36 @@
* interface.
*/
@Override
- public TestNetworkInterface createInterface(boolean isTun, boolean bringUp,
- LinkAddress[] linkAddrs) {
+ public TestNetworkInterface createInterface(boolean isTun, boolean hasCarrier, boolean bringUp,
+ LinkAddress[] linkAddrs, @Nullable String iface) {
enforceTestNetworkPermissions(mContext);
Objects.requireNonNull(linkAddrs, "missing linkAddrs");
- String ifacePrefix = isTun ? TEST_TUN_PREFIX : TEST_TAP_PREFIX;
- String iface = ifacePrefix + sTestTunIndex.getAndIncrement();
+ String interfaceName = iface;
+ if (iface == null) {
+ String ifacePrefix = isTun ? TEST_TUN_PREFIX : TEST_TAP_PREFIX;
+ interfaceName = ifacePrefix + sTestTunIndex.getAndIncrement();
+ } else if (!isValidInterfaceName(iface)) {
+ throw new IllegalArgumentException("invalid interface name requested: " + iface);
+ }
+
final long token = Binder.clearCallingIdentity();
try {
- ParcelFileDescriptor tunIntf =
- ParcelFileDescriptor.adoptFd(jniCreateTunTap(isTun, iface));
+ ParcelFileDescriptor tunIntf = ParcelFileDescriptor.adoptFd(
+ nativeCreateTunTap(isTun, hasCarrier, interfaceName));
for (LinkAddress addr : linkAddrs) {
mNetd.interfaceAddAddress(
- iface,
+ interfaceName,
addr.getAddress().getHostAddress(),
addr.getPrefixLength());
}
if (bringUp) {
- NetdUtils.setInterfaceUp(mNetd, iface);
+ NetdUtils.setInterfaceUp(mNetd, interfaceName);
}
- return new TestNetworkInterface(tunIntf, iface);
+ return new TestNetworkInterface(tunIntf, interfaceName);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} finally {
@@ -360,4 +380,20 @@
public static void enforceTestNetworkPermissions(@NonNull Context context) {
context.enforceCallingOrSelfPermission(PERMISSION_NAME, "TestNetworkService");
}
+
+ /** Enable / disable TestNetworkInterface carrier */
+ @Override
+ public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
+ enforceTestNetworkPermissions(mContext);
+ nativeSetTunTapCarrierEnabled(iface.getInterfaceName(), iface.getFileDescriptor().getFd(),
+ enabled);
+ // Explicitly close fd after use to prevent StrictMode from complaining.
+ // Also, explicitly referencing iface guarantees that the object is not garbage collected
+ // before nativeSetTunTapCarrierEnabled() executes.
+ try {
+ iface.getFileDescriptor().close();
+ } catch (IOException e) {
+ // if the close fails, there is not much that can be done -- move on.
+ }
+ }
}
diff --git a/service/src/com/android/server/UidOwnerValue.java b/service/src/com/android/server/UidOwnerValue.java
new file mode 100644
index 0000000..f89e354
--- /dev/null
+++ b/service/src/com/android/server/UidOwnerValue.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import com.android.net.module.util.Struct;
+
+/** Value type for per uid traffic control configuration map */
+public class UidOwnerValue extends Struct {
+ // Allowed interface index. Only applicable if IIF_MATCH is set in the rule bitmask below.
+ @Field(order = 0, type = Type.U32)
+ public final long iif;
+
+ // A bitmask of match type.
+ @Field(order = 1, type = Type.U32)
+ public final long rule;
+
+ public UidOwnerValue(final long iif, final long rule) {
+ this.iif = iif;
+ this.rule = rule;
+ }
+}
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index 8aa5990..5ea586a 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -36,6 +36,7 @@
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.IBpfMap;
@@ -100,11 +101,11 @@
private static final String CLAT_INGRESS6_MAP_PATH = makeMapPath("ingress6");
private static String makeMapPath(String which) {
- return "/sys/fs/bpf/map_clatd_clat_" + which + "_map";
+ return "/sys/fs/bpf/net_shared/map_clatd_clat_" + which + "_map";
}
private static String makeProgPath(boolean ingress, boolean ether) {
- String path = "/sys/fs/bpf/prog_clatd_schedcls_"
+ String path = "/sys/fs/bpf/net_shared/prog_clatd_schedcls_"
+ (ingress ? "ingress6" : "egress4")
+ "_clat_"
+ (ether ? "ether" : "rawip");
@@ -122,8 +123,12 @@
@Nullable
private ClatdTracker mClatdTracker = null;
+ /**
+ * Dependencies of ClatCoordinator which makes ConnectivityService injection
+ * in tests.
+ */
@VisibleForTesting
- abstract static class Dependencies {
+ public abstract static class Dependencies {
/**
* Get netd.
*/
@@ -342,6 +347,19 @@
&& this.pid == that.pid
&& this.cookie == that.cookie;
}
+
+ @Override
+ public String toString() {
+ return "iface: " + iface
+ + " (" + ifIndex + ")"
+ + ", v4iface: " + v4iface
+ + " (" + v4ifIndex + ")"
+ + ", v4: " + v4
+ + ", v6: " + v6
+ + ", pfx96: " + pfx96
+ + ", pid: " + pid
+ + ", cookie: " + cookie;
+ }
};
@VisibleForTesting
@@ -493,6 +511,31 @@
}
}
+ private void maybeCleanUp(ParcelFileDescriptor tunFd, ParcelFileDescriptor readSock6,
+ ParcelFileDescriptor writeSock6) {
+ if (tunFd != null) {
+ try {
+ tunFd.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Fail to close tun file descriptor " + e);
+ }
+ }
+ if (readSock6 != null) {
+ try {
+ readSock6.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Fail to close read socket " + e);
+ }
+ }
+ if (writeSock6 != null) {
+ try {
+ writeSock6.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Fail to close write socket " + e);
+ }
+ }
+ }
+
/**
* Start clatd for a given interface and NAT64 prefix.
*/
@@ -541,8 +584,15 @@
// [3] Open, configure and bring up the tun interface.
// Create the v4-... tun interface.
+
+ // Initialize all required file descriptors with null pointer. This makes the following
+ // error handling easier. Simply always call #maybeCleanUp for closing file descriptors,
+ // if any valid ones, in error handling.
+ ParcelFileDescriptor tunFd = null;
+ ParcelFileDescriptor readSock6 = null;
+ ParcelFileDescriptor writeSock6 = null;
+
final String tunIface = CLAT_PREFIX + iface;
- final ParcelFileDescriptor tunFd;
try {
tunFd = mDeps.adoptFd(mDeps.createTunInterface(tunIface));
} catch (IOException e) {
@@ -551,7 +601,7 @@
final int tunIfIndex = mDeps.getInterfaceIndex(tunIface);
if (tunIfIndex == INVALID_IFINDEX) {
- tunFd.close();
+ maybeCleanUp(tunFd, readSock6, writeSock6);
throw new IOException("Fail to get interface index for interface " + tunIface);
}
@@ -559,24 +609,27 @@
try {
mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */);
} catch (RemoteException | ServiceSpecificException e) {
- tunFd.close();
Log.e(TAG, "Disable IPv6 on " + tunIface + " failed: " + e);
}
// Detect ipv4 mtu.
final Integer fwmark = getFwmark(netId);
- final int detectedMtu = mDeps.detectMtu(pfx96Str,
+ final int detectedMtu;
+ try {
+ detectedMtu = mDeps.detectMtu(pfx96Str,
ByteBuffer.wrap(GOOGLE_DNS_4.getAddress()).getInt(), fwmark);
+ } catch (IOException e) {
+ maybeCleanUp(tunFd, readSock6, writeSock6);
+ throw new IOException("Detect MTU on " + tunIface + " failed: " + e);
+ }
final int mtu = adjustMtu(detectedMtu);
Log.i(TAG, "ipv4 mtu is " + mtu);
- // TODO: add setIptablesDropRule
-
// Config tun interface mtu, address and bring up.
try {
mNetd.interfaceSetMtu(tunIface, mtu);
} catch (RemoteException | ServiceSpecificException e) {
- tunFd.close();
+ maybeCleanUp(tunFd, readSock6, writeSock6);
throw new IOException("Set MTU " + mtu + " on " + tunIface + " failed: " + e);
}
final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
@@ -588,14 +641,13 @@
try {
mNetd.interfaceSetCfg(ifConfig);
} catch (RemoteException | ServiceSpecificException e) {
- tunFd.close();
+ maybeCleanUp(tunFd, readSock6, writeSock6);
throw new IOException("Setting IPv4 address to " + ifConfig.ipv4Addr + "/"
+ ifConfig.prefixLength + " failed on " + ifConfig.ifName + ": " + e);
}
// [4] Open and configure local 464xlat read/write sockets.
// Opens a packet socket to receive IPv6 packets in clatd.
- final ParcelFileDescriptor readSock6;
try {
// Use a JNI call to get native file descriptor instead of Os.socket() because we would
// like to use ParcelFileDescriptor to manage file descriptor. But ctor
@@ -603,27 +655,23 @@
// descriptor to initialize ParcelFileDescriptor object instead.
readSock6 = mDeps.adoptFd(mDeps.openPacketSocket());
} catch (IOException e) {
- tunFd.close();
+ maybeCleanUp(tunFd, readSock6, writeSock6);
throw new IOException("Open packet socket failed: " + e);
}
// Opens a raw socket with a given fwmark to send IPv6 packets in clatd.
- final ParcelFileDescriptor writeSock6;
try {
// Use a JNI call to get native file descriptor instead of Os.socket(). See above
// reason why we use jniOpenPacketSocket6().
writeSock6 = mDeps.adoptFd(mDeps.openRawSocket6(fwmark));
} catch (IOException e) {
- tunFd.close();
- readSock6.close();
+ maybeCleanUp(tunFd, readSock6, writeSock6);
throw new IOException("Open raw socket failed: " + e);
}
final int ifIndex = mDeps.getInterfaceIndex(iface);
if (ifIndex == INVALID_IFINDEX) {
- tunFd.close();
- readSock6.close();
- writeSock6.close();
+ maybeCleanUp(tunFd, readSock6, writeSock6);
throw new IOException("Fail to get interface index for interface " + iface);
}
@@ -631,9 +679,7 @@
try {
mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6Str, ifIndex);
} catch (IOException e) {
- tunFd.close();
- readSock6.close();
- writeSock6.close();
+ maybeCleanUp(tunFd, readSock6, writeSock6);
throw new IOException("add anycast sockopt failed: " + e);
}
@@ -642,9 +688,7 @@
try {
cookie = mDeps.tagSocketAsClat(writeSock6.getFileDescriptor());
} catch (IOException e) {
- tunFd.close();
- readSock6.close();
- writeSock6.close();
+ maybeCleanUp(tunFd, readSock6, writeSock6);
throw new IOException("tag raw socket failed: " + e);
}
@@ -652,9 +696,7 @@
try {
mDeps.configurePacketSocket(readSock6.getFileDescriptor(), v6Str, ifIndex);
} catch (IOException e) {
- tunFd.close();
- readSock6.close();
- writeSock6.close();
+ maybeCleanUp(tunFd, readSock6, writeSock6);
throw new IOException("configure packet socket failed: " + e);
}
@@ -668,9 +710,9 @@
mDeps.untagSocket(cookie);
throw new IOException("Error start clatd on " + iface + ": " + e);
} finally {
- tunFd.close();
- readSock6.close();
- writeSock6.close();
+ // The file descriptors have been duplicated (dup2) to clatd in native_startClatd().
+ // Close these file descriptor stubs which are unused anymore.
+ maybeCleanUp(tunFd, readSock6, writeSock6);
}
// [6] Initialize and store clatd tracker object.
@@ -738,6 +780,69 @@
mClatdTracker = null;
}
+ private void dumpBpfIngress(@NonNull IndentingPrintWriter pw) {
+ if (mIngressMap == null) {
+ pw.println("No BPF ingress6 map");
+ return;
+ }
+
+ try {
+ if (mIngressMap.isEmpty()) {
+ pw.println("<empty>");
+ }
+ pw.println("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif");
+ pw.increaseIndent();
+ mIngressMap.forEach((k, v) -> {
+ // TODO: print interface name
+ pw.println(String.format("%d %s/96 %s -> %s %d", k.iif, k.pfx96, k.local6,
+ v.local4, v.oif));
+ });
+ pw.decreaseIndent();
+ } catch (ErrnoException e) {
+ pw.println("Error dumping BPF ingress6 map: " + e);
+ }
+ }
+
+ private void dumpBpfEgress(@NonNull IndentingPrintWriter pw) {
+ if (mEgressMap == null) {
+ pw.println("No BPF egress4 map");
+ return;
+ }
+
+ try {
+ if (mEgressMap.isEmpty()) {
+ pw.println("<empty>");
+ }
+ pw.println("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif");
+ pw.increaseIndent();
+ mEgressMap.forEach((k, v) -> {
+ // TODO: print interface name
+ pw.println(String.format("%d %s -> %s %s/96 %d %s", k.iif, k.local4, v.local6,
+ v.pfx96, v.oif, v.oifIsEthernet != 0 ? "ether" : "rawip"));
+ });
+ pw.decreaseIndent();
+ } catch (ErrnoException e) {
+ pw.println("Error dumping BPF egress4 map: " + e);
+ }
+ }
+
+ /**
+ * Dump the cordinator information.
+ *
+ * @param pw print writer.
+ */
+ public void dump(@NonNull IndentingPrintWriter pw) {
+ // TODO: move map dump to a global place to avoid duplicate dump while there are two or
+ // more IPv6 only networks.
+ pw.println("CLAT tracker: " + mClatdTracker.toString());
+ pw.println("Forwarding rules:");
+ pw.increaseIndent();
+ dumpBpfIngress(pw);
+ dumpBpfEgress(pw);
+ pw.decreaseIndent();
+ pw.println();
+ }
+
/**
* Get clatd tracker. For test only.
*/
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index cde6ea7..c1ba40e 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -47,10 +47,11 @@
private static final String TAG = ConnectivityNativeService.class.getSimpleName();
private static final String CGROUP_PATH = "/sys/fs/cgroup";
private static final String V4_PROG_PATH =
- "/sys/fs/bpf/prog_block_bind4_block_port";
+ "/sys/fs/bpf/net_shared/prog_block_bind4_block_port";
private static final String V6_PROG_PATH =
- "/sys/fs/bpf/prog_block_bind6_block_port";
- private static final String BLOCKED_PORTS_MAP_PATH = "/sys/fs/bpf/map_block_blocked_ports_map";
+ "/sys/fs/bpf/net_shared/prog_block_bind6_block_port";
+ private static final String BLOCKED_PORTS_MAP_PATH =
+ "/sys/fs/bpf/net_shared/map_block_blocked_ports_map";
private final Context mContext;
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
index 53b276e..7829d1a 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -19,6 +19,7 @@
import static android.net.NetworkAgent.DSCP_POLICY_STATUS_DELETED;
import static android.net.NetworkAgent.DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
import static android.net.NetworkAgent.DSCP_POLICY_STATUS_POLICY_NOT_FOUND;
+import static android.net.NetworkAgent.DSCP_POLICY_STATUS_REQUEST_DECLINED;
import static android.net.NetworkAgent.DSCP_POLICY_STATUS_SUCCESS;
import static android.system.OsConstants.ETH_P_ALL;
@@ -37,6 +38,7 @@
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.NetworkInterface;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
@@ -50,7 +52,7 @@
private static final String TAG = DscpPolicyTracker.class.getSimpleName();
private static final String PROG_PATH =
- "/sys/fs/bpf/prog_dscp_policy_schedcls_set_dscp";
+ "/sys/fs/bpf/net_shared/prog_dscp_policy_schedcls_set_dscp";
// Name is "map + *.o + map_name + map". Can probably shorten this
private static final String IPV4_POLICY_MAP_PATH = makeMapPath(
"dscp_policy_ipv4_dscp_policies");
@@ -59,37 +61,75 @@
private static final int MAX_POLICIES = 16;
private static String makeMapPath(String which) {
- return "/sys/fs/bpf/map_" + which + "_map";
+ return "/sys/fs/bpf/net_shared/map_" + which + "_map";
}
private Set<String> mAttachedIfaces;
private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv4Policies;
private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv6Policies;
- private final SparseIntArray mPolicyIdToBpfMapIndex;
+
+ // The actual policy rules used by the BPF code to process packets
+ // are in mBpfDscpIpv4Policies and mBpfDscpIpv4Policies. Both of
+ // these can contain up to MAX_POLICIES rules.
+ //
+ // A given policy always consumes one entry in both the IPv4 and
+ // IPv6 maps even if if's an IPv4-only or IPv6-only policy.
+ //
+ // Each interface index has a SparseIntArray of rules which maps a
+ // policy ID to the index of the corresponding rule in the maps.
+ // mIfaceIndexToPolicyIdBpfMapIndex maps the interface index to
+ // the per-interface SparseIntArray.
+ private final HashMap<Integer, SparseIntArray> mIfaceIndexToPolicyIdBpfMapIndex;
public DscpPolicyTracker() throws ErrnoException {
mAttachedIfaces = new HashSet<String>();
-
- mPolicyIdToBpfMapIndex = new SparseIntArray(MAX_POLICIES);
+ mIfaceIndexToPolicyIdBpfMapIndex = new HashMap<Integer, SparseIntArray>();
mBpfDscpIpv4Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH,
BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
mBpfDscpIpv6Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,
BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
}
+ private boolean isUnusedIndex(int index) {
+ for (SparseIntArray ifacePolicies : mIfaceIndexToPolicyIdBpfMapIndex.values()) {
+ if (ifacePolicies.indexOfValue(index) >= 0) return false;
+ }
+ return true;
+ }
+
private int getFirstFreeIndex() {
+ if (mIfaceIndexToPolicyIdBpfMapIndex.size() == 0) return 0;
for (int i = 0; i < MAX_POLICIES; i++) {
- if (mPolicyIdToBpfMapIndex.indexOfValue(i) < 0) return i;
+ if (isUnusedIndex(i)) {
+ return i;
+ }
}
return MAX_POLICIES;
}
+ private int findIndex(int policyId, int ifIndex) {
+ SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(ifIndex);
+ if (ifacePolicies != null) {
+ final int existingIndex = ifacePolicies.get(policyId, -1);
+ if (existingIndex != -1) {
+ return existingIndex;
+ }
+ }
+
+ final int firstIndex = getFirstFreeIndex();
+ if (firstIndex >= MAX_POLICIES) {
+ // New policy is being added, but max policies has already been reached.
+ return -1;
+ }
+ return firstIndex;
+ }
+
private void sendStatus(NetworkAgentInfo nai, int policyId, int status) {
try {
nai.networkAgent.onDscpPolicyStatusUpdated(policyId, status);
} catch (RemoteException e) {
- Log.d(TAG, "Failed update policy status: ", e);
+ Log.e(TAG, "Failed update policy status: ", e);
}
}
@@ -107,37 +147,43 @@
|| policy.getSourceAddress() instanceof Inet6Address));
}
- private int addDscpPolicyInternal(DscpPolicy policy) {
+ private int getIfaceIndex(NetworkAgentInfo nai) {
+ String iface = nai.linkProperties.getInterfaceName();
+ NetworkInterface netIface;
+ try {
+ netIface = NetworkInterface.getByName(iface);
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to get iface index for " + iface + ": " + e);
+ netIface = null;
+ }
+ return (netIface != null) ? netIface.getIndex() : 0;
+ }
+
+ private int addDscpPolicyInternal(DscpPolicy policy, int ifIndex) {
// If there is no existing policy with a matching ID, and we are already at
// the maximum number of policies then return INSUFFICIENT_PROCESSING_RESOURCES.
- final int existingIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId(), -1);
- if (existingIndex == -1 && mPolicyIdToBpfMapIndex.size() >= MAX_POLICIES) {
- return DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+ SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(ifIndex);
+ if (ifacePolicies == null) {
+ ifacePolicies = new SparseIntArray(MAX_POLICIES);
}
// Currently all classifiers are supported, if any are removed return
// DSCP_POLICY_STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED,
// and for any other generic error DSCP_POLICY_STATUS_REQUEST_DECLINED
- int addIndex = 0;
- // If a policy with a matching ID exists, replace it, otherwise use the next free
- // index for the policy.
- if (existingIndex != -1) {
- addIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId());
- } else {
- addIndex = getFirstFreeIndex();
+ final int addIndex = findIndex(policy.getPolicyId(), ifIndex);
+ if (addIndex == -1) {
+ return DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
}
try {
- mPolicyIdToBpfMapIndex.put(policy.getPolicyId(), addIndex);
-
// Add v4 policy to mBpfDscpIpv4Policies if source and destination address
- // are both null or if they are both instances of Inet6Address.
+ // are both null or if they are both instances of Inet4Address.
if (matchesIpv4(policy)) {
mBpfDscpIpv4Policies.insertOrReplaceEntry(
new Struct.U32(addIndex),
new DscpPolicyValue(policy.getSourceAddress(),
- policy.getDestinationAddress(),
+ policy.getDestinationAddress(), ifIndex,
policy.getSourcePort(), policy.getDestinationPortRange(),
(short) policy.getProtocol(), (short) policy.getDscpValue()));
}
@@ -148,10 +194,16 @@
mBpfDscpIpv6Policies.insertOrReplaceEntry(
new Struct.U32(addIndex),
new DscpPolicyValue(policy.getSourceAddress(),
- policy.getDestinationAddress(),
+ policy.getDestinationAddress(), ifIndex,
policy.getSourcePort(), policy.getDestinationPortRange(),
(short) policy.getProtocol(), (short) policy.getDscpValue()));
}
+
+ ifacePolicies.put(policy.getPolicyId(), addIndex);
+ // Only add the policy to the per interface map if the policy was successfully
+ // added to both bpf maps above. It is safe to assume that if insert fails for
+ // one map then it fails for both.
+ mIfaceIndexToPolicyIdBpfMapIndex.put(ifIndex, ifacePolicies);
} catch (ErrnoException e) {
Log.e(TAG, "Failed to insert policy into map: ", e);
return DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
@@ -166,6 +218,7 @@
*
* DSCP_POLICY_STATUS_SUCCESS - if policy was added successfully
* DSCP_POLICY_STATUS_INSUFFICIENT_PROCESSING_RESOURCES - if max policies were already set
+ * DSCP_POLICY_STATUS_REQUEST_DECLINED - Interface index was invalid
*/
public void addDscpPolicy(NetworkAgentInfo nai, DscpPolicy policy) {
if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
@@ -177,11 +230,19 @@
}
}
- int status = addDscpPolicyInternal(policy);
+ final int ifIndex = getIfaceIndex(nai);
+ if (ifIndex == 0) {
+ Log.e(TAG, "Iface index is invalid");
+ sendStatus(nai, policy.getPolicyId(), DSCP_POLICY_STATUS_REQUEST_DECLINED);
+ return;
+ }
+
+ int status = addDscpPolicyInternal(policy, ifIndex);
sendStatus(nai, policy.getPolicyId(), status);
}
- private void removePolicyFromMap(NetworkAgentInfo nai, int policyId, int index) {
+ private void removePolicyFromMap(NetworkAgentInfo nai, int policyId, int index,
+ boolean sendCallback) {
int status = DSCP_POLICY_STATUS_POLICY_NOT_FOUND;
try {
mBpfDscpIpv4Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE);
@@ -191,7 +252,9 @@
Log.e(TAG, "Failed to delete policy from map: ", e);
}
- sendStatus(nai, policyId, status);
+ if (sendCallback) {
+ sendStatus(nai, policyId, status);
+ }
}
/**
@@ -204,36 +267,44 @@
return;
}
- if (mPolicyIdToBpfMapIndex.get(policyId, -1) != -1) {
- removePolicyFromMap(nai, policyId, mPolicyIdToBpfMapIndex.get(policyId));
- mPolicyIdToBpfMapIndex.delete(policyId);
+ SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(getIfaceIndex(nai));
+ if (ifacePolicies == null) return;
+
+ final int existingIndex = ifacePolicies.get(policyId, -1);
+ if (existingIndex == -1) {
+ Log.e(TAG, "Policy " + policyId + " does not exist in map.");
+ sendStatus(nai, policyId, DSCP_POLICY_STATUS_POLICY_NOT_FOUND);
+ return;
}
- // TODO: detach should only occur if no more policies are present on the nai's iface.
- if (mPolicyIdToBpfMapIndex.size() == 0) {
+ removePolicyFromMap(nai, policyId, existingIndex, true);
+ ifacePolicies.delete(policyId);
+
+ if (ifacePolicies.size() == 0) {
detachProgram(nai.linkProperties.getInterfaceName());
}
}
/**
- * Remove all DSCP policies and detach program.
+ * Remove all DSCP policies and detach program. Send callback if requested.
*/
- // TODO: Remove all should only remove policies from corresponding nai iface.
- public void removeAllDscpPolicies(NetworkAgentInfo nai) {
+ public void removeAllDscpPolicies(NetworkAgentInfo nai, boolean sendCallback) {
if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
// Nothing to remove since program is not attached. Send update for policy
// id 0. The status update must contain a policy ID, and 0 is an invalid id.
- sendStatus(nai, 0, DSCP_POLICY_STATUS_SUCCESS);
+ if (sendCallback) {
+ sendStatus(nai, 0, DSCP_POLICY_STATUS_SUCCESS);
+ }
return;
}
- for (int i = 0; i < mPolicyIdToBpfMapIndex.size(); i++) {
- removePolicyFromMap(nai, mPolicyIdToBpfMapIndex.keyAt(i),
- mPolicyIdToBpfMapIndex.valueAt(i));
+ SparseIntArray ifacePolicies = mIfaceIndexToPolicyIdBpfMapIndex.get(getIfaceIndex(nai));
+ if (ifacePolicies == null) return;
+ for (int i = 0; i < ifacePolicies.size(); i++) {
+ removePolicyFromMap(nai, ifacePolicies.keyAt(i), ifacePolicies.valueAt(i),
+ sendCallback);
}
- mPolicyIdToBpfMapIndex.clear();
-
- // Can detach program since no policies are active.
+ ifacePolicies.clear();
detachProgram(nai.linkProperties.getInterfaceName());
}
@@ -241,12 +312,12 @@
* Attach BPF program
*/
private boolean attachProgram(@NonNull String iface) {
- // TODO: attach needs to be per iface not program.
-
try {
NetworkInterface netIface = NetworkInterface.getByName(iface);
+ boolean isEth = TcUtils.isEthernet(iface);
+ String path = PROG_PATH + (isEth ? "_ether" : "_raw_ip");
TcUtils.tcFilterAddDevBpf(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL,
- PROG_PATH);
+ path);
} catch (IOException e) {
Log.e(TAG, "Unable to attach to TC on " + iface + ": " + e);
return false;
@@ -264,9 +335,9 @@
if (netIface != null) {
TcUtils.tcFilterDelDev(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL);
}
+ mAttachedIfaces.remove(iface);
} catch (IOException e) {
Log.e(TAG, "Unable to detach to TC on " + iface + ": " + e);
}
- mAttachedIfaces.remove(iface);
}
}
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java
index cb40306..6e4e7eb 100644
--- a/service/src/com/android/server/connectivity/DscpPolicyValue.java
+++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -31,29 +31,31 @@
public class DscpPolicyValue extends Struct {
private static final String TAG = DscpPolicyValue.class.getSimpleName();
- // TODO: add the interface index.
@Field(order = 0, type = Type.ByteArray, arraysize = 16)
public final byte[] src46;
@Field(order = 1, type = Type.ByteArray, arraysize = 16)
public final byte[] dst46;
- @Field(order = 2, type = Type.UBE16)
- public final int srcPort;
+ @Field(order = 2, type = Type.U32)
+ public final long ifIndex;
@Field(order = 3, type = Type.UBE16)
- public final int dstPortStart;
+ public final int srcPort;
@Field(order = 4, type = Type.UBE16)
+ public final int dstPortStart;
+
+ @Field(order = 5, type = Type.UBE16)
public final int dstPortEnd;
- @Field(order = 5, type = Type.U8)
+ @Field(order = 6, type = Type.U8)
public final short proto;
- @Field(order = 6, type = Type.U8)
+ @Field(order = 7, type = Type.U8)
public final short dscp;
- @Field(order = 7, type = Type.U8, padding = 3)
+ @Field(order = 8, type = Type.U8, padding = 3)
public final short mask;
private static final int SRC_IP_MASK = 0x1;
@@ -69,6 +71,7 @@
return true;
}
+ // TODO: move to frameworks/libs/net and have this and BpfCoordinator import it.
private byte[] toIpv4MappedAddressBytes(InetAddress ia) {
final byte[] addr6 = new byte[16];
if (ia != null) {
@@ -117,13 +120,12 @@
return mask;
}
- // This constructor is necessary for BpfMap#getValue since all values must be
- // in the constructor.
- public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
- final int dstPortStart, final int dstPortEnd, final short proto,
+ private DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final long ifIndex,
+ final int srcPort, final int dstPortStart, final int dstPortEnd, final short proto,
final short dscp) {
this.src46 = toAddressField(src46);
this.dst46 = toAddressField(dst46);
+ this.ifIndex = ifIndex;
// These params need to be stored as 0 because uints are used in BpfMap.
// If they are -1 BpfMap write will throw errors.
@@ -138,15 +140,15 @@
this.mask = makeMask(this.src46, this.dst46, srcPort, dstPortStart, proto, dscp);
}
- public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
- final Range<Integer> dstPort, final short proto,
+ public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final long ifIndex,
+ final int srcPort, final Range<Integer> dstPort, final short proto,
final short dscp) {
- this(src46, dst46, srcPort, dstPort != null ? dstPort.getLower() : -1,
+ this(src46, dst46, ifIndex, srcPort, dstPort != null ? dstPort.getLower() : -1,
dstPort != null ? dstPort.getUpper() : -1, proto, dscp);
}
public static final DscpPolicyValue NONE = new DscpPolicyValue(
- null /* src46 */, null /* dst46 */, -1 /* srcPort */,
+ null /* src46 */, null /* dst46 */, 0 /* ifIndex */, -1 /* srcPort */,
-1 /* dstPortStart */, -1 /* dstPortEnd */, (short) -1 /* proto */,
(short) 0 /* dscp */);
@@ -170,9 +172,9 @@
try {
return String.format(
- "src46: %s, dst46: %s, srcPort: %d, dstPortStart: %d, dstPortEnd: %d,"
- + " protocol: %d, dscp %s", srcIpString, dstIpString, srcPort, dstPortStart,
- dstPortEnd, proto, dscp);
+ "src46: %s, dst46: %s, ifIndex: %d, srcPort: %d, dstPortStart: %d,"
+ + " dstPortEnd: %d, protocol: %d, dscp %s", srcIpString, dstIpString,
+ ifIndex, srcPort, dstPortStart, dstPortEnd, proto, dscp);
} catch (IllegalArgumentException e) {
return String.format("String format error: " + e);
}
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index 7b06682..e4ad391 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -17,6 +17,7 @@
package com.android.server.connectivity;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static com.android.net.module.util.CollectionUtils.contains;
@@ -36,9 +37,12 @@
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.NetworkStackConstants;
import com.android.server.ConnectivityService;
+import java.io.IOException;
import java.net.Inet6Address;
import java.util.Objects;
@@ -96,6 +100,7 @@
private String mIface;
private Inet6Address mIPv6Address;
private State mState = State.IDLE;
+ private ClatCoordinator mClatCoordinator;
private boolean mEnableClatOnCellular;
private boolean mPrefixDiscoveryRunning;
@@ -106,6 +111,7 @@
mNetd = netd;
mNetwork = nai;
mEnableClatOnCellular = deps.getCellular464XlatEnabled();
+ mClatCoordinator = deps.getClatCoordinator(mNetd);
}
/**
@@ -122,6 +128,11 @@
final boolean supported = contains(NETWORK_TYPES, nai.networkInfo.getType());
final boolean connected = contains(NETWORK_STATES, nai.networkInfo.getState());
+ // Allow to run clat on test network.
+ // TODO: merge to boolean "supported" once boolean "supported" is migrated to
+ // NetworkCapabilities.TRANSPORT_*.
+ final boolean isTestNetwork = nai.networkCapabilities.hasTransport(TRANSPORT_TEST);
+
// Only run clat on networks that have a global IPv6 address and don't have a native IPv4
// address.
LinkProperties lp = nai.linkProperties;
@@ -132,8 +143,8 @@
final boolean skip464xlat = (nai.netAgentConfig() != null)
&& nai.netAgentConfig().skip464xlat;
- return supported && connected && isIpv6OnlyNetwork && !skip464xlat && !nai.destroyed
- && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
+ return (supported || isTestNetwork) && connected && isIpv6OnlyNetwork && !skip464xlat
+ && !nai.destroyed && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
? isCellular464XlatEnabled() : true);
}
@@ -179,10 +190,18 @@
private void enterStartingState(String baseIface) {
mNat64PrefixInUse = selectNat64Prefix();
String addrStr = null;
- try {
- addrStr = mNetd.clatdStart(baseIface, mNat64PrefixInUse.toString());
- } catch (RemoteException | ServiceSpecificException e) {
- Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e);
+ if (SdkLevel.isAtLeastT()) {
+ try {
+ addrStr = mClatCoordinator.clatStart(baseIface, getNetId(), mNat64PrefixInUse);
+ } catch (IOException e) {
+ Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e);
+ }
+ } else {
+ try {
+ addrStr = mNetd.clatdStart(baseIface, mNat64PrefixInUse.toString());
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e);
+ }
}
mIface = CLAT_PREFIX + baseIface;
mBaseIface = baseIface;
@@ -256,10 +275,18 @@
}
Log.i(TAG, "Stopping clatd on " + mBaseIface);
- try {
- mNetd.clatdStop(mBaseIface);
- } catch (RemoteException | ServiceSpecificException e) {
- Log.e(TAG, "Error stopping clatd on " + mBaseIface + ": " + e);
+ if (SdkLevel.isAtLeastT()) {
+ try {
+ mClatCoordinator.clatStop();
+ } catch (IOException e) {
+ Log.e(TAG, "Error stopping clatd on " + mBaseIface + ": " + e);
+ }
+ } else {
+ try {
+ mNetd.clatdStop(mBaseIface);
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Error stopping clatd on " + mBaseIface + ": " + e);
+ }
}
String iface = mIface;
@@ -506,6 +533,24 @@
mNetwork.handler().post(() -> handleInterfaceRemoved(iface));
}
+ /**
+ * Dump the NAT64 xlat information.
+ *
+ * @param pw print writer.
+ */
+ public void dump(IndentingPrintWriter pw) {
+ if (SdkLevel.isAtLeastT()) {
+ if (isStarted()) {
+ pw.println("ClatCoordinator:");
+ pw.increaseIndent();
+ mClatCoordinator.dump(pw);
+ pw.decreaseIndent();
+ } else {
+ pw.println("<not started>");
+ }
+ }
+ }
+
@Override
public String toString() {
return "mBaseIface: " + mBaseIface + ", mIface: " + mIface + ", mState: " + mState;
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 1fc5a8f..b40b6e0 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -19,6 +19,7 @@
import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.transportNamesOf;
@@ -59,6 +60,7 @@
import android.util.Pair;
import android.util.SparseArray;
+import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.WakeupMessage;
import com.android.modules.utils.build.SdkLevel;
import com.android.server.ConnectivityService;
@@ -1186,6 +1188,15 @@
}
/**
+ * Dump the NAT64 xlat information.
+ *
+ * @param pw print writer.
+ */
+ public void dumpNat464Xlat(IndentingPrintWriter pw) {
+ clatd.dump(pw);
+ }
+
+ /**
* Sets the most recent ConnectivityReport for this network.
*
* <p>This should only be called from the ConnectivityService thread.
@@ -1214,20 +1225,22 @@
*
* @param nc the capabilities to sanitize
* @param creatorUid the UID of the process creating this network agent
+ * @param hasAutomotiveFeature true if this device has the automotive feature, false otherwise
* @param authenticator the carrier privilege authenticator to check for telephony constraints
*/
public static void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
- final int creatorUid, @NonNull final CarrierPrivilegeAuthenticator authenticator) {
+ final int creatorUid, final boolean hasAutomotiveFeature,
+ @Nullable final CarrierPrivilegeAuthenticator authenticator) {
if (nc.hasTransport(TRANSPORT_TEST)) {
nc.restrictCapabilitiesForTestNetwork(creatorUid);
}
- if (!areAllowedUidsAcceptableFromNetworkAgent(nc, authenticator)) {
+ if (!areAllowedUidsAcceptableFromNetworkAgent(nc, hasAutomotiveFeature, authenticator)) {
nc.setAllowedUids(new ArraySet<>());
}
}
private static boolean areAllowedUidsAcceptableFromNetworkAgent(
- @NonNull final NetworkCapabilities nc,
+ @NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
@Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) {
// NCs without access UIDs are fine.
if (!nc.hasAllowedUids()) return true;
@@ -1242,6 +1255,11 @@
// access UIDs
if (nc.hasTransport(TRANSPORT_TEST)) return true;
+ // Factories that make ethernet networks can allow UIDs for automotive devices.
+ if (nc.hasSingleTransport(TRANSPORT_ETHERNET) && hasAutomotiveFeature) {
+ return true;
+ }
+
// Factories that make cell networks can allow the UID for the carrier service package.
// This can only work in T where there is support for CarrierPrivilegeAuthenticator
if (null != carrierPrivilegeAuthenticator
@@ -1252,8 +1270,6 @@
return true;
}
- // TODO : accept Railway callers
-
return false;
}
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
index 1e975dd..eea382e 100644
--- a/service/src/com/android/server/connectivity/NetworkOffer.java
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -22,6 +22,7 @@
import android.net.NetworkRequest;
import android.os.RemoteException;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
@@ -143,6 +144,11 @@
@Override
public String toString() {
- return "NetworkOffer [ Score " + score + " Caps " + caps + "]";
+ final ArrayList<Integer> neededRequestIds = new ArrayList<>();
+ for (final NetworkRequest request : mCurrentlyNeeded) {
+ neededRequestIds.add(request.requestId);
+ }
+ return "NetworkOffer [ Provider Id (" + providerId + ") " + score + " Caps "
+ + caps + " Needed by " + neededRequestIds + "]";
}
}
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index 62b3add..fd1ed60 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -37,6 +37,7 @@
import static com.android.net.module.util.CollectionUtils.toIntArray;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -50,7 +51,6 @@
import android.net.INetd;
import android.net.UidRange;
import android.net.Uri;
-import android.net.util.SharedLog;
import android.os.Build;
import android.os.Process;
import android.os.RemoteException;
@@ -69,12 +69,12 @@
import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.ProcessShimImpl;
import com.android.networkstack.apishim.common.ProcessShim;
import com.android.server.BpfNetMaps;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -108,10 +108,19 @@
@GuardedBy("this")
private final SparseIntArray mUidToNetworkPerm = new SparseIntArray();
- // Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges
- // for apps under the VPN
+ // NonNull keys are active non-bypassable and fully-routed VPN's interface name, Values are uid
+ // ranges for apps under the VPNs which enable interface filtering.
+ // If key is null, Values are uid ranges for apps under the VPNs which are connected but do not
+ // enable interface filtering.
@GuardedBy("this")
- private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>();
+ private final Map<String, Set<UidRange>> mVpnInterfaceUidRanges = new ArrayMap<>();
+
+ // Items are uid ranges for apps under the VPN Lockdown
+ // Ranges were given through ConnectivityManager#setRequireVpnForUids, and ranges are allowed to
+ // have duplicates. Also, it is allowed to give ranges that are already subject to lockdown.
+ // So we need to maintain uid range with multiset.
+ @GuardedBy("this")
+ private final MultiSet<UidRange> mVpnLockdownUidRanges = new MultiSet<>();
// A set of appIds for apps across all users on the device. We track appIds instead of uids
// directly to reduce its size and also eliminate the need to update this set when user is
@@ -134,7 +143,9 @@
// Store appIds traffic permissions for each user.
// Keys are users, Values are SparseArrays where each entry maps an appId to the permissions
- // that appId has within that user.
+ // that appId has within that user. The permissions are a bitmask of PERMISSION_INTERNET and
+ // PERMISSION_UPDATE_DEVICE_STATS, or 0 (PERMISSION_NONE) if the app has neither of those
+ // permissions. They can never be PERMISSION_UNINSTALLED.
@GuardedBy("this")
private final Map<UserHandle, SparseIntArray> mUsersTrafficPermissions = new ArrayMap<>();
@@ -199,6 +210,38 @@
}
}
+ private static class MultiSet<T> {
+ private final Map<T, Integer> mMap = new ArrayMap<>();
+
+ /**
+ * Returns the number of key in the set before this addition.
+ */
+ public int add(T key) {
+ final int oldCount = mMap.getOrDefault(key, 0);
+ mMap.put(key, oldCount + 1);
+ return oldCount;
+ }
+
+ /**
+ * Return the number of key in the set before this removal.
+ */
+ public int remove(T key) {
+ final int oldCount = mMap.getOrDefault(key, 0);
+ if (oldCount == 0) {
+ Log.wtf(TAG, "Attempt to remove non existing key = " + key.toString());
+ } else if (oldCount == 1) {
+ mMap.remove(key);
+ } else {
+ mMap.put(key, oldCount - 1);
+ }
+ return oldCount;
+ }
+
+ public Set<T> getSet() {
+ return mMap.keySet();
+ }
+ }
+
public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
@NonNull final BpfNetMaps bpfNetMaps) {
this(context, netd, bpfNetMaps, new Dependencies());
@@ -419,7 +462,14 @@
if (appInfo == null) return false;
// Check whether package's uid is in allowed on restricted networks uid list. If so, this
// uid can have netd system permission.
- return mUidsAllowedOnRestrictedNetworks.contains(appInfo.uid);
+ return isUidAllowedOnRestrictedNetworks(appInfo.uid);
+ }
+
+ /**
+ * Returns whether the given uid is in allowed on restricted networks list.
+ */
+ public synchronized boolean isUidAllowedOnRestrictedNetworks(final int uid) {
+ return mUidsAllowedOnRestrictedNetworks.contains(uid);
}
@VisibleForTesting
@@ -545,17 +595,21 @@
// Remove appIds traffic permission that belongs to the user
final SparseIntArray removedUserAppIds = mUsersTrafficPermissions.remove(user);
- // Generate appIds from left users.
+ // Generate appIds from the remaining users.
final SparseIntArray appIds = makeAppIdsTrafficPermForAllUsers();
+
+ if (removedUserAppIds == null) {
+ Log.wtf(TAG, "onUserRemoved: Receive unknown user=" + user);
+ return;
+ }
+
// Clear permission on those appIds belong to this user only, set the permission to
// PERMISSION_UNINSTALLED.
- if (removedUserAppIds != null) {
- for (int i = 0; i < removedUserAppIds.size(); i++) {
- final int appId = removedUserAppIds.keyAt(i);
- // Need to clear permission if the removed appId is not found in the array.
- if (appIds.indexOfKey(appId) < 0) {
- appIds.put(appId, PERMISSION_UNINSTALLED);
- }
+ for (int i = 0; i < removedUserAppIds.size(); i++) {
+ final int appId = removedUserAppIds.keyAt(i);
+ // Need to clear permission if the removed appId is not found in the array.
+ if (appIds.indexOfKey(appId) < 0) {
+ appIds.put(appId, PERMISSION_UNINSTALLED);
}
}
sendAppIdsTrafficPermission(appIds);
@@ -613,16 +667,30 @@
}
private synchronized void updateVpnUid(int uid, boolean add) {
- for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+ // Apps that can use restricted networks can always bypass VPNs.
+ if (hasRestrictedNetworksPermission(uid)) {
+ return;
+ }
+ for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
if (UidRange.containsUid(vpn.getValue(), uid)) {
final Set<Integer> changedUids = new HashSet<>();
changedUids.add(uid);
- removeBypassingUids(changedUids, -1 /* vpnAppUid */);
updateVpnUidsInterfaceRules(vpn.getKey(), changedUids, add);
}
}
}
+ private synchronized void updateLockdownUid(int uid, boolean add) {
+ // Apps that can use restricted networks can always bypass VPNs.
+ if (hasRestrictedNetworksPermission(uid)) {
+ return;
+ }
+
+ if (UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid)) {
+ updateLockdownUidRule(uid, add);
+ }
+ }
+
/**
* This handles both network and traffic permission, because there is no overlap in actual
* values, where network permission is NETWORK or SYSTEM, and traffic permission is INTERNET
@@ -650,7 +718,6 @@
}
private synchronized void updateAppIdTrafficPermission(int uid) {
- final int appId = UserHandle.getAppId(uid);
final int uidTrafficPerm = getTrafficPermissionForUid(uid);
final SparseIntArray userTrafficPerms =
mUsersTrafficPermissions.get(UserHandle.getUserHandleForUid(uid));
@@ -661,6 +728,7 @@
// Do not put PERMISSION_UNINSTALLED into the array. If no package left on the uid
// (PERMISSION_UNINSTALLED), remove the appId from the array. Otherwise, update the latest
// permission to the appId.
+ final int appId = UserHandle.getAppId(uid);
if (uidTrafficPerm == PERMISSION_UNINSTALLED) {
userTrafficPerms.delete(appId);
} else {
@@ -716,13 +784,14 @@
// If the newly-installed package falls within some VPN's uid range, update Netd with it.
// This needs to happen after the mUidToNetworkPerm update above, since
- // removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
- // package can bypass VPN.
+ // hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
+ // mUidToNetworkPerm to check if the package can bypass VPN.
updateVpnUid(uid, true /* add */);
+ updateLockdownUid(uid, true /* add */);
mAllApps.add(appId);
// Log package added.
- mPermissionUpdateLogs.log("Package add: name=" + packageName + ", uid=" + uid
+ mPermissionUpdateLogs.log("Package add: uid=" + uid
+ ", nPerm=(" + permissionToString(permission) + "/"
+ permissionToString(currentPermission) + ")"
+ ", tPerm=" + permissionToString(appIdTrafficPerm));
@@ -762,9 +831,10 @@
// If the newly-removed package falls within some VPN's uid range, update Netd with it.
// This needs to happen before the mUidToNetworkPerm update below, since
- // removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
- // package can bypass VPN.
+ // hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
+ // mUidToNetworkPerm to check if the package can bypass VPN.
updateVpnUid(uid, false /* add */);
+ updateLockdownUid(uid, false /* add */);
// If the package has been removed from all users on the device, clear it form mAllApps.
if (mPackageManager.getNameForUid(uid) == null) {
mAllApps.remove(appId);
@@ -774,7 +844,7 @@
final int permission = highestUidNetworkPermission(uid);
// Log package removed.
- mPermissionUpdateLogs.log("Package remove: name=" + packageName + ", uid=" + uid
+ mPermissionUpdateLogs.log("Package remove: uid=" + uid
+ ", nPerm=(" + permissionToString(permission) + "/"
+ permissionToString(currentPermission) + ")"
+ ", tPerm=" + permissionToString(appIdTrafficPerm));
@@ -846,48 +916,100 @@
/**
* Called when a new set of UID ranges are added to an active VPN network
*
- * @param iface The active VPN network's interface name
+ * @param iface The active VPN network's interface name. Null iface indicates that the app is
+ * allowed to receive packets on all interfaces.
* @param rangesToAdd The new UID ranges to be added to the network
* @param vpnAppUid The uid of the VPN app
*/
- public synchronized void onVpnUidRangesAdded(@NonNull String iface, Set<UidRange> rangesToAdd,
+ public synchronized void onVpnUidRangesAdded(@Nullable String iface, Set<UidRange> rangesToAdd,
int vpnAppUid) {
// Calculate the list of new app uids under the VPN due to the new UID ranges and update
// Netd about them. Because mAllApps only contains appIds instead of uids, the result might
// be an overestimation if an app is not installed on the user on which the VPN is running,
- // but that's safe.
+ // but that's safe: if an app is not installed, it cannot receive any packets, so dropping
+ // packets to that UID is fine.
final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps);
removeBypassingUids(changedUids, vpnAppUid);
updateVpnUidsInterfaceRules(iface, changedUids, true /* add */);
- if (mVpnUidRanges.containsKey(iface)) {
- mVpnUidRanges.get(iface).addAll(rangesToAdd);
+ if (mVpnInterfaceUidRanges.containsKey(iface)) {
+ mVpnInterfaceUidRanges.get(iface).addAll(rangesToAdd);
} else {
- mVpnUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
+ mVpnInterfaceUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
}
}
/**
* Called when a set of UID ranges are removed from an active VPN network
*
- * @param iface The VPN network's interface name
+ * @param iface The VPN network's interface name. Null iface indicates that the app is allowed
+ * to receive packets on all interfaces.
* @param rangesToRemove Existing UID ranges to be removed from the VPN network
* @param vpnAppUid The uid of the VPN app
*/
- public synchronized void onVpnUidRangesRemoved(@NonNull String iface,
+ public synchronized void onVpnUidRangesRemoved(@Nullable String iface,
Set<UidRange> rangesToRemove, int vpnAppUid) {
// Calculate the list of app uids that are no longer under the VPN due to the removed UID
// ranges and update Netd about them.
final Set<Integer> changedUids = intersectUids(rangesToRemove, mAllApps);
removeBypassingUids(changedUids, vpnAppUid);
updateVpnUidsInterfaceRules(iface, changedUids, false /* add */);
- Set<UidRange> existingRanges = mVpnUidRanges.getOrDefault(iface, null);
+ Set<UidRange> existingRanges = mVpnInterfaceUidRanges.getOrDefault(iface, null);
if (existingRanges == null) {
loge("Attempt to remove unknown vpn uid Range iface = " + iface);
return;
}
existingRanges.removeAll(rangesToRemove);
if (existingRanges.size() == 0) {
- mVpnUidRanges.remove(iface);
+ mVpnInterfaceUidRanges.remove(iface);
+ }
+ }
+
+ /**
+ * Called when UID ranges under VPN Lockdown are updated
+ *
+ * @param add {@code true} if the uids are to be added to the Lockdown, {@code false} if they
+ * are to be removed from the Lockdown.
+ * @param ranges The updated UID ranges under VPN Lockdown. This function does not treat the VPN
+ * app's UID in any special way. The caller is responsible for excluding the VPN
+ * app UID from the passed-in ranges.
+ * Ranges can have duplications and/or contain the range that is already subject
+ * to lockdown. However, ranges can not have overlaps with other ranges including
+ * ranges that are currently subject to lockdown.
+ */
+ public synchronized void updateVpnLockdownUidRanges(boolean add, UidRange[] ranges) {
+ final Set<UidRange> affectedUidRanges = new HashSet<>();
+
+ for (final UidRange range : ranges) {
+ if (add) {
+ // Rule will be added if mVpnLockdownUidRanges does not have this uid range entry
+ // currently.
+ if (mVpnLockdownUidRanges.add(range) == 0) {
+ affectedUidRanges.add(range);
+ }
+ } else {
+ // Rule will be removed if the number of the range in the set is 1 before the
+ // removal.
+ if (mVpnLockdownUidRanges.remove(range) == 1) {
+ affectedUidRanges.add(range);
+ }
+ }
+ }
+
+ // mAllApps only contains appIds instead of uids. So the generated uid list might contain
+ // apps that are installed only on some users but not others. But that's safe: if an app is
+ // not installed, it cannot receive any packets, so dropping packets to that UID is fine.
+ final Set<Integer> affectedUids = intersectUids(affectedUidRanges, mAllApps);
+
+ // We skip adding rule to privileged apps and allow them to bypass incoming packet
+ // filtering. The behaviour is consistent with how lockdown works for outgoing packets, but
+ // the implementation is different: while ConnectivityService#setRequireVpnForUids does not
+ // exclude privileged apps from the prohibit routing rules used to implement outgoing packet
+ // filtering, privileged apps can still bypass outgoing packet filtering because the
+ // prohibit rules observe the protected from VPN bit.
+ for (final int uid: affectedUids) {
+ if (!hasRestrictedNetworksPermission(uid)) {
+ updateLockdownUidRule(uid, add);
+ }
}
}
@@ -926,7 +1048,7 @@
*/
private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
uids.remove(vpnAppUid);
- uids.removeIf(uid -> mUidToNetworkPerm.get(uid, PERMISSION_NONE) == PERMISSION_SYSTEM);
+ uids.removeIf(this::hasRestrictedNetworksPermission);
}
/**
@@ -935,6 +1057,7 @@
*
* This is to instruct netd to set up appropriate filtering rules for these uids, such that they
* can only receive ingress packets from the VPN's tunnel interface (and loopback).
+ * Null iface set up a wildcard rule that allow app to receive packets on all interfaces.
*
* @param iface the interface name of the active VPN connection
* @param add {@code true} if the uids are to be added to the interface, {@code false} if they
@@ -955,6 +1078,14 @@
}
}
+ private void updateLockdownUidRule(int uid, boolean add) {
+ try {
+ mBpfNetMaps.updateUidLockdownRule(uid, add);
+ } catch (ServiceSpecificException e) {
+ loge("Failed to " + (add ? "add" : "remove") + " Lockdown rule: " + e);
+ }
+ }
+
/**
* Send the updated permission information to netd. Called upon package install/uninstall.
*
@@ -984,10 +1115,6 @@
*/
@VisibleForTesting
void sendAppIdsTrafficPermission(SparseIntArray netdPermissionsAppIds) {
- if (mNetd == null) {
- Log.e(TAG, "Failed to get the netd service");
- return;
- }
final ArrayList<Integer> allPermissionAppIds = new ArrayList<>();
final ArrayList<Integer> internetPermissionAppIds = new ArrayList<>();
final ArrayList<Integer> updateStatsPermissionAppIds = new ArrayList<>();
@@ -1046,8 +1173,14 @@
/** Should only be used by unit tests */
@VisibleForTesting
- public Set<UidRange> getVpnUidRanges(String iface) {
- return mVpnUidRanges.get(iface);
+ public Set<UidRange> getVpnInterfaceUidRanges(String iface) {
+ return mVpnInterfaceUidRanges.get(iface);
+ }
+
+ /** Should only be used by unit tests */
+ @VisibleForTesting
+ public Set<UidRange> getVpnLockdownUidRanges() {
+ return mVpnLockdownUidRanges.getSet();
}
private synchronized void onSettingChanged() {
@@ -1112,7 +1245,7 @@
public void dump(IndentingPrintWriter pw) {
pw.println("Interface filtering rules:");
pw.increaseIndent();
- for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+ for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
pw.println("Interface: " + vpn.getKey());
pw.println("UIDs: " + vpn.getValue().toString());
pw.println();
@@ -1120,6 +1253,14 @@
pw.decreaseIndent();
pw.println();
+ pw.println("Lockdown filtering rules:");
+ pw.increaseIndent();
+ for (final UidRange range : mVpnLockdownUidRanges.getSet()) {
+ pw.println("UIDs: " + range);
+ }
+ pw.decreaseIndent();
+
+ pw.println();
pw.println("Update logs:");
pw.increaseIndent();
mPermissionUpdateLogs.reverseDump(pw);
diff --git a/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java b/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
index 71f342d..5bafef9 100644
--- a/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
+++ b/service/src/com/android/server/connectivity/ProfileNetworkPreferenceList.java
@@ -70,23 +70,33 @@
/**
* Returns a new object consisting of this object plus the passed preference.
*
- * If a preference already exists for the same user, it will be replaced by the passed
- * preference. Passing a Preference object containing a null capabilities object is equivalent
- * to (and indeed, implemented as) removing the preference for this user.
+ * It is not expected that unwanted preference already exists for the same user.
+ * All preferences for the user that were previously configured should be cleared before
+ * adding a new preference.
+ * Passing a Preference object containing a null capabilities object is equivalent
+ * to removing the preference for this user.
*/
public ProfileNetworkPreferenceList plus(@NonNull final Preference pref) {
- final ArrayList<Preference> newPrefs = new ArrayList<>();
- for (final Preference existingPref : preferences) {
- if (!existingPref.user.equals(pref.user)) {
- newPrefs.add(existingPref);
- }
- }
+ final ArrayList<Preference> newPrefs = new ArrayList<>(preferences);
if (null != pref.capabilities) {
newPrefs.add(pref);
}
return new ProfileNetworkPreferenceList(newPrefs);
}
+ /**
+ * Remove all preferences corresponding to a user.
+ */
+ public ProfileNetworkPreferenceList withoutUser(UserHandle user) {
+ final ArrayList<Preference> newPrefs = new ArrayList<>();
+ for (final Preference existingPref : preferences) {
+ if (!existingPref.user.equals(user)) {
+ newPrefs.add(existingPref);
+ }
+ }
+ return new ProfileNetworkPreferenceList(newPrefs);
+ }
+
public boolean isEmpty() {
return preferences.isEmpty();
}
diff --git a/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
index 534dbe7..e682026 100644
--- a/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
+++ b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
@@ -30,6 +30,8 @@
import android.telephony.data.NrQosSessionAttributes;
import android.util.Log;
+import com.android.modules.utils.build.SdkLevel;
+
import java.util.Objects;
/**
@@ -149,6 +151,7 @@
void sendEventEpsQosSessionAvailable(final QosSession session,
final EpsBearerQosSessionAttributes attributes) {
+ if (!validateOrSendErrorAndUnregister()) return;
try {
if (DBG) log("sendEventEpsQosSessionAvailable: sending...");
mCallback.onQosEpsBearerSessionAvailable(session, attributes);
@@ -159,6 +162,7 @@
void sendEventNrQosSessionAvailable(final QosSession session,
final NrQosSessionAttributes attributes) {
+ if (!validateOrSendErrorAndUnregister()) return;
try {
if (DBG) log("sendEventNrQosSessionAvailable: sending...");
mCallback.onNrQosSessionAvailable(session, attributes);
@@ -168,6 +172,7 @@
}
void sendEventQosSessionLost(@NonNull final QosSession session) {
+ if (!validateOrSendErrorAndUnregister()) return;
try {
if (DBG) log("sendEventQosSessionLost: sending...");
mCallback.onQosSessionLost(session);
@@ -185,6 +190,21 @@
}
}
+ private boolean validateOrSendErrorAndUnregister() {
+ final int exceptionType = mFilter.validate();
+ if (exceptionType != EX_TYPE_FILTER_NONE) {
+ log("validation fail before sending QosCallback.");
+ // Error callback is returned from Android T to prevent any disruption of application
+ // running on Android S.
+ if (SdkLevel.isAtLeastT()) {
+ sendEventQosCallbackError(exceptionType);
+ mQosCallbackTracker.unregisterCallback(mCallback);
+ }
+ return false;
+ }
+ return true;
+ }
+
private static void log(@NonNull final String msg) {
Log.d(TAG, msg);
}
diff --git a/service/src/com/android/server/net/DelayedDiskWrite.java b/service/src/com/android/server/net/DelayedDiskWrite.java
index 35dc455..41cb419 100644
--- a/service/src/com/android/server/net/DelayedDiskWrite.java
+++ b/service/src/com/android/server/net/DelayedDiskWrite.java
@@ -21,6 +21,8 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
@@ -32,11 +34,42 @@
public class DelayedDiskWrite {
private static final String TAG = "DelayedDiskWrite";
+ private final Dependencies mDeps;
+
private HandlerThread mDiskWriteHandlerThread;
private Handler mDiskWriteHandler;
/* Tracks multiple writes on the same thread */
private int mWriteSequence = 0;
+ public DelayedDiskWrite() {
+ this(new Dependencies());
+ }
+
+ @VisibleForTesting
+ DelayedDiskWrite(Dependencies deps) {
+ mDeps = deps;
+ }
+
+ /**
+ * Dependencies class of DelayedDiskWrite, used for injection in tests.
+ */
+ @VisibleForTesting
+ static class Dependencies {
+ /**
+ * Create a HandlerThread to use in DelayedDiskWrite.
+ */
+ public HandlerThread makeHandlerThread() {
+ return new HandlerThread("DelayedDiskWriteThread");
+ }
+
+ /**
+ * Quit the HandlerThread looper.
+ */
+ public void quitHandlerThread(HandlerThread handlerThread) {
+ handlerThread.getLooper().quit();
+ }
+ }
+
/**
* Used to do a delayed data write to a given {@link OutputStream}.
*/
@@ -65,7 +98,7 @@
/* Do a delayed write to disk on a separate handler thread */
synchronized (this) {
if (++mWriteSequence == 1) {
- mDiskWriteHandlerThread = new HandlerThread("DelayedDiskWriteThread");
+ mDiskWriteHandlerThread = mDeps.makeHandlerThread();
mDiskWriteHandlerThread.start();
mDiskWriteHandler = new Handler(mDiskWriteHandlerThread.getLooper());
}
@@ -99,9 +132,9 @@
// Quit if no more writes sent
synchronized (this) {
if (--mWriteSequence == 0) {
- mDiskWriteHandler.getLooper().quit();
- mDiskWriteHandler = null;
+ mDeps.quitHandlerThread(mDiskWriteHandlerThread);
mDiskWriteHandlerThread = null;
+ mDiskWriteHandler = null;
}
}
}
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 509e881..58731e0 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -140,6 +140,30 @@
],
}
+// defaults for tests that need to build against framework-connectivity's @hide APIs, but also
+// using fully @hide classes that are jarjared (because they have no API member). Similar to
+// framework-connectivity-test-defaults above but uses pre-jarjar class names.
+// Only usable from targets that have visibility on framework-connectivity-pre-jarjar, and apply
+// connectivity jarjar rules so that references to jarjared classes still match: this is limited to
+// connectivity internal tests only.
+java_defaults {
+ name: "framework-connectivity-internal-test-defaults",
+ sdk_version: "core_platform", // tests can use @CorePlatformApi's
+ libs: [
+ // order matters: classes in framework-connectivity are resolved before framework,
+ // meaning @hide APIs in framework-connectivity are resolved before @SystemApi
+ // stubs in framework
+ "framework-connectivity-pre-jarjar",
+ "framework-connectivity-t-pre-jarjar",
+ "framework-tethering.impl",
+ "framework",
+
+ // if sdk_version="" this gets automatically included, but here we need to add manually.
+ "framework-res",
+ ],
+ defaults_visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
+}
+
// Defaults for tests that want to run in mainline-presubmit.
// Not widely used because many of our tests have AndroidTest.xml files and
// use the mainline-param config-descriptor metadata in AndroidTest.xml.
diff --git a/tests/common/AndroidTest_Coverage.xml b/tests/common/AndroidTest_Coverage.xml
index d4898b2..48d26b8 100644
--- a/tests/common/AndroidTest_Coverage.xml
+++ b/tests/common/AndroidTest_Coverage.xml
@@ -14,7 +14,8 @@
-->
<configuration description="Runs coverage tests for Connectivity">
<target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
- <option name="test-file-name" value="ConnectivityCoverageTests.apk" />
+ <option name="test-file-name" value="ConnectivityCoverageTests.apk" />
+ <option name="install-arg" value="-t" />
</target_preparer>
<option name="test-tag" value="ConnectivityCoverageTests" />
diff --git a/tests/common/java/android/net/EthernetNetworkManagementExceptionTest.java b/tests/common/java/android/net/EthernetNetworkManagementExceptionTest.java
new file mode 100644
index 0000000..84b6e54
--- /dev/null
+++ b/tests/common/java/android/net/EthernetNetworkManagementExceptionTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+public class EthernetNetworkManagementExceptionTest {
+ private static final String ERROR_MESSAGE = "Test error message";
+
+ @Test
+ public void testEthernetNetworkManagementExceptionParcelable() {
+ final EthernetNetworkManagementException e =
+ new EthernetNetworkManagementException(ERROR_MESSAGE);
+
+ assertParcelingIsLossless(e);
+ }
+
+ @Test
+ public void testEthernetNetworkManagementExceptionHasExpectedErrorMessage() {
+ final EthernetNetworkManagementException e =
+ new EthernetNetworkManagementException(ERROR_MESSAGE);
+
+ assertEquals(ERROR_MESSAGE, e.getMessage());
+ }
+}
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index 8fc636a..9ed2bb3 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -20,7 +20,6 @@
import static android.net.RouteInfo.RTN_UNICAST;
import static android.net.RouteInfo.RTN_UNREACHABLE;
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
import static com.android.testutils.ParcelUtils.parcelingRoundTrip;
@@ -46,12 +45,14 @@
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk31;
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
import java.net.Inet4Address;
@@ -67,11 +68,13 @@
@SmallTest
@ConnectivityModuleTest
public class LinkPropertiesTest {
+ // Use a RuleChain to explicitly specify the order of rules. DevSdkIgnoreRule must run before
+ // PlatformCompatChange rule, because otherwise tests with that should be skipped when targeting
+ // target SDK 33 will still attempt to override compat changes (which on user builds will crash)
+ // before being skipped.
@Rule
- public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
- @Rule
- public final PlatformCompatChangeRule compatChangeRule = new PlatformCompatChangeRule();
+ public final RuleChain chain = RuleChain.outerRule(
+ new DevSdkIgnoreRule()).around(new PlatformCompatChangeRule());
private static final InetAddress ADDRV4 = address("75.208.6.1");
private static final InetAddress ADDRV6 = address("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
@@ -1261,7 +1264,21 @@
assertFalse(lp.hasIpv4UnreachableDefaultRoute());
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
+ @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
+ public void testHasExcludeRoute() {
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName("tun0");
+ lp.addRoute(new RouteInfo(new IpPrefix(ADDRV4, 24), RTN_UNICAST));
+ lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 0), RTN_UNICAST));
+ assertFalse(lp.hasExcludeRoute());
+ lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 32), RTN_THROW));
+ assertTrue(lp.hasExcludeRoute());
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
@EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
public void testRouteAddWithSameKey() throws Exception {
LinkProperties lp = new LinkProperties();
@@ -1278,7 +1295,8 @@
assertEquals(2, lp.getRoutes().size());
}
- @Test @IgnoreUpTo(SC_V2)
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
@EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
public void testExcludedRoutesEnabled() {
final LinkProperties lp = new LinkProperties();
@@ -1294,7 +1312,8 @@
assertEquals(3, lp.getRoutes().size());
}
- @Test @IgnoreUpTo(SC_V2)
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
@DisableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
public void testExcludedRoutesDisabled() {
final LinkProperties lp = new LinkProperties();
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 9ae5fab..c30e1d3 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -82,12 +82,11 @@
import android.util.ArraySet;
import android.util.Range;
-import androidx.test.runner.AndroidJUnit4;
-
import com.android.testutils.CompatUtil;
import com.android.testutils.ConnectivityModuleTest;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Rule;
import org.junit.Test;
@@ -99,8 +98,12 @@
import java.util.List;
import java.util.Set;
-@RunWith(AndroidJUnit4.class)
@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+// NetworkCapabilities is only updatable on S+, and this test covers behavior which implementation
+// is self-contained within NetworkCapabilities.java, so it does not need to be run on, or
+// compatible with, earlier releases.
+@IgnoreUpTo(Build.VERSION_CODES.R)
@ConnectivityModuleTest
public class NetworkCapabilitiesTest {
private static final String TEST_SSID = "TEST_SSID";
@@ -489,7 +492,7 @@
assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
public void testOemPrivate() {
NetworkCapabilities nc = new NetworkCapabilities();
// By default OEM_PRIVATE is neither in the required or forbidden lists and the network is
@@ -516,7 +519,7 @@
assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities()));
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
public void testForbiddenCapabilities() {
NetworkCapabilities network = new NetworkCapabilities();
@@ -630,7 +633,7 @@
return new Range<Integer>(from, to);
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testSetAdministratorUids() {
NetworkCapabilities nc =
new NetworkCapabilities().setAdministratorUids(new int[] {2, 1, 3});
@@ -638,7 +641,7 @@
assertArrayEquals(new int[] {1, 2, 3}, nc.getAdministratorUids());
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testSetAdministratorUidsWithDuplicates() {
try {
new NetworkCapabilities().setAdministratorUids(new int[] {1, 1});
@@ -750,7 +753,7 @@
() -> nc2.addTransportType(TRANSPORT_WIFI));
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.R) // New behavior in updatable NetworkCapabilities (S+)
+ @Test
public void testSetNetworkSpecifierOnTestMultiTransportNc() {
final NetworkSpecifier specifier = CompatUtil.makeEthernetNetworkSpecifier("eth0");
NetworkCapabilities nc = new NetworkCapabilities.Builder()
@@ -859,7 +862,7 @@
assertEquals(TRANSPORT_TEST, transportTypes[3]);
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testTelephonyNetworkSpecifier() {
final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1);
final NetworkCapabilities nc1 = new NetworkCapabilities.Builder()
@@ -970,7 +973,7 @@
assertEquals(specifier, nc.getNetworkSpecifier());
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testAdministratorUidsAndOwnerUid() {
// Test default owner uid.
// If the owner uid is not set, the default value should be Process.INVALID_UID.
@@ -1014,7 +1017,7 @@
return nc;
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
public void testSubIds() throws Exception {
final NetworkCapabilities ncWithoutId = capsWithSubIds();
final NetworkCapabilities ncWithId = capsWithSubIds(TEST_SUBID1);
@@ -1036,7 +1039,7 @@
assertTrue(requestWithoutId.canBeSatisfiedBy(ncWithId));
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
public void testEqualsSubIds() throws Exception {
assertEquals(capsWithSubIds(), capsWithSubIds());
assertNotEquals(capsWithSubIds(), capsWithSubIds(TEST_SUBID1));
@@ -1185,7 +1188,7 @@
}
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testBuilder() {
final int ownerUid = 1001;
final int signalStrength = -80;
@@ -1255,7 +1258,7 @@
}
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
public void testBuilderWithoutDefaultCap() {
final NetworkCapabilities nc =
NetworkCapabilities.Builder.withoutDefaultCapabilities().build();
@@ -1266,12 +1269,12 @@
assertEquals(0, nc.getCapabilities().length);
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
public void testRestrictCapabilitiesForTestNetworkByNotOwnerWithNonRestrictedNc() {
testRestrictCapabilitiesForTestNetworkWithNonRestrictedNc(false /* isOwner */);
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
public void testRestrictCapabilitiesForTestNetworkByOwnerWithNonRestrictedNc() {
testRestrictCapabilitiesForTestNetworkWithNonRestrictedNc(true /* isOwner */);
}
@@ -1316,12 +1319,12 @@
assertEquals(expectedNcBuilder.build(), nonRestrictedNc);
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
public void testRestrictCapabilitiesForTestNetworkByNotOwnerWithRestrictedNc() {
testRestrictCapabilitiesForTestNetworkWithRestrictedNc(false /* isOwner */);
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
public void testRestrictCapabilitiesForTestNetworkByOwnerWithRestrictedNc() {
testRestrictCapabilitiesForTestNetworkWithRestrictedNc(true /* isOwner */);
}
diff --git a/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt b/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt
new file mode 100644
index 0000000..368a519
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkStatsCollectionTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.netstats
+
+import android.net.NetworkIdentity
+import android.net.NetworkStatsCollection
+import android.net.NetworkStatsHistory
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+import kotlin.test.fail
+
+@ConnectivityModuleTest
+@RunWith(JUnit4::class)
+@SmallTest
+class NetworkStatsCollectionTest {
+ @Rule
+ @JvmField
+ val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+ @Test
+ fun testBuilder() {
+ val ident = setOf<NetworkIdentity>()
+ val key1 = NetworkStatsCollection.Key(ident, /* uid */ 0, /* set */ 0, /* tag */ 0)
+ val key2 = NetworkStatsCollection.Key(ident, /* uid */ 1, /* set */ 0, /* tag */ 0)
+ val bucketDuration = 10L
+ val entry1 = NetworkStatsHistory.Entry(10, 10, 40, 4, 50, 5, 60)
+ val entry2 = NetworkStatsHistory.Entry(30, 10, 3, 41, 7, 1, 0)
+ val history1 = NetworkStatsHistory.Builder(10, 5)
+ .addEntry(entry1)
+ .addEntry(entry2)
+ .build()
+ val history2 = NetworkStatsHistory(10, 5)
+ val actualCollection = NetworkStatsCollection.Builder(bucketDuration)
+ .addEntry(key1, history1)
+ .addEntry(key2, history2)
+ .build()
+
+ // The builder will omit any entry with empty history. Thus, only history1
+ // is expected in the result collection.
+ val actualEntries = actualCollection.entries
+ assertEquals(1, actualEntries.size)
+ val actualHistory = actualEntries[key1] ?: fail("There should be an entry for $key1")
+ assertEquals(history1.entries, actualHistory.entries)
+ }
+}
diff --git a/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt b/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt
new file mode 100644
index 0000000..a6c9f3c
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.netstats
+
+import android.net.NetworkStatsHistory
+import android.text.format.DateUtils
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+
+@ConnectivityModuleTest
+@RunWith(JUnit4::class)
+@SmallTest
+class NetworkStatsHistoryTest {
+ @Rule
+ @JvmField
+ val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+ @Test
+ fun testBuilder() {
+ val entry1 = NetworkStatsHistory.Entry(10, 30, 40, 4, 50, 5, 60)
+ val entry2 = NetworkStatsHistory.Entry(30, 15, 3, 41, 7, 1, 0)
+ val entry3 = NetworkStatsHistory.Entry(7, 301, 11, 14, 31, 2, 80)
+ val statsEmpty = NetworkStatsHistory
+ .Builder(DateUtils.HOUR_IN_MILLIS, /* initialCapacity */ 10).build()
+ assertEquals(0, statsEmpty.entries.size)
+ assertEquals(DateUtils.HOUR_IN_MILLIS, statsEmpty.bucketDuration)
+ val statsSingle = NetworkStatsHistory
+ .Builder(DateUtils.HOUR_IN_MILLIS, /* initialCapacity */ 8)
+ .addEntry(entry1)
+ .build()
+ statsSingle.assertEntriesEqual(entry1)
+ assertEquals(DateUtils.HOUR_IN_MILLIS, statsSingle.bucketDuration)
+
+ val statsMultiple = NetworkStatsHistory
+ .Builder(DateUtils.SECOND_IN_MILLIS, /* initialCapacity */ 0)
+ .addEntry(entry1).addEntry(entry2).addEntry(entry3)
+ .build()
+ assertEquals(DateUtils.SECOND_IN_MILLIS, statsMultiple.bucketDuration)
+ // Verify the entries exist and sorted.
+ statsMultiple.assertEntriesEqual(entry3, entry1, entry2)
+ }
+
+ @Test
+ fun testBuilderSortAndDeduplicate() {
+ val entry1 = NetworkStatsHistory.Entry(10, 30, 40, 4, 50, 5, 60)
+ val entry2 = NetworkStatsHistory.Entry(30, 15, 3, 41, 7, 1, 0)
+ val entry3 = NetworkStatsHistory.Entry(30, 999, 11, 14, 31, 2, 80)
+ val entry4 = NetworkStatsHistory.Entry(10, 15, 1, 17, 5, 33, 10)
+ val entry5 = NetworkStatsHistory.Entry(6, 1, 9, 11, 29, 1, 7)
+
+ // Entries for verification.
+ // Note that active time of 2 + 3 is truncated to bucket duration since the active time
+ // should not go over bucket duration.
+ val entry2and3 = NetworkStatsHistory.Entry(30, 1000, 14, 55, 38, 3, 80)
+ val entry1and4 = NetworkStatsHistory.Entry(10, 45, 41, 21, 55, 38, 70)
+
+ val statsMultiple = NetworkStatsHistory
+ .Builder(DateUtils.SECOND_IN_MILLIS, /* initialCapacity */ 0)
+ .addEntry(entry1).addEntry(entry2).addEntry(entry3)
+ .addEntry(entry4).addEntry(entry5).build()
+ assertEquals(DateUtils.SECOND_IN_MILLIS, statsMultiple.bucketDuration)
+ // Verify the entries sorted and deduplicated.
+ statsMultiple.assertEntriesEqual(entry5, entry1and4, entry2and3)
+ }
+
+ fun NetworkStatsHistory.assertEntriesEqual(vararg entries: NetworkStatsHistory.Entry) {
+ assertEquals(entries.size, this.entries.size)
+ entries.forEachIndexed { i, element ->
+ assertEquals(element, this.entries[i])
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/common/java/android/net/netstats/NetworkTemplateTest.kt b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
new file mode 100644
index 0000000..192694b
--- /dev/null
+++ b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.netstats
+
+import android.net.NetworkStats.DEFAULT_NETWORK_ALL
+import android.net.NetworkStats.METERED_ALL
+import android.net.NetworkStats.METERED_YES
+import android.net.NetworkStats.ROAMING_YES
+import android.net.NetworkStats.ROAMING_ALL
+import android.net.NetworkTemplate
+import android.net.NetworkTemplate.MATCH_BLUETOOTH
+import android.net.NetworkTemplate.MATCH_CARRIER
+import android.net.NetworkTemplate.MATCH_ETHERNET
+import android.net.NetworkTemplate.MATCH_MOBILE
+import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
+import android.net.NetworkTemplate.MATCH_PROXY
+import android.net.NetworkTemplate.MATCH_WIFI
+import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
+import android.net.NetworkTemplate.NETWORK_TYPE_ALL
+import android.net.NetworkTemplate.OEM_MANAGED_ALL
+import android.telephony.TelephonyManager
+import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL
+import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.SC_V2
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+private const val TEST_IMSI1 = "imsi"
+private const val TEST_WIFI_KEY1 = "wifiKey1"
+private const val TEST_WIFI_KEY2 = "wifiKey2"
+
+@RunWith(JUnit4::class)
+@ConnectivityModuleTest
+class NetworkTemplateTest {
+ @Rule
+ @JvmField
+ val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+ @Test
+ fun testBuilderMatchRules() {
+ // Verify unknown match rules cannot construct templates.
+ listOf(Integer.MIN_VALUE, -1, Integer.MAX_VALUE).forEach {
+ assertFailsWith<IllegalArgumentException> {
+ NetworkTemplate.Builder(it).build()
+ }
+ }
+
+ // Verify hidden match rules cannot construct templates.
+ listOf(MATCH_WIFI_WILDCARD, MATCH_MOBILE_WILDCARD, MATCH_PROXY).forEach {
+ assertFailsWith<IllegalArgumentException> {
+ NetworkTemplate.Builder(it).build()
+ }
+ }
+
+ // Verify template which matches metered cellular and carrier networks with
+ // the given IMSI. See buildTemplateMobileAll and buildTemplateCarrierMetered.
+ listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
+ NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
+ .setMeteredness(METERED_YES).build().let {
+ val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
+ arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+ ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+ assertEquals(expectedTemplate, it)
+ }
+ }
+
+ // Verify template which matches roaming cellular and carrier networks with
+ // the given IMSI.
+ listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
+ NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
+ .setRoaming(ROAMING_YES).setMeteredness(METERED_YES).build().let {
+ val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
+ arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+ ROAMING_YES, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+ assertEquals(expectedTemplate, it)
+ }
+ }
+
+ // Verify carrier template cannot be created without IMSI.
+ assertFailsWith<IllegalArgumentException> {
+ NetworkTemplate.Builder(MATCH_CARRIER).build()
+ }
+
+ // Verify template which matches metered cellular networks,
+ // regardless of IMSI. See buildTemplateMobileWildcard.
+ NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build().let {
+ val expectedTemplate = NetworkTemplate(MATCH_MOBILE_WILDCARD, null /*subscriberId*/,
+ null /*subscriberIds*/, arrayOf<String>(),
+ METERED_YES, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+ assertEquals(expectedTemplate, it)
+ }
+
+ // Verify template which matches metered cellular networks and ratType.
+ // See NetworkTemplate#buildTemplateMobileWithRatType.
+ NetworkTemplate.Builder(MATCH_MOBILE).setSubscriberIds(setOf(TEST_IMSI1))
+ .setMeteredness(METERED_YES).setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
+ .build().let {
+ val expectedTemplate = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1,
+ arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
+ ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_UMTS,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+ assertEquals(expectedTemplate, it)
+ }
+
+ // Verify template which matches all wifi networks,
+ // regardless of Wifi Network Key. See buildTemplateWifiWildcard and buildTemplateWifi.
+ NetworkTemplate.Builder(MATCH_WIFI).build().let {
+ val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
+ null /*subscriberIds*/, arrayOf<String>(),
+ METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+ assertEquals(expectedTemplate, it)
+ }
+
+ // Verify template which matches wifi networks with the given Wifi Network Key.
+ // See buildTemplateWifi(wifiNetworkKey).
+ NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
+ val expectedTemplate = NetworkTemplate(MATCH_WIFI, null /*subscriberId*/,
+ null /*subscriberIds*/, arrayOf(TEST_WIFI_KEY1),
+ METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+ assertEquals(expectedTemplate, it)
+ }
+
+ // Verify template which matches all wifi networks with the
+ // given Wifi Network Key, and IMSI. See buildTemplateWifi(wifiNetworkKey, subscriberId).
+ NetworkTemplate.Builder(MATCH_WIFI).setSubscriberIds(setOf(TEST_IMSI1))
+ .setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
+ val expectedTemplate = NetworkTemplate(MATCH_WIFI, TEST_IMSI1,
+ arrayOf(TEST_IMSI1), arrayOf(TEST_WIFI_KEY1),
+ METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
+ assertEquals(expectedTemplate, it)
+ }
+
+ // Verify template which matches ethernet and bluetooth networks.
+ // See buildTemplateEthernet and buildTemplateBluetooth.
+ listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
+ NetworkTemplate.Builder(matchRule).build().let {
+ val expectedTemplate = NetworkTemplate(matchRule, null /*subscriberId*/,
+ null /*subscriberIds*/, arrayOf<String>(),
+ METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+ assertEquals(expectedTemplate, it)
+ }
+ }
+ }
+
+ @Test
+ fun testBuilderWifiNetworkKeys() {
+ // Verify template builder which generates same template with the given different
+ // sequence keys.
+ NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
+ setOf(TEST_WIFI_KEY1, TEST_WIFI_KEY2)).build().let {
+ val expectedTemplate = NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
+ setOf(TEST_WIFI_KEY2, TEST_WIFI_KEY1)).build()
+ assertEquals(expectedTemplate, it)
+ }
+
+ // Verify template which matches non-wifi networks with the given key is invalid.
+ listOf(MATCH_MOBILE, MATCH_CARRIER, MATCH_ETHERNET, MATCH_BLUETOOTH, -1,
+ Integer.MAX_VALUE).forEach { matchRule ->
+ assertFailsWith<IllegalArgumentException> {
+ NetworkTemplate.Builder(matchRule).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build()
+ }
+ }
+
+ // Verify template which matches wifi networks with the given null key is invalid.
+ assertFailsWith<IllegalArgumentException> {
+ NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(null)).build()
+ }
+
+ // Verify template which matches wifi wildcard with the given empty key set.
+ NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf<String>()).build().let {
+ val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
+ arrayOf<String>() /*subscriberIds*/, arrayOf<String>(),
+ METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
+ OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
+ assertEquals(expectedTemplate, it)
+ }
+ }
+}
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index b684068..ac84e57 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -26,6 +26,7 @@
"tradefed",
],
static_libs: [
+ "CompatChangeGatingTestBase",
"modules-utils-build-testing",
],
// Tag this module as a cts test artifact
@@ -34,4 +35,12 @@
"general-tests",
"sts"
],
+ data: [
+ ":CtsHostsideNetworkTestsApp",
+ ":CtsHostsideNetworkTestsApp2",
+ ":CtsHostsideNetworkTestsApp3",
+ ":CtsHostsideNetworkTestsApp3PreT",
+ ":CtsHostsideNetworkTestsAppNext",
+ ],
+ per_testcase_directory: true,
}
diff --git a/tests/cts/hostside/TEST_MAPPING b/tests/cts/hostside/TEST_MAPPING
index fcec483..ab6de82 100644
--- a/tests/cts/hostside/TEST_MAPPING
+++ b/tests/cts/hostside/TEST_MAPPING
@@ -4,9 +4,6 @@
"name": "CtsHostsideNetworkTests",
"options": [
{
- "include-filter": "com.android.cts.net.HostsideRestrictBackgroundNetworkTests"
- },
- {
"exclude-annotation": "androidx.test.filters.FlakyTest"
},
{
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
index 28437c2..e7b2815 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
@@ -28,5 +28,5 @@
void sendNotification(int notificationId, String notificationType);
void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
void unregisterNetworkCallback();
- void scheduleJob(in JobInfo jobInfo);
+ int scheduleJob(in JobInfo jobInfo);
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index f460180..93e9dcd 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -16,6 +16,7 @@
package com.android.cts.net.hostside;
+import static android.app.job.JobScheduler.RESULT_SUCCESS;
import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
import static android.os.BatteryManager.BATTERY_PLUGGED_AC;
import static android.os.BatteryManager.BATTERY_PLUGGED_USB;
@@ -151,7 +152,7 @@
protected static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 20_000; // 20 sec
- private static final long BROADCAST_TIMEOUT_MS = 15_000;
+ private static final long BROADCAST_TIMEOUT_MS = 5_000;
protected Context mContext;
protected Instrumentation mInstrumentation;
@@ -216,7 +217,10 @@
Log.d(TAG, "Expecting count " + expectedCount + " but actual is " + count + " after "
+ attempts + " attempts; sleeping "
+ SLEEP_TIME_SEC + " seconds before trying again");
- SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
+ // No sleep after the last turn
+ if (attempts <= maxAttempts) {
+ SystemClock.sleep(SLEEP_TIME_SEC * SECOND_IN_MS);
+ }
} while (attempts <= maxAttempts);
assertEquals("Number of expected broadcasts for " + receiverName + " not reached after "
+ maxAttempts * SLEEP_TIME_SEC + " seconds", expectedCount, count);
@@ -327,7 +331,10 @@
}
Log.d(TAG, "App not on background state (" + state + ") on attempt #" + i
+ "; sleeping 1s before trying again");
- SystemClock.sleep(SECOND_IN_MS);
+ // No sleep after the last turn
+ if (i < maxTries) {
+ SystemClock.sleep(SECOND_IN_MS);
+ }
}
fail("App2 (" + mUid + ") is not on background state after "
+ maxTries + " attempts: " + state);
@@ -346,7 +353,10 @@
Log.d(TAG, "App not on foreground state on attempt #" + i
+ "; sleeping 1s before trying again");
turnScreenOn();
- SystemClock.sleep(SECOND_IN_MS);
+ // No sleep after the last turn
+ if (i < maxTries) {
+ SystemClock.sleep(SECOND_IN_MS);
+ }
}
fail("App2 (" + mUid + ") is not on foreground state after "
+ maxTries + " attempts: " + state);
@@ -364,7 +374,10 @@
}
Log.d(TAG, "App not on foreground service state on attempt #" + i
+ "; sleeping 1s before trying again");
- SystemClock.sleep(SECOND_IN_MS);
+ // No sleep after the last turn
+ if (i < maxTries) {
+ SystemClock.sleep(SECOND_IN_MS);
+ }
}
fail("App2 (" + mUid + ") is not on foreground service state after "
+ maxTries + " attempts: " + state);
@@ -505,7 +518,10 @@
Log.v(TAG, "Command '" + command + "' returned '" + result + " instead of '"
+ checker.getExpected() + "' on attempt #" + i
+ "; sleeping " + napTimeSeconds + "s before trying again");
- SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
+ // No sleep after the last turn
+ if (i < maxTries) {
+ SystemClock.sleep(napTimeSeconds * SECOND_IN_MS);
+ }
}
fail("Command '" + command + "' did not return '" + checker.getExpected() + "' after "
+ maxTries
@@ -577,7 +593,10 @@
}
Log.v(TAG, list + " check for uid " + uid + " doesn't match yet (expected "
+ expected + ", got " + actual + "); sleeping 1s before polling again");
- SystemClock.sleep(SECOND_IN_MS);
+ // No sleep after the last turn
+ if (i < maxTries) {
+ SystemClock.sleep(SECOND_IN_MS);
+ }
}
fail(list + " check for uid " + uid + " failed: expected " + expected + ", got " + actual
+ ". Full list: " + uids);
@@ -737,7 +756,8 @@
protected void assertAppIdle(boolean enabled) throws Exception {
try {
- assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG, 15, 2, "Idle=" + enabled);
+ assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG,
+ 30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + enabled);
} catch (Throwable e) {
throw e;
}
@@ -764,7 +784,10 @@
return;
}
Log.v(TAG, "app2 receiver is not ready yet; sleeping 1s before polling again");
- SystemClock.sleep(SECOND_IN_MS);
+ // No sleep after the last turn
+ if (i < maxTries) {
+ SystemClock.sleep(SECOND_IN_MS);
+ }
}
fail("app2 receiver is not ready in " + mUid);
}
@@ -813,8 +836,6 @@
return;
} else if (type == TYPE_COMPONENT_ACTIVTIY) {
turnScreenOn();
- // Wait for screen-on state to propagate through the system.
- SystemClock.sleep(2000);
final CountDownLatch latch = new CountDownLatch(1);
final Intent launchIntent = getIntentForComponent(type);
final Bundle extras = new Bundle();
@@ -855,7 +876,8 @@
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setTransientExtras(extras)
.build();
- mServiceClient.scheduleJob(jobInfo);
+ assertEquals("Error scheduling " + jobInfo,
+ RESULT_SUCCESS, mServiceClient.scheduleJob(jobInfo));
forceRunJob(TEST_APP2_PKG, TEST_JOB_ID);
if (latch.await(JOB_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
final int resultCode = result.get(0).first;
@@ -896,7 +918,7 @@
final Intent intent = new Intent();
if (type == TYPE_COMPONENT_ACTIVTIY) {
intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_ACTIVITY_CLASS))
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
} else if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS))
.setFlags(1);
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
new file mode 100644
index 0000000..10775d0
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getUiDevice;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DOZE_MODE;
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class ConnOnActivityStartTest extends AbstractRestrictBackgroundNetworkTestCase {
+ private static final int TEST_ITERATION_COUNT = 5;
+
+ @Before
+ public final void setUp() throws Exception {
+ super.setUp();
+ resetDeviceState();
+ }
+
+ @After
+ public final void tearDown() throws Exception {
+ super.tearDown();
+ resetDeviceState();
+ }
+
+ private void resetDeviceState() throws Exception {
+ resetBatteryState();
+ setBatterySaverMode(false);
+ setRestrictBackground(false);
+ setAppIdle(false);
+ setDozeMode(false);
+ }
+
+
+ @Test
+ @RequiredProperties({BATTERY_SAVER_MODE})
+ public void testStartActivity_batterySaver() throws Exception {
+ setBatterySaverMode(true);
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver");
+ }
+
+ @Test
+ @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
+ public void testStartActivity_dataSaver() throws Exception {
+ setRestrictBackground(true);
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver");
+ }
+
+ @Test
+ @RequiredProperties({DOZE_MODE})
+ public void testStartActivity_doze() throws Exception {
+ setDozeMode(true);
+ // TODO (235284115): We need to turn on Doze every time before starting
+ // the activity.
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_doze");
+ }
+
+ @Test
+ @RequiredProperties({APP_STANDBY_MODE})
+ public void testStartActivity_appStandby() throws Exception {
+ turnBatteryOn();
+ setAppIdle(true);
+ // TODO (235284115): We need to put the app into app standby mode every
+ // time before starting the activity.
+ assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby");
+ }
+
+ private void assertLaunchedActivityHasNetworkAccess(String testName) throws Exception {
+ for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
+ Log.i(TAG, testName + " start #" + i);
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ getUiDevice().pressHome();
+ assertBackgroundState();
+ Log.i(TAG, testName + " end #" + i);
+ }
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index 8b70f9b..0610774 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -107,7 +107,7 @@
mService.unregisterNetworkCallback();
}
- public void scheduleJob(JobInfo jobInfo) throws RemoteException {
- mService.scheduleJob(jobInfo);
+ public int scheduleJob(JobInfo jobInfo) throws RemoteException {
+ return mService.scheduleJob(jobInfo);
}
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
index b6218d2..c53276b 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -25,7 +25,7 @@
import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED;
import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE;
-import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
import static org.junit.Assert.assertEquals;
@@ -57,6 +57,7 @@
import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
import com.android.compatibility.common.util.AppStandbyUtils;
import com.android.compatibility.common.util.BatteryUtils;
@@ -390,7 +391,7 @@
}
public static String executeShellCommand(String command) {
- final String result = runShellCommand(command).trim();
+ final String result = runShellCommandOrThrow(command).trim();
Log.d(TAG, "Output of '" + command + "': '" + result + "'");
return result;
}
@@ -438,6 +439,10 @@
return InstrumentationRegistry.getInstrumentation();
}
+ public static UiDevice getUiDevice() {
+ return UiDevice.getInstance(getInstrumentation());
+ }
+
// When power saver mode or restrict background enabled or adding any white/black list into
// those modes, NetworkPolicy may need to take some time to update the rules of uids. So having
// this function and using PollingCheck to try to make sure the uid has updated and reduce the
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index dc67c70..dd8b523 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -378,10 +378,6 @@
if (mNetwork == null) {
fail("VPN did not become available after " + TIMEOUT_MS + "ms");
}
-
- // Unfortunately, when the available callback fires, the VPN UID ranges are not yet
- // configured. Give the system some time to do so. http://b/18436087 .
- try { Thread.sleep(3000); } catch(InterruptedException e) {}
}
private void stopVpn() {
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index 01c8cd2..edfaf9f 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -23,6 +23,7 @@
defaults: ["cts_support_defaults"],
sdk_version: "test_current",
static_libs: [
+ "androidx.annotation_annotation",
"CtsHostsideNetworkTestsAidl",
"NetworkStackApiStableShims",
],
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
index 9fdb9c9..82f13ae 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
@@ -29,6 +29,9 @@
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;
+import android.view.WindowManager;
+
+import androidx.annotation.GuardedBy;
import com.android.cts.net.hostside.INetworkStateObserver;
@@ -37,27 +40,24 @@
*/
public class MyActivity extends Activity {
+ @GuardedBy("this")
private BroadcastReceiver finishCommandReceiver = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "MyActivity.onCreate()");
- Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
- finishCommandReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- Log.d(TAG, "Finishing MyActivity");
- MyActivity.this.finish();
- }
- };
- registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY));
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
@Override
public void finish() {
- if (finishCommandReceiver != null) {
- unregisterReceiver(finishCommandReceiver);
+ synchronized (this) {
+ if (finishCommandReceiver != null) {
+ unregisterReceiver(finishCommandReceiver);
+ finishCommandReceiver = null;
+ }
}
super.finish();
}
@@ -69,6 +69,31 @@
}
@Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ Log.d(TAG, "MyActivity.onNewIntent()");
+ setIntent(intent);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(TAG, "MyActivity.onResume(): " + getIntent());
+ Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
+ synchronized (this) {
+ finishCommandReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "Finishing MyActivity");
+ MyActivity.this.finish();
+ }
+ };
+ registerReceiver(finishCommandReceiver, new IntentFilter(ACTION_FINISH_ACTIVITY),
+ Context.RECEIVER_EXPORTED);
+ }
+ }
+
+ @Override
protected void onDestroy() {
Log.d(TAG, "MyActivity.onDestroy()");
super.onDestroy();
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
index f2a7b3f..3ed5391 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -165,10 +165,10 @@
}
@Override
- public void scheduleJob(JobInfo jobInfo) {
+ public int scheduleJob(JobInfo jobInfo) {
final JobScheduler jobScheduler = getApplicationContext()
.getSystemService(JobScheduler.class);
- jobScheduler.schedule(jobInfo);
+ return jobScheduler.schedule(jobInfo);
}
};
diff --git a/tests/cts/hostside/app3/Android.bp b/tests/cts/hostside/app3/Android.bp
new file mode 100644
index 0000000..141cf03
--- /dev/null
+++ b/tests/cts/hostside/app3/Android.bp
@@ -0,0 +1,54 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_defaults {
+ name: "CtsHostsideNetworkTestsApp3Defaults",
+ srcs: ["src/**/*.java"],
+ libs: [
+ "junit",
+ ],
+ static_libs: [
+ "ctstestrunner-axt",
+ "truth-prebuilt",
+ ],
+
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+}
+
+android_test_helper_app {
+ name: "CtsHostsideNetworkTestsApp3",
+ defaults: [
+ "cts_support_defaults",
+ "CtsHostsideNetworkTestsApp3Defaults",
+ ],
+}
+
+android_test_helper_app {
+ name: "CtsHostsideNetworkTestsApp3PreT",
+ target_sdk_version: "31",
+ defaults: [
+ "cts_support_defaults",
+ "CtsHostsideNetworkTestsApp3Defaults",
+ ],
+}
diff --git a/tests/cts/hostside/app3/AndroidManifest.xml b/tests/cts/hostside/app3/AndroidManifest.xml
new file mode 100644
index 0000000..eabcacb
--- /dev/null
+++ b/tests/cts/hostside/app3/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.cts.net.hostside.app3">
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.cts.net.hostside.app3" />
+
+</manifest>
diff --git a/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java b/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java
new file mode 100644
index 0000000..a1a8209
--- /dev/null
+++ b/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside.app3;
+
+import static org.junit.Assert.assertEquals;
+
+import android.Manifest;
+import android.net.IpPrefix;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests to verify {@link LinkProperties#getRoutes} behavior, depending on
+ * {@LinkProperties#EXCLUDED_ROUTES} change state.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExcludedRoutesGatingTest {
+ @Before
+ public void setUp() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG);
+ }
+
+ @After
+ public void tearDown() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void testExcludedRoutesChangeEnabled() {
+ final LinkProperties lp = makeLinkPropertiesWithExcludedRoutes();
+
+ // Excluded routes change is enabled: non-RTN_UNICAST routes are visible.
+ assertEquals(2, lp.getRoutes().size());
+ assertEquals(2, lp.getAllRoutes().size());
+ }
+
+ @Test
+ public void testExcludedRoutesChangeDisabled() {
+ final LinkProperties lp = makeLinkPropertiesWithExcludedRoutes();
+
+ // Excluded routes change is disabled: non-RTN_UNICAST routes are filtered out.
+ assertEquals(0, lp.getRoutes().size());
+ assertEquals(0, lp.getAllRoutes().size());
+ }
+
+ private LinkProperties makeLinkPropertiesWithExcludedRoutes() {
+ final LinkProperties lp = new LinkProperties();
+
+ lp.addRoute(new RouteInfo(new IpPrefix("10.0.0.0/8"), null, null, RouteInfo.RTN_THROW));
+ lp.addRoute(new RouteInfo(new IpPrefix("2001:db8::/64"), null, null,
+ RouteInfo.RTN_UNREACHABLE));
+
+ return lp;
+ }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
new file mode 100644
index 0000000..cfd3130
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net;
+
+import android.platform.test.annotations.FlakyTest;
+
+public class HostsideConnOnActivityStartTest extends HostsideNetworkTestCase {
+ private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ uninstallPackage(TEST_APP2_PKG, false);
+ installPackage(TEST_APP2_APK);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ uninstallPackage(TEST_APP2_PKG, true);
+ }
+
+ public void testStartActivity_batterySaver() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
+ }
+
+ public void testStartActivity_dataSaver() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
+ }
+
+ @FlakyTest(bugId = 231440256)
+ public void testStartActivity_doze() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
+ }
+
+ public void testStartActivity_appStandby() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
+ }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
new file mode 100644
index 0000000..9a1fa42
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net;
+
+import android.compat.cts.CompatChangeGatingTestCase;
+
+import java.util.Set;
+
+/**
+ * Tests for the {@link android.net.LinkProperties#EXCLUDED_ROUTES} compatibility change.
+ *
+ * TODO: see if we can delete this cumbersome host test by moving the coverage to CtsNetTestCases
+ * and CtsNetTestCasesMaxTargetSdk31.
+ */
+public class HostsideLinkPropertiesGatingTests extends CompatChangeGatingTestCase {
+ private static final String TEST_APK = "CtsHostsideNetworkTestsApp3.apk";
+ private static final String TEST_APK_PRE_T = "CtsHostsideNetworkTestsApp3PreT.apk";
+ private static final String TEST_PKG = "com.android.cts.net.hostside.app3";
+ private static final String TEST_CLASS = ".ExcludedRoutesGatingTest";
+
+ private static final long EXCLUDED_ROUTES_CHANGE_ID = 186082280;
+
+ protected void tearDown() throws Exception {
+ uninstallPackage(TEST_PKG, true);
+ }
+
+ public void testExcludedRoutesChangeEnabled() throws Exception {
+ installPackage(TEST_APK, true);
+ runDeviceCompatTest("testExcludedRoutesChangeEnabled");
+ }
+
+ public void testExcludedRoutesChangeDisabledPreT() throws Exception {
+ installPackage(TEST_APK_PRE_T, true);
+ runDeviceCompatTest("testExcludedRoutesChangeDisabled");
+ }
+
+ public void testExcludedRoutesChangeDisabledByOverrideOnDebugBuild() throws Exception {
+ // Must install APK even when skipping test, because tearDown expects uninstall to succeed.
+ installPackage(TEST_APK, true);
+
+ // This test uses an app with a target SDK where the compat change is on by default.
+ // Because user builds do not allow overriding compat changes, only run this test on debug
+ // builds. This seems better than deleting this test and not running it anywhere because we
+ // could in the future run this test on userdebug builds in presubmit.
+ //
+ // We cannot use assumeXyz here because CompatChangeGatingTestCase ultimately inherits from
+ // junit.framework.TestCase, which does not understand assumption failures.
+ if ("user".equals(getDevice().getProperty("ro.build.type"))) return;
+
+ runDeviceCompatTestWithChangeDisabled("testExcludedRoutesChangeDisabled");
+ }
+
+ public void testExcludedRoutesChangeEnabledByOverridePreT() throws Exception {
+ installPackage(TEST_APK_PRE_T, true);
+ runDeviceCompatTestWithChangeEnabled("testExcludedRoutesChangeEnabled");
+ }
+
+ private void runDeviceCompatTest(String methodName) throws Exception {
+ runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(), Set.of());
+ }
+
+ private void runDeviceCompatTestWithChangeEnabled(String methodName) throws Exception {
+ runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(EXCLUDED_ROUTES_CHANGE_ID),
+ Set.of());
+ }
+
+ private void runDeviceCompatTestWithChangeDisabled(String methodName) throws Exception {
+ runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(),
+ Set.of(EXCLUDED_ROUTES_CHANGE_ID));
+ }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index cc07fd1..d0567ae 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -34,8 +34,6 @@
import java.io.FileNotFoundException;
import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
abstract class HostsideNetworkTestCase extends DeviceTestCase implements IAbiReceiver,
IBuildReceiver {
@@ -171,18 +169,19 @@
}
}
- private static final Pattern UID_PATTERN =
- Pattern.compile(".*userId=([0-9]+)$", Pattern.MULTILINE);
-
protected int getUid(String packageName) throws DeviceNotAvailableException {
- final String output = runCommand("dumpsys package " + packageName);
- final Matcher matcher = UID_PATTERN.matcher(output);
- while (matcher.find()) {
- final String match = matcher.group(1);
- return Integer.parseInt(match);
+ final int currentUser = getDevice().getCurrentUser();
+ final String uidLines = runCommand(
+ "cmd package list packages -U --user " + currentUser + " " + packageName);
+ for (String uidLine : uidLines.split("\n")) {
+ if (uidLine.startsWith("package:" + packageName + " uid:")) {
+ final String[] uidLineParts = uidLine.split(":");
+ // 3rd entry is package uid
+ return Integer.parseInt(uidLineParts[2].trim());
+ }
}
- throw new RuntimeException("Did not find regexp '" + UID_PATTERN + "' on adb output\n"
- + output);
+ throw new IllegalStateException("Failed to find the test app on the device; pkg="
+ + packageName + ", u=" + currentUser);
}
protected String runCommand(String command) throws DeviceNotAvailableException {
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index e979a3b..a6ed762 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -62,6 +62,7 @@
// sdk_version: "current",
platform_apis: true,
required: ["ConnectivityChecker"],
+ test_config_template: "AndroidTestTemplate.xml",
}
// Networking CTS tests for development and release. These tests always target the platform SDK
@@ -79,7 +80,16 @@
"cts",
"general-tests",
],
- test_config_template: "AndroidTestTemplate.xml",
+}
+
+java_defaults {
+ name: "CtsNetTestCasesApiStableDefaults",
+ // TODO: CTS should not depend on the entirety of the networkstack code.
+ static_libs: [
+ "NetworkStackApiStableLib",
+ ],
+ jni_uses_sdk_apis: true,
+ min_sdk_version: "29",
}
// Networking CTS tests that target the latest released SDK. These tests can be installed on release
@@ -87,14 +97,11 @@
// on release devices.
android_test {
name: "CtsNetTestCasesLatestSdk",
- defaults: ["CtsNetTestCasesDefaults"],
- // TODO: CTS should not depend on the entirety of the networkstack code.
- static_libs: [
- "NetworkStackApiStableLib",
+ defaults: [
+ "CtsNetTestCasesDefaults",
+ "CtsNetTestCasesApiStableDefaults",
],
- jni_uses_sdk_apis: true,
- min_sdk_version: "29",
- target_sdk_version: "30",
+ target_sdk_version: "33",
test_suites: [
"general-tests",
"mts-dnsresolver",
@@ -102,5 +109,21 @@
"mts-tethering",
"mts-wifi",
],
- test_config_template: "AndroidTestTemplate.xml",
}
+
+android_test {
+ name: "CtsNetTestCasesMaxTargetSdk31", // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
+ defaults: [
+ "CtsNetTestCasesDefaults",
+ "CtsNetTestCasesApiStableDefaults",
+ ],
+ target_sdk_version: "31",
+ package_name: "android.net.cts.maxtargetsdk31", // CTS package names must be unique.
+ instrumentation_target_package: "android.net.cts.maxtargetsdk31",
+ test_suites: [
+ "cts",
+ "general-tests",
+ "mts-networking",
+ ],
+}
+
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index 3b47100..6b5bb93 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -44,7 +44,8 @@
android.permission.MANAGE_TEST_NETWORKS
-->
- <application android:usesCleartextTraffic="true">
+ <application android:debuggable="true"
+ android:usesCleartextTraffic="true">
<uses-library android:name="android.test.runner" />
<uses-library android:name="org.apache.http.legacy" android:required="false" />
</application>
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 48a1c79..d2fb04a 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -33,9 +33,36 @@
<target_preparer class="com.android.testutils.DisableConfigSyncTargetPreparer">
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
- <option name="package" value="android.net.cts" />
+ <option name="package" value="{PACKAGE}" />
<option name="runtime-hint" value="9m4s" />
<option name="hidden-api-checks" value="false" />
<option name="isolated-storage" value="false" />
+ <!-- Test filter that allows test APKs to select which tests they want to run by annotating
+ those tests with an annotation matching the name of the APK.
+
+ This allows us to maintain one AndroidTestTemplate.xml for all CtsNetTestCases*.apk,
+ and have CtsNetTestCases and CtsNetTestCasesLatestSdk run all tests, but have
+ CtsNetTestCasesMaxTargetSdk31 run only tests that require target SDK 31.
+
+ This relies on the fact that if the class specified in include-annotation exists, then
+ the runner will only run the tests annotated with that annotation, but if it does not,
+ the runner will run all the tests. -->
+ <option name="include-annotation" value="com.android.testutils.filters.{MODULE}" />
</test>
+ <!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if
+ one of the Mainline modules below is present on the device used for testing. -->
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <!-- Tethering Module (internal version). -->
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ <!-- Tethering Module (AOSP version). -->
+ <option name="mainline-module-package-name" value="com.android.tethering" />
+ <!-- NetworkStack Module (internal version). Should always be installed with CaptivePortalLogin. -->
+ <option name="mainline-module-package-name" value="com.google.android.networkstack" />
+ <!-- NetworkStack Module (AOSP version). Should always be installed with CaptivePortalLogin. -->
+ <option name="mainline-module-package-name" value="com.android.networkstack" />
+ <!-- Resolver Module (internal version). -->
+ <option name="mainline-module-package-name" value="com.google.android.resolv" />
+ <!-- Resolver Module (AOSP version). -->
+ <option name="mainline-module-package-name" value="com.android.resolv" />
+ </object>
</configuration>
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 5b37294..9b81a56 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -51,5 +51,8 @@
"cts",
"general-tests",
],
-
+ data: [
+ ":CtsNetTestAppForApi23",
+ ],
+ per_testcase_directory: true,
}
diff --git a/tests/cts/net/native/src/BpfCompatTest.cpp b/tests/cts/net/native/src/BpfCompatTest.cpp
index 97ecb9e..e52533b 100644
--- a/tests/cts/net/native/src/BpfCompatTest.cpp
+++ b/tests/cts/net/native/src/BpfCompatTest.cpp
@@ -31,8 +31,13 @@
std::ifstream elfFile(elfPath, std::ios::in | std::ios::binary);
ASSERT_TRUE(elfFile.is_open());
- EXPECT_EQ(48, readSectionUint("size_of_bpf_map_def", elfFile, 0));
- EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+ if (android::modules::sdklevel::IsAtLeastT()) {
+ EXPECT_EQ(116, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+ EXPECT_EQ(92, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+ } else {
+ EXPECT_EQ(48, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+ EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+ }
}
TEST(BpfTest, bpfStructSizeTestPreT) {
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index 0344604..1b77d5f 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -33,7 +33,6 @@
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkRequest
import android.net.Uri
-import android.net.cts.NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig
import android.net.cts.NetworkValidationTestUtil.setHttpUrlDeviceConfig
import android.net.cts.NetworkValidationTestUtil.setHttpsUrlDeviceConfig
import android.net.cts.NetworkValidationTestUtil.setUrlExpirationDeviceConfig
@@ -60,6 +59,8 @@
import org.junit.Assume.assumeTrue
import org.junit.Assume.assumeFalse
import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
import org.junit.runner.RunWith
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@@ -99,34 +100,42 @@
private val server = TestHttpServer("localhost")
+ @get:Rule
+ val deviceConfigRule = DeviceConfigRule(retryCountBeforeSIfConfigChanged = 5)
+
+ companion object {
+ @JvmStatic @BeforeClass
+ fun setUpClass() {
+ runAsShell(READ_DEVICE_CONFIG) {
+ // Verify that the test URLs are not normally set on the device, but do not fail if
+ // the test URLs are set to what this test uses (URLs on localhost), in case the
+ // test was interrupted manually and rerun.
+ assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
+ assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
+ }
+ NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig()
+ }
+
+ private fun assertEmptyOrLocalhostUrl(urlKey: String) {
+ val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
+ assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
+ "$urlKey must not be set in production scenarios (current value: $url)")
+ }
+ }
+
@Before
fun setUp() {
- runAsShell(READ_DEVICE_CONFIG) {
- // Verify that the test URLs are not normally set on the device, but do not fail if the
- // test URLs are set to what this test uses (URLs on localhost), in case the test was
- // interrupted manually and rerun.
- assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
- assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
- }
- clearValidationTestUrlsDeviceConfig()
server.start()
}
@After
fun tearDown() {
- clearValidationTestUrlsDeviceConfig()
if (pm.hasSystemFeature(FEATURE_WIFI)) {
- reconnectWifi()
+ deviceConfigRule.runAfterNextCleanup { reconnectWifi() }
}
server.stop()
}
- private fun assertEmptyOrLocalhostUrl(urlKey: String) {
- val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
- assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
- "$urlKey must not be set in production scenarios (current value: $url)")
- }
-
@Test
fun testCaptivePortalIsNotDefaultNetwork() {
assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
@@ -154,12 +163,13 @@
server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR)
val headers = mapOf("Location" to makeUrl(TEST_PORTAL_URL_PATH))
server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT, headers)
- setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH))
- setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH))
+ setHttpsUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTPS_URL_PATH))
+ setHttpUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTP_URL_PATH))
Log.d(TAG, "Set portal URLs to $TEST_HTTPS_URL_PATH and $TEST_HTTP_URL_PATH")
// URL expiration needs to be in the next 10 minutes
assertTrue(WIFI_CONNECT_TIMEOUT_MS < TimeUnit.MINUTES.toMillis(10))
- setUrlExpirationDeviceConfig(System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
+ setUrlExpirationDeviceConfig(deviceConfigRule,
+ System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
// Wait for a captive portal to be detected on the network
val wifiNetworkFuture = CompletableFuture<Network>()
@@ -215,4 +225,4 @@
utils.ensureWifiDisconnected(null /* wifiNetworkToCheck */)
utils.ensureWifiConnected()
}
-}
\ No newline at end of file
+}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index 68fa38d..7d1e13f 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -113,7 +113,7 @@
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 DELAY_FOR_ADMIN_UIDS_MILLIS = 2000;
+ private static final int DELAY_FOR_ADMIN_UIDS_MILLIS = 5000;
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 9055861..766d62f 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -37,6 +37,11 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.ConnectivityManager.EXTRA_NETWORK;
import static android.net.ConnectivityManager.EXTRA_NETWORK_REQUEST;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
import static android.net.ConnectivityManager.TYPE_ETHERNET;
@@ -52,6 +57,7 @@
import static android.net.ConnectivityManager.TYPE_PROXY;
import static android.net.ConnectivityManager.TYPE_VPN;
import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
+import static android.net.ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -151,6 +157,7 @@
import android.os.Looper;
import android.os.MessageQueue;
import android.os.Process;
+import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
@@ -163,7 +170,6 @@
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
-import android.util.Pair;
import android.util.Range;
import androidx.test.InstrumentationRegistry;
@@ -181,6 +187,7 @@
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRuleKt;
+import com.android.testutils.DeviceInfoUtils;
import com.android.testutils.DumpTestUtils;
import com.android.testutils.RecorderCallback.CallbackEntry;
import com.android.testutils.TestHttpServer;
@@ -202,6 +209,8 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
import java.net.HttpURLConnection;
import java.net.Inet4Address;
import java.net.Inet6Address;
@@ -216,6 +225,7 @@
import java.util.Collection;
import java.util.List;
import java.util.Objects;
+import java.util.Random;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
@@ -239,6 +249,10 @@
@Rule
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+ @Rule
+ public final DeviceConfigRule mTestValidationConfigRule = new DeviceConfigRule(
+ 5 /* retryCountBeforeSIfConfigChanged */);
+
private static final String TAG = ConnectivityManagerTest.class.getSimpleName();
public static final int TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE;
@@ -253,6 +267,7 @@
private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 5_000;
private static final int NO_CALLBACK_TIMEOUT_MS = 100;
+ 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;
// device could have only one interface: data, wifi.
@@ -661,10 +676,12 @@
// CaptivePortalApiUrl & CaptivePortalData will be preserved if the given uid holds the
// NETWORK_SETTINGS permission.
- assertEquals(capportUrl,
+ assertNotNull(lp.getCaptivePortalApiUrl());
+ assertNotNull(lp.getCaptivePortalData());
+ assertEquals(lp.getCaptivePortalApiUrl(),
mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
.getCaptivePortalApiUrl());
- assertEquals(capportData,
+ assertEquals(lp.getCaptivePortalData(),
mCm.getRedactedLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
.getCaptivePortalData());
});
@@ -887,9 +904,21 @@
//
// Note that this test this will still fail in instant mode if a device supports Ethernet
// via other hardware means. We are not currently aware of any such device.
- return (mContext.getSystemService(Context.ETHERNET_SERVICE) != null) ||
- mPackageManager.hasSystemFeature(FEATURE_ETHERNET) ||
- mPackageManager.hasSystemFeature(FEATURE_USB_HOST);
+ return hasEthernetService()
+ || mPackageManager.hasSystemFeature(FEATURE_ETHERNET)
+ || mPackageManager.hasSystemFeature(FEATURE_USB_HOST);
+ }
+
+ private boolean hasEthernetService() {
+ // On Q creating EthernetManager from a thread that does not have a looper (like the test
+ // thread) crashes because it tried to use Looper.myLooper() through the default Handler
+ // constructor to run onAvailabilityChanged callbacks. Use ServiceManager to check whether
+ // the service exists instead.
+ // TODO: remove once Q is no longer supported in MTS, as ServiceManager is hidden API
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+ return ServiceManager.getService(Context.ETHERNET_SERVICE) != null;
+ }
+ return mContext.getSystemService(Context.ETHERNET_SERVICE) != null;
}
private boolean shouldBeSupported(int networkType) {
@@ -1590,51 +1619,7 @@
private static boolean isTcpKeepaliveSupportedByKernel() {
final String kVersionString = VintfRuntimeInfo.getKernelVersion();
- return compareMajorMinorVersion(kVersionString, "4.8") >= 0;
- }
-
- private static Pair<Integer, Integer> getVersionFromString(String version) {
- // Only gets major and minor number of the version string.
- final Pattern versionPattern = Pattern.compile("^(\\d+)(\\.(\\d+))?.*");
- final Matcher m = versionPattern.matcher(version);
- if (m.matches()) {
- final int major = Integer.parseInt(m.group(1));
- final int minor = TextUtils.isEmpty(m.group(3)) ? 0 : Integer.parseInt(m.group(3));
- return new Pair<>(major, minor);
- } else {
- return new Pair<>(0, 0);
- }
- }
-
- // TODO: Move to util class.
- private static int compareMajorMinorVersion(final String s1, final String s2) {
- final Pair<Integer, Integer> v1 = getVersionFromString(s1);
- final Pair<Integer, Integer> v2 = getVersionFromString(s2);
-
- if (v1.first == v2.first) {
- return Integer.compare(v1.second, v2.second);
- } else {
- return Integer.compare(v1.first, v2.first);
- }
- }
-
- /**
- * Verifies that version string compare logic returns expected result for various cases.
- * Note that only major and minor number are compared.
- */
- @Test
- public void testMajorMinorVersionCompare() {
- assertEquals(0, compareMajorMinorVersion("4.8.1", "4.8"));
- assertEquals(1, compareMajorMinorVersion("4.9", "4.8.1"));
- assertEquals(1, compareMajorMinorVersion("5.0", "4.8"));
- assertEquals(1, compareMajorMinorVersion("5", "4.8"));
- assertEquals(0, compareMajorMinorVersion("5", "5.0"));
- assertEquals(1, compareMajorMinorVersion("5-beta1", "4.8"));
- assertEquals(0, compareMajorMinorVersion("4.8.0.0", "4.8"));
- assertEquals(0, compareMajorMinorVersion("4.8-RC1", "4.8"));
- assertEquals(0, compareMajorMinorVersion("4.8", "4.8"));
- assertEquals(-1, compareMajorMinorVersion("3.10", "4.8.0"));
- assertEquals(-1, compareMajorMinorVersion("4.7.10.10", "4.8"));
+ return DeviceInfoUtils.compareMajorMinorVersion(kVersionString, "4.8") >= 0;
}
/**
@@ -2784,9 +2769,8 @@
// Accept partial connectivity network should result in a validated network
expectNetworkHasCapability(network, NET_CAPABILITY_VALIDATED, WIFI_CONNECT_TIMEOUT_MS);
} finally {
- resetValidationConfig();
- // Reconnect wifi to reset the wifi status
- reconnectWifi();
+ mHttpServer.stop();
+ mTestValidationConfigRule.runAfterNextCleanup(this::reconnectWifi);
}
}
@@ -2811,11 +2795,13 @@
// Reject partial connectivity network should cause the network being torn down
assertEquals(network, cb.waitForLost());
} finally {
- resetValidationConfig();
+ mHttpServer.stop();
// Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
// apply here. Thus, turn off wifi first and restart to restore.
- runShellCommand("svc wifi disable");
- mCtsNetUtils.ensureWifiConnected();
+ mTestValidationConfigRule.runAfterNextCleanup(() -> {
+ runShellCommand("svc wifi disable");
+ mCtsNetUtils.ensureWifiConnected();
+ });
}
}
@@ -2851,11 +2837,13 @@
});
waitForLost(wifiCb);
} finally {
- resetValidationConfig();
+ mHttpServer.stop();
/// Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
// apply here. Thus, turn off wifi first and restart to restore.
- runShellCommand("svc wifi disable");
- mCtsNetUtils.ensureWifiConnected();
+ mTestValidationConfigRule.runAfterNextCleanup(() -> {
+ runShellCommand("svc wifi disable");
+ mCtsNetUtils.ensureWifiConnected();
+ });
}
}
@@ -2915,9 +2903,8 @@
wifiCb.assertNoCallbackThat(NO_CALLBACK_TIMEOUT_MS, c -> isValidatedCaps(c));
} finally {
resetAvoidBadWifi(previousAvoidBadWifi);
- resetValidationConfig();
- // Reconnect wifi to reset the wifi status
- reconnectWifi();
+ mHttpServer.stop();
+ mTestValidationConfigRule.runAfterNextCleanup(this::reconnectWifi);
}
}
@@ -2961,11 +2948,6 @@
return future.get(timeout, TimeUnit.MILLISECONDS);
}
- private void resetValidationConfig() {
- NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig();
- mHttpServer.stop();
- }
-
private void prepareHttpServer() throws Exception {
runAsShell(READ_DEVICE_CONFIG, () -> {
// Verify that the test URLs are not normally set on the device, but do not fail if the
@@ -3038,9 +3020,11 @@
mHttpServer.addResponse(new TestHttpServer.Request(
TEST_HTTP_URL_PATH, Method.GET, "" /* queryParameters */),
httpStatusCode, null /* locationHeader */, "" /* content */);
- NetworkValidationTestUtil.setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH));
- NetworkValidationTestUtil.setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH));
- NetworkValidationTestUtil.setUrlExpirationDeviceConfig(
+ NetworkValidationTestUtil.setHttpsUrlDeviceConfig(mTestValidationConfigRule,
+ makeUrl(TEST_HTTPS_URL_PATH));
+ NetworkValidationTestUtil.setHttpUrlDeviceConfig(mTestValidationConfigRule,
+ makeUrl(TEST_HTTP_URL_PATH));
+ NetworkValidationTestUtil.setUrlExpirationDeviceConfig(mTestValidationConfigRule,
System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS);
}
@@ -3197,7 +3181,7 @@
@AppModeFull(reason = "WRITE_SECURE_SETTINGS permission can't be granted to instant apps")
@Test
public void testUidsAllowedOnRestrictedNetworks() throws Exception {
- assumeTrue(TestUtils.shouldTestSApis());
+ assumeTestSApis();
// TODO (b/175199465): figure out a reasonable permission check for
// setUidsAllowedOnRestrictedNetworks that allows tests but not system-external callers.
@@ -3210,10 +3194,10 @@
// because it has been just installed to device. In case the uid is existed in setting
// mistakenly, try to remove the uid and set correct uids to setting.
originalUidsAllowedOnRestrictedNetworks.remove(uid);
- runWithShellPermissionIdentity(() ->
- ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks(
- mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+ runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
+ mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+ // File a restricted network request with permission first to hold the connection.
final TestableNetworkCallback testNetworkCb = new TestableNetworkCallback();
final NetworkRequest testRequest = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_TEST)
@@ -3225,6 +3209,19 @@
runWithShellPermissionIdentity(() -> requestNetwork(testRequest, testNetworkCb),
CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+ // File another restricted network request without permission.
+ final TestableNetworkCallback restrictedNetworkCb = new TestableNetworkCallback();
+ final NetworkRequest restrictedRequest = new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
+ TEST_RESTRICTED_NW_IFACE_NAME))
+ .build();
+ // Uid is not in allowed list and no permissions. Expect that SecurityException will throw.
+ assertThrows(SecurityException.class,
+ () -> mCm.requestNetwork(restrictedRequest, restrictedNetworkCb));
+
final NetworkAgent agent = createRestrictedNetworkAgent(mContext);
final Network network = agent.getNetwork();
@@ -3244,19 +3241,28 @@
final Set<Integer> newUidsAllowedOnRestrictedNetworks =
new ArraySet<>(originalUidsAllowedOnRestrictedNetworks);
newUidsAllowedOnRestrictedNetworks.add(uid);
- runWithShellPermissionIdentity(() ->
- ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks(
- mContext, newUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+ runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
+ mContext, newUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
// Wait a while for sending allowed uids on the restricted network to netd.
- // TODD: Have a significant signal to know the uids has been send to netd.
+ // TODD: Have a significant signal to know the uids has been sent to netd.
assertBindSocketToNetworkSuccess(network);
+
+ if (TestUtils.shouldTestTApis()) {
+ // Uid is in allowed list. Try file network request again.
+ requestNetwork(restrictedRequest, restrictedNetworkCb);
+ // Verify that the network is restricted.
+ restrictedNetworkCb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
+ NETWORK_CALLBACK_TIMEOUT_MS,
+ entry -> network.equals(entry.getNetwork())
+ && (!((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+ .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)));
+ }
} finally {
agent.unregister();
// Restore setting.
- runWithShellPermissionIdentity(() ->
- ConnectivitySettingsManager.setUidsAllowedOnRestrictedNetworks(
- mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
+ runWithShellPermissionIdentity(() -> setUidsAllowedOnRestrictedNetworks(
+ mContext, originalUidsAllowedOnRestrictedNetworks), NETWORK_SETTINGS);
}
}
@@ -3280,6 +3286,119 @@
assertTrue(dumpOutput, dumpOutput.contains("BPF map content"));
}
+ private void checkFirewallBlocking(final DatagramSocket srcSock, final DatagramSocket dstSock,
+ final boolean expectBlock) throws Exception {
+ final Random random = new Random();
+ final byte[] sendData = new byte[100];
+ random.nextBytes(sendData);
+
+ final DatagramPacket pkt = new DatagramPacket(sendData, sendData.length,
+ InetAddresses.parseNumericAddress("::1"), dstSock.getLocalPort());
+ try {
+ srcSock.send(pkt);
+ } catch (IOException e) {
+ if (expectBlock) {
+ return;
+ }
+ fail("Expect not to be blocked by firewall but sending packet was blocked");
+ }
+
+ if (expectBlock) {
+ fail("Expect to be blocked by firewall but sending packet was not blocked");
+ }
+
+ dstSock.receive(pkt);
+ assertArrayEquals(sendData, pkt.getData());
+ }
+
+ private static final boolean EXPECT_PASS = false;
+ private static final boolean EXPECT_BLOCK = true;
+
+ private void doTestFirewallBlockingDenyRule(final int chain) {
+ runWithShellPermissionIdentity(() -> {
+ try (DatagramSocket srcSock = new DatagramSocket();
+ DatagramSocket dstSock = new DatagramSocket()) {
+ dstSock.setSoTimeout(SOCKET_TIMEOUT_MS);
+
+ // No global config, No uid config
+ checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+ // Has global config, No uid config
+ mCm.setFirewallChainEnabled(chain, true /* enable */);
+ checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+ // Has global config, Has uid config
+ mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
+ checkFirewallBlocking(srcSock, dstSock, EXPECT_BLOCK);
+
+ // No global config, Has uid config
+ mCm.setFirewallChainEnabled(chain, false /* enable */);
+ checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+ // No global config, No uid config
+ mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
+ checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+ } finally {
+ mCm.setFirewallChainEnabled(chain, false /* enable */);
+ mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
+ }
+ }, NETWORK_SETTINGS);
+ }
+
+ private void doTestFirewallBlockingAllowRule(final int chain) {
+ runWithShellPermissionIdentity(() -> {
+ try (DatagramSocket srcSock = new DatagramSocket();
+ DatagramSocket dstSock = new DatagramSocket()) {
+ dstSock.setSoTimeout(SOCKET_TIMEOUT_MS);
+
+ // No global config, No uid config
+ checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+ // Has global config, No uid config
+ mCm.setFirewallChainEnabled(chain, true /* enable */);
+ checkFirewallBlocking(srcSock, dstSock, EXPECT_BLOCK);
+
+ // Has global config, Has uid config
+ mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_ALLOW);
+ checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+ // No global config, Has uid config
+ mCm.setFirewallChainEnabled(chain, false /* enable */);
+ checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+
+ // No global config, No uid config
+ mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
+ checkFirewallBlocking(srcSock, dstSock, EXPECT_PASS);
+ } finally {
+ mCm.setFirewallChainEnabled(chain, false /* enable */);
+ mCm.setUidFirewallRule(chain, Process.myUid(), FIREWALL_RULE_DENY);
+ }
+ }, NETWORK_SETTINGS);
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testFirewallBlocking() {
+ // Following tests affect the actual state of networking on the device after the test.
+ // This might cause unexpected behaviour of the device. So, we skip them for now.
+ // We will enable following tests after adding the logic of firewall state restoring.
+ // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_DOZABLE);
+ // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_POWERSAVE);
+ // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_RESTRICTED);
+ // doTestFirewallBlockingAllowRule(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+
+ // doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_STANDBY);
+ doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_1);
+ doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_2);
+ doTestFirewallBlockingDenyRule(FIREWALL_CHAIN_OEM_DENY_3);
+ }
+
+ private void assumeTestSApis() {
+ // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+ // shims, and @IgnoreUpTo does not check that.
+ assumeTrue(TestUtils.shouldTestSApis());
+ }
+
private void unregisterRegisteredCallbacks() {
for (NetworkCallback callback: mRegisteredCallbacks) {
mCm.unregisterNetworkCallback(callback);
diff --git a/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt b/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
new file mode 100644
index 0000000..d31a4e0
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.provider.DeviceConfig
+import android.util.Log
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+private val TAG = DeviceConfigRule::class.simpleName
+
+/**
+ * A [TestRule] that helps set [DeviceConfig] for tests and clean up the test configuration
+ * automatically on teardown.
+ *
+ * The rule can also optionally retry tests when they fail following an external change of
+ * DeviceConfig before S; this typically happens because device config flags are synced while the
+ * test is running, and DisableConfigSyncTargetPreparer is only usable starting from S.
+ *
+ * @param retryCountBeforeSIfConfigChanged if > 0, when the test fails before S, check if
+ * the configs that were set through this rule were changed, and retry the test
+ * up to the specified number of times if yes.
+ */
+class DeviceConfigRule @JvmOverloads constructor(
+ val retryCountBeforeSIfConfigChanged: Int = 0
+) : TestRule {
+ // Maps (namespace, key) -> value
+ private val originalConfig = mutableMapOf<Pair<String, String>, String?>()
+ private val usedConfig = mutableMapOf<Pair<String, String>, String?>()
+
+ /**
+ * Actions to be run after cleanup of the config, for the current test only.
+ */
+ private val currentTestCleanupActions = mutableListOf<Runnable>()
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return TestValidationUrlStatement(base, description)
+ }
+
+ private inner class TestValidationUrlStatement(
+ private val base: Statement,
+ private val description: Description
+ ) : Statement() {
+ override fun evaluate() {
+ var retryCount = if (SdkLevel.isAtLeastS()) 1 else retryCountBeforeSIfConfigChanged + 1
+ while (retryCount > 0) {
+ retryCount--
+ tryTest {
+ base.evaluate()
+ // Can't use break/return out of a loop here because this is a tryTest lambda,
+ // so set retryCount to exit instead
+ retryCount = 0
+ }.catch<Throwable> { e -> // junit AssertionFailedError does not extend Exception
+ if (retryCount == 0) throw e
+ usedConfig.forEach { (key, value) ->
+ val currentValue = runAsShell(READ_DEVICE_CONFIG) {
+ DeviceConfig.getProperty(key.first, key.second)
+ }
+ if (currentValue != value) {
+ Log.w(TAG, "Test failed with unexpected device config change, retrying")
+ return@catch
+ }
+ }
+ throw e
+ } cleanupStep {
+ runAsShell(WRITE_DEVICE_CONFIG) {
+ originalConfig.forEach { (key, value) ->
+ DeviceConfig.setProperty(
+ key.first, key.second, value, false /* makeDefault */)
+ }
+ }
+ } cleanupStep {
+ originalConfig.clear()
+ usedConfig.clear()
+ } cleanup {
+ currentTestCleanupActions.forEach { it.run() }
+ currentTestCleanupActions.clear()
+ }
+ }
+ }
+ }
+
+ /**
+ * Set a configuration key/value. After the test case ends, it will be restored to the value it
+ * had when this method was first called.
+ */
+ fun setConfig(namespace: String, key: String, value: String?) {
+ runAsShell(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG) {
+ val keyPair = Pair(namespace, key)
+ if (!originalConfig.containsKey(keyPair)) {
+ originalConfig[keyPair] = DeviceConfig.getProperty(namespace, key)
+ }
+ usedConfig[keyPair] = value
+ DeviceConfig.setProperty(namespace, key, value, false /* makeDefault */)
+ }
+ }
+
+ /**
+ * Add an action to be run after config cleanup when the current test case ends.
+ */
+ fun runAfterNextCleanup(action: Runnable) {
+ currentTestCleanupActions.add(action)
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index c6fc38f..0c53411 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -49,6 +49,7 @@
import android.os.Handler;
import android.os.Looper;
import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
import android.system.ErrnoException;
import android.util.Log;
@@ -727,6 +728,18 @@
@Test
public void testPrivateDnsBypass() throws InterruptedException {
+ final String dataStallSetting = Settings.Global.getString(mCR,
+ Settings.Global.DATA_STALL_RECOVERY_ON_BAD_NETWORK);
+ Settings.Global.putInt(mCR, Settings.Global.DATA_STALL_RECOVERY_ON_BAD_NETWORK, 0);
+ try {
+ doTestPrivateDnsBypass();
+ } finally {
+ Settings.Global.putString(mCR, Settings.Global.DATA_STALL_RECOVERY_ON_BAD_NETWORK,
+ dataStallSetting);
+ }
+ }
+
+ private void doTestPrivateDnsBypass() throws InterruptedException {
final Network[] testNetworks = getTestableNetworks();
// Set an invalid private DNS server
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index e8add6b..621b743 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -48,9 +48,11 @@
import android.platform.test.annotations.AppModeFull
import android.system.Os
import android.system.OsConstants.AF_INET
+import android.system.OsConstants.AF_INET6
import android.system.OsConstants.IPPROTO_UDP
import android.system.OsConstants.SOCK_DGRAM
import android.system.OsConstants.SOCK_NONBLOCK
+import android.util.Log
import android.util.Range
import androidx.test.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
@@ -71,6 +73,8 @@
import org.junit.Test
import org.junit.runner.RunWith
import java.net.Inet4Address
+import java.net.Inet6Address
+import java.net.InetAddress
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.regex.Pattern
@@ -81,6 +85,9 @@
private const val MAX_PACKET_LENGTH = 1500
+private const val IP4_PREFIX_LEN = 32
+private const val IP6_PREFIX_LEN = 128
+
private val instrumentation: Instrumentation
get() = InstrumentationRegistry.getInstrumentation()
@@ -97,6 +104,9 @@
private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
private val TEST_TARGET_IPV4_ADDR =
InetAddresses.parseNumericAddress("8.8.8.8") as Inet4Address
+ private val LOCAL_IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::1")
+ private val TEST_TARGET_IPV6_ADDR =
+ InetAddresses.parseNumericAddress("2001:4860:4860::8888") as Inet6Address
private val realContext = InstrumentationRegistry.getContext()
private val cm = realContext.getSystemService(ConnectivityManager::class.java)
@@ -126,13 +136,15 @@
@Before
fun setUp() {
- // For BPF support kernel needs to be at least 5.4.
- assumeTrue(kernelIsAtLeast(5, 4))
+ // For BPF support kernel needs to be at least 5.15.
+ assumeTrue(kernelIsAtLeast(5, 15))
runAsShell(MANAGE_TEST_NETWORKS) {
val tnm = realContext.getSystemService(TestNetworkManager::class.java)
- iface = tnm.createTunInterface(Array(1) { LinkAddress(LOCAL_IPV4_ADDRESS, 32) })
+ iface = tnm.createTunInterface(arrayOf(
+ LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN),
+ LinkAddress(LOCAL_IPV6_ADDRESS, IP6_PREFIX_LEN)))
assertNotNull(iface)
}
@@ -146,7 +158,7 @@
@After
fun tearDown() {
- if (!kernelIsAtLeast(5, 4)) {
+ if (!kernelIsAtLeast(5, 15)) {
return;
}
agentsToCleanUp.forEach { it.unregister() }
@@ -154,6 +166,8 @@
// reader.stop() cleans up tun fd
reader.handler.post { reader.stop() }
+ if (iface.fileDescriptor.fileDescriptor != null)
+ Os.close(iface.fileDescriptor.fileDescriptor)
handlerThread.quitSafely()
}
@@ -196,9 +210,11 @@
}
}
val lp = LinkProperties().apply {
- addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, IP4_PREFIX_LEN))
+ addLinkAddress(LinkAddress(LOCAL_IPV6_ADDRESS, IP6_PREFIX_LEN))
addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
- setInterfaceName(iface.getInterfaceName())
+ addRoute(RouteInfo(InetAddress.getByName("fe80::1234")))
+ setInterfaceName(specifier)
}
val config = NetworkAgentConfig.Builder().build()
val agent = TestableNetworkAgent(context, handlerThread.looper, nc, lp, config)
@@ -218,47 +234,114 @@
eachByte -> "%02x".format(eachByte)
}
- fun checkDscpValue(
+ fun sendPacket(
agent: TestableNetworkAgent,
- callback: TestableNetworkCallback,
- dscpValue: Int = 0,
- dstPort: Int = 0
+ sendV6: Boolean,
+ dstPort: Int = 0,
) {
val testString = "test string"
val testPacket = ByteBuffer.wrap(testString.toByteArray(Charsets.UTF_8))
var packetFound = false
- val socket = Os.socket(AF_INET, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_UDP)
+ val socket = Os.socket(if (sendV6) AF_INET6 else AF_INET, SOCK_DGRAM or SOCK_NONBLOCK,
+ IPPROTO_UDP)
agent.network.bindSocket(socket)
val originalPacket = testPacket.readAsArray()
- Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size,
- 0 /* flags */, TEST_TARGET_IPV4_ADDR, dstPort)
-
+ Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size, 0 /* flags */,
+ if(sendV6) TEST_TARGET_IPV6_ADDR else TEST_TARGET_IPV4_ADDR, dstPort)
Os.close(socket)
+ }
+
+ fun parseV4PacketDscp(buffer : ByteBuffer) : Int {
+ val ip_ver = buffer.get()
+ val tos = buffer.get()
+ val length = buffer.getShort()
+ val id = buffer.getShort()
+ val offset = buffer.getShort()
+ val ttl = buffer.get()
+ val ipType = buffer.get()
+ val checksum = buffer.getShort()
+ return tos.toInt().shr(2)
+ }
+
+ fun parseV6PacketDscp(buffer : ByteBuffer) : Int {
+ val ip_ver = buffer.get()
+ val tc = buffer.get()
+ val fl = buffer.getShort()
+ val length = buffer.getShort()
+ val proto = buffer.get()
+ val hop = buffer.get()
+ // DSCP is bottom 4 bits of ip_ver and top 2 of tc.
+ val ip_ver_bottom = ip_ver.toInt().and(0xf)
+ val tc_dscp = tc.toInt().shr(6)
+ return ip_ver_bottom.toInt().shl(2) + tc_dscp
+ }
+
+ fun parsePacketIp(
+ buffer : ByteBuffer,
+ sendV6 : Boolean,
+ ) : Boolean {
+ val ipAddr = if (sendV6) ByteArray(16) else ByteArray(4)
+ buffer.get(ipAddr)
+ val srcIp = if (sendV6) Inet6Address.getByAddress(ipAddr)
+ else Inet4Address.getByAddress(ipAddr)
+ buffer.get(ipAddr)
+ val dstIp = if (sendV6) Inet6Address.getByAddress(ipAddr)
+ else Inet4Address.getByAddress(ipAddr)
+
+ Log.e(TAG, "IP Src:" + srcIp + " dst: " + dstIp)
+
+ if ((sendV6 && srcIp == LOCAL_IPV6_ADDRESS && dstIp == TEST_TARGET_IPV6_ADDR) ||
+ (!sendV6 && srcIp == LOCAL_IPV4_ADDRESS && dstIp == TEST_TARGET_IPV4_ADDR)) {
+ Log.e(TAG, "IP return true");
+ return true
+ }
+ Log.e(TAG, "IP return false");
+ return false
+ }
+
+ fun parsePacketPort(
+ buffer : ByteBuffer,
+ srcPort : Int,
+ dstPort : Int
+ ) : Boolean {
+ if (srcPort == 0 && dstPort == 0) return true
+
+ val packetSrcPort = buffer.getShort().toInt()
+ val packetDstPort = buffer.getShort().toInt()
+
+ Log.e(TAG, "Port Src:" + packetSrcPort + " dst: " + packetDstPort)
+
+ if ((srcPort == 0 || (srcPort != 0 && srcPort == packetSrcPort)) &&
+ (dstPort == 0 || (dstPort != 0 && dstPort == packetDstPort))) {
+ Log.e(TAG, "Port return true");
+ return true
+ }
+ Log.e(TAG, "Port return false");
+ return false
+ }
+
+ fun validatePacket(
+ agent : TestableNetworkAgent,
+ sendV6 : Boolean = false,
+ dscpValue : Int = 0,
+ dstPort : Int = 0,
+ ) {
+ var packetFound = false;
+ sendPacket(agent, sendV6, dstPort)
+ // TODO: grab source port from socket in sendPacket
+
+ Log.e(TAG, "find DSCP value:" + dscpValue)
generateSequence { reader.poll(PACKET_TIMEOUT_MS) }.forEach { packet ->
val buffer = ByteBuffer.wrap(packet, 0, packet.size).order(ByteOrder.BIG_ENDIAN)
- val ip_ver = buffer.get()
- val tos = buffer.get()
- val length = buffer.getShort()
- val id = buffer.getShort()
- val offset = buffer.getShort()
- val ttl = buffer.get()
- val ipType = buffer.get()
- val checksum = buffer.getShort()
+ val dscp = if (sendV6) parseV6PacketDscp(buffer) else parseV4PacketDscp(buffer)
+ Log.e(TAG, "DSCP value:" + dscp)
- val ipAddr = ByteArray(4)
- buffer.get(ipAddr)
- val srcIp = Inet4Address.getByAddress(ipAddr)
- buffer.get(ipAddr)
- val dstIp = Inet4Address.getByAddress(ipAddr)
- val packetSrcPort = buffer.getShort().toInt()
- val packetDstPort = buffer.getShort().toInt()
-
- // TODO: Add source port comparison.
- if (srcIp == LOCAL_IPV4_ADDRESS && dstIp == TEST_TARGET_IPV4_ADDR &&
- packetDstPort == dstPort) {
- assertEquals(dscpValue, (tos.toInt().shr(2)))
+ // TODO: Add source port comparison. Use 0 for now.
+ if (parsePacketIp(buffer, sendV6) && parsePacketPort(buffer, 0, dstPort)) {
+ Log.e(TAG, "DSCP value found")
+ assertEquals(dscpValue, dscp)
packetFound = true
}
}
@@ -275,12 +358,12 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(policyId, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- checkDscpValue(agent, callback, dstPort = portNumber)
}
}
@Test
- fun testDscpPolicyAddPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+ fun testDscpPolicyAddPolicies(): Unit = createConnectedNetworkAgent().let {
+ (agent, callback) ->
val policy = DscpPolicy.Builder(1, 1)
.setDestinationPortRange(Range(4444, 4444)).build()
agent.sendAddDscpPolicy(policy)
@@ -288,8 +371,7 @@
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
-
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444)
+ validatePacket(agent, dscpValue = 1, dstPort = 4444)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -298,15 +380,54 @@
}
val policy2 = DscpPolicy.Builder(1, 4)
- .setDestinationPortRange(Range(5555, 5555)).setSourceAddress(LOCAL_IPV4_ADDRESS)
- .setDestinationAddress(TEST_TARGET_IPV4_ADDR).setProtocol(IPPROTO_UDP).build()
+ .setDestinationPortRange(Range(5555, 5555))
+ .setDestinationAddress(TEST_TARGET_IPV4_ADDR)
+ .setSourceAddress(LOCAL_IPV4_ADDRESS)
+ .setProtocol(IPPROTO_UDP).build()
agent.sendAddDscpPolicy(policy2)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
}
- checkDscpValue(agent, callback, dscpValue = 4, dstPort = 5555)
+ validatePacket(agent, dscpValue = 4, dstPort = 5555)
+
+ agent.sendRemoveDscpPolicy(1)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
+ }
+ }
+
+ @Test
+ fun testDscpPolicyAddV6Policies(): Unit = createConnectedNetworkAgent().let {
+ (agent, callback) ->
+ val policy = DscpPolicy.Builder(1, 1)
+ .setDestinationPortRange(Range(4444, 4444)).build()
+ agent.sendAddDscpPolicy(policy)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+ }
+ validatePacket(agent, true, dscpValue = 1, dstPort = 4444)
+
+ agent.sendRemoveDscpPolicy(1)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
+ }
+
+ val policy2 = DscpPolicy.Builder(1, 4)
+ .setDestinationPortRange(Range(5555, 5555))
+ .setDestinationAddress(TEST_TARGET_IPV6_ADDR)
+ .setSourceAddress(LOCAL_IPV6_ADDRESS)
+ .setProtocol(IPPROTO_UDP).build()
+ agent.sendAddDscpPolicy(policy2)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
+ }
+ validatePacket(agent, true, dscpValue = 4, dstPort = 5555)
agent.sendRemoveDscpPolicy(1)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -324,7 +445,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+ validatePacket(agent, dscpValue = 1, dstPort = 1111)
}
val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -332,7 +453,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+ validatePacket(agent, dscpValue = 1, dstPort = 2222)
}
val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -340,13 +461,16 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+ validatePacket(agent, dscpValue = 1, dstPort = 3333)
}
/* Remove Policies and check CE is no longer set */
doRemovePolicyTest(agent, callback, 1)
+ validatePacket(agent, dscpValue = 0, dstPort = 1111)
doRemovePolicyTest(agent, callback, 2)
+ validatePacket(agent, dscpValue = 0, dstPort = 2222)
doRemovePolicyTest(agent, callback, 3)
+ validatePacket(agent, dscpValue = 0, dstPort = 3333)
}
@Test
@@ -357,7 +481,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+ validatePacket(agent, dscpValue = 1, dstPort = 1111)
}
doRemovePolicyTest(agent, callback, 1)
@@ -366,7 +490,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+ validatePacket(agent, dscpValue = 1, dstPort = 2222)
}
doRemovePolicyTest(agent, callback, 2)
@@ -375,7 +499,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+ validatePacket(agent, dscpValue = 1, dstPort = 3333)
}
doRemovePolicyTest(agent, callback, 3)
}
@@ -389,7 +513,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+ validatePacket(agent, dscpValue = 1, dstPort = 1111)
}
val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
@@ -397,7 +521,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+ validatePacket(agent, dscpValue = 1, dstPort = 2222)
}
val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
@@ -405,7 +529,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+ validatePacket(agent, dscpValue = 1, dstPort = 3333)
}
/* Remove Policies and check CE is no longer set */
@@ -423,14 +547,15 @@
}
@Test
- fun testRemoveAllDscpPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+ fun testRemoveAllDscpPolicies(): Unit = createConnectedNetworkAgent().let {
+ (agent, callback) ->
val policy = DscpPolicy.Builder(1, 1)
.setDestinationPortRange(Range(1111, 1111)).build()
agent.sendAddDscpPolicy(policy)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+ validatePacket(agent, dscpValue = 1, dstPort = 1111)
}
val policy2 = DscpPolicy.Builder(2, 1)
@@ -439,7 +564,7 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+ validatePacket(agent, dscpValue = 1, dstPort = 2222)
}
val policy3 = DscpPolicy.Builder(3, 1)
@@ -448,24 +573,24 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+ validatePacket(agent, dscpValue = 1, dstPort = 3333)
}
agent.sendRemoveAllDscpPolicies()
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- checkDscpValue(agent, callback, dstPort = 1111)
+ validatePacket(agent, false, dstPort = 1111)
}
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(2, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- checkDscpValue(agent, callback, dstPort = 2222)
+ validatePacket(agent, false, dstPort = 2222)
}
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(3, it.policyId)
assertEquals(DSCP_POLICY_STATUS_DELETED, it.status)
- checkDscpValue(agent, callback, dstPort = 3333)
+ validatePacket(agent, false, dstPort = 3333)
}
}
@@ -477,12 +602,9 @@
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
assertEquals(1, it.policyId)
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444)
+ validatePacket(agent, dscpValue = 1, dstPort = 4444)
}
- // TODO: send packet on socket and confirm that changing the DSCP policy
- // updates the mark to the new value.
-
val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(5555, 5555)).build()
agent.sendAddDscpPolicy(policy2)
agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
@@ -490,8 +612,8 @@
assertEquals(DSCP_POLICY_STATUS_SUCCESS, it.status)
// Sending packet with old policy should fail
- checkDscpValue(agent, callback, dstPort = 4444)
- checkDscpValue(agent, callback, dscpValue = 1, dstPort = 5555)
+ validatePacket(agent, dscpValue = 0, dstPort = 4444)
+ validatePacket(agent, dscpValue = 1, dstPort = 5555)
}
agent.sendRemoveDscpPolicy(1)
@@ -504,15 +626,31 @@
@Test
fun testParcelingDscpPolicyIsLossless(): Unit = createConnectedNetworkAgent().let {
(agent, callback) ->
+ val policyId = 1
+ val dscpValue = 1
+ val range = Range(4444, 4444)
+ val srcPort = 555
+
// Check that policy with partial parameters is lossless.
- val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)).build()
+ val policy = DscpPolicy.Builder(policyId, dscpValue).setDestinationPortRange(range).build()
+ assertEquals(policyId, policy.policyId)
+ assertEquals(dscpValue, policy.dscpValue)
+ assertEquals(range, policy.destinationPortRange)
assertParcelingIsLossless(policy)
// Check that policy with all parameters is lossless.
- val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444))
+ val policy2 = DscpPolicy.Builder(policyId, dscpValue).setDestinationPortRange(range)
.setSourceAddress(LOCAL_IPV4_ADDRESS)
.setDestinationAddress(TEST_TARGET_IPV4_ADDR)
+ .setSourcePort(srcPort)
.setProtocol(IPPROTO_UDP).build()
+ assertEquals(policyId, policy2.policyId)
+ assertEquals(dscpValue, policy2.dscpValue)
+ assertEquals(range, policy2.destinationPortRange)
+ assertEquals(TEST_TARGET_IPV4_ADDR, policy2.destinationAddress)
+ assertEquals(LOCAL_IPV4_ADDRESS, policy2.sourceAddress)
+ assertEquals(srcPort, policy2.sourcePort)
+ assertEquals(IPPROTO_UDP, policy2.protocol)
assertParcelingIsLossless(policy2)
}
}
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 30e0015..1748612 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -15,83 +15,126 @@
*/
package android.net.cts
+import android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS
import android.Manifest.permission.MANAGE_TEST_NETWORKS
import android.Manifest.permission.NETWORK_SETTINGS
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.EthernetManager
+import android.net.EthernetManager.InterfaceStateListener
+import android.net.EthernetManager.ROLE_CLIENT
+import android.net.EthernetManager.ROLE_NONE
+import android.net.EthernetManager.ROLE_SERVER
+import android.net.EthernetManager.STATE_ABSENT
+import android.net.EthernetManager.STATE_LINK_DOWN
+import android.net.EthernetManager.STATE_LINK_UP
+import android.net.EthernetManager.TetheredInterfaceCallback
+import android.net.EthernetManager.TetheredInterfaceRequest
+import android.net.EthernetNetworkManagementException
+import android.net.EthernetNetworkSpecifier
+import android.net.EthernetNetworkUpdateRequest
import android.net.InetAddresses
import android.net.IpConfiguration
import android.net.MacAddress
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkRequest
import android.net.TestNetworkInterface
import android.net.TestNetworkManager
+import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.os.OutcomeReceiver
import android.platform.test.annotations.AppModeFull
+import android.util.ArraySet
import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.runner.AndroidJUnit4
import com.android.net.module.util.ArrayTrackRecord
import com.android.net.module.util.TrackRecord
+import com.android.testutils.anyNetwork
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast
import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.SC_V2
-import com.android.testutils.runAsShell
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import android.content.Context
-import org.junit.runner.RunWith
-import kotlin.test.assertNull
-import kotlin.test.fail
-import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
-import android.os.Handler
-import android.os.HandlerExecutor
-import android.os.Looper
-import com.android.networkstack.apishim.common.EthernetManagerShim.InterfaceStateListener
-import com.android.networkstack.apishim.common.EthernetManagerShim.STATE_ABSENT
-import com.android.networkstack.apishim.common.EthernetManagerShim.STATE_LINK_DOWN
-import com.android.networkstack.apishim.common.EthernetManagerShim.STATE_LINK_UP
-import com.android.networkstack.apishim.common.EthernetManagerShim.ROLE_CLIENT
-import com.android.networkstack.apishim.common.EthernetManagerShim.ROLE_NONE
-import com.android.networkstack.apishim.EthernetManagerShimImpl
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.RouterAdvertisementResponder
import com.android.testutils.TapPacketReader
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
import com.android.testutils.waitForIdle
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
import java.net.Inet6Address
-import java.util.concurrent.Executor
-import kotlin.test.assertFalse
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeoutException
+import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
import kotlin.test.assertTrue
-import java.net.NetworkInterface
+import kotlin.test.fail
-private const val TIMEOUT_MS = 1000L
+// TODO: try to lower this timeout in the future. Currently, ethernet tests are still flaky because
+// the interface is not ready fast enough (mostly due to the up / up / down / up issue).
+private const val TIMEOUT_MS = 2000L
private const val NO_CALLBACK_TIMEOUT_MS = 200L
private val DEFAULT_IP_CONFIGURATION = IpConfiguration(IpConfiguration.IpAssignment.DHCP,
IpConfiguration.ProxySettings.NONE, null, null)
+private val ETH_REQUEST: NetworkRequest = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_TEST)
+ .addTransportType(TRANSPORT_ETHERNET)
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .build()
@AppModeFull(reason = "Instant apps can't access EthernetManager")
-@RunWith(AndroidJUnit4::class)
+// EthernetManager is not updatable before T, so tests do not need to be backwards compatible.
+@RunWith(DevSdkIgnoreRunner::class)
+// This test depends on behavior introduced post-T as part of connectivity module updates
+@ConnectivityModuleTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class EthernetManagerTest {
- // EthernetManager is not updatable before T, so tests do not need to be backwards compatible
- @get:Rule
- val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
- private val em by lazy { EthernetManagerShimImpl.newInstance(context) }
+ private val em by lazy { context.getSystemService(EthernetManager::class.java) }
+ private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+ private val handler by lazy { Handler(Looper.getMainLooper()) }
+ private val ifaceListener = EthernetStateListener()
private val createdIfaces = ArrayList<EthernetTestInterface>()
- private val addedListeners = ArrayList<InterfaceStateListener>()
+ private val addedListeners = ArrayList<EthernetStateListener>()
+ private val registeredCallbacks = ArrayList<TestableNetworkCallback>()
+
+ private var tetheredInterfaceRequest: TetheredInterfaceRequest? = null
private class EthernetTestInterface(
context: Context,
- private val handler: Handler
+ private val handler: Handler,
+ hasCarrier: Boolean
) {
private val tapInterface: TestNetworkInterface
private val packetReader: TapPacketReader
private val raResponder: RouterAdvertisementResponder
- val interfaceName get() = tapInterface.interfaceName
+ private val tnm: TestNetworkManager
+ val name get() = tapInterface.interfaceName
init {
- tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
- val tnm = context.getSystemService(TestNetworkManager::class.java)
- tnm.createTapInterface(false /* bringUp */)
+ tnm = runAsShell(MANAGE_TEST_NETWORKS) {
+ context.getSystemService(TestNetworkManager::class.java)
}
- val mtu = NetworkInterface.getByName(tapInterface.interfaceName).getMTU()
+ tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
+ tnm.createTapInterface(hasCarrier, false /* bringUp */)
+ }
+ val mtu = tapInterface.mtu
packetReader = TapPacketReader(handler, tapInterface.fileDescriptor.fileDescriptor, mtu)
raResponder = RouterAdvertisementResponder(packetReader)
raResponder.addRouterEntry(MacAddress.fromString("01:23:45:67:89:ab"),
@@ -101,6 +144,14 @@
raResponder.start()
}
+ // WARNING: this function requires kernel support. Call assumeChangingCarrierSupported() at
+ // the top of your test.
+ fun setCarrierEnabled(enabled: Boolean) {
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ tnm.setCarrierEnabled(tapInterface, enabled)
+ }
+ }
+
fun destroy() {
raResponder.stop()
handler.post({ packetReader.stop() })
@@ -141,23 +192,90 @@
}
fun expectCallback(iface: EthernetTestInterface, state: Int, role: Int) {
- expectCallback(InterfaceStateChanged(iface.interfaceName, state, role,
- if (state != STATE_ABSENT) DEFAULT_IP_CONFIGURATION else null))
+ expectCallback(createChangeEvent(iface.name, state, role))
}
+ fun createChangeEvent(iface: String, state: Int, role: Int) =
+ InterfaceStateChanged(iface, state, role,
+ if (state != STATE_ABSENT) DEFAULT_IP_CONFIGURATION else null)
+
fun pollForNextCallback(): CallbackEntry {
return events.poll(TIMEOUT_MS) ?: fail("Did not receive callback after ${TIMEOUT_MS}ms")
}
+ fun eventuallyExpect(expected: CallbackEntry) = events.poll(TIMEOUT_MS) { it == expected }
+
+ fun eventuallyExpect(iface: EthernetTestInterface, state: Int, role: Int) {
+ assertNotNull(eventuallyExpect(createChangeEvent(iface.name, state, role)))
+ }
+
fun assertNoCallback() {
val cb = events.poll(NO_CALLBACK_TIMEOUT_MS)
assertNull(cb, "Expected no callback but got $cb")
}
}
+ private class TetheredInterfaceListener : TetheredInterfaceCallback {
+ private val available = CompletableFuture<String>()
+
+ override fun onAvailable(iface: String) {
+ available.complete(iface)
+ }
+
+ override fun onUnavailable() {
+ available.completeExceptionally(IllegalStateException("onUnavailable was called"))
+ }
+
+ fun expectOnAvailable(): String {
+ return available.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ }
+
+ fun expectOnUnavailable() {
+ // Assert that the future fails with the IllegalStateException from the
+ // completeExceptionally() call inside onUnavailable.
+ assertFailsWith(IllegalStateException::class) {
+ try {
+ available.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ } catch (e: ExecutionException) {
+ throw e.cause!!
+ }
+ }
+ }
+ }
+
+ private class EthernetOutcomeReceiver :
+ OutcomeReceiver<String, EthernetNetworkManagementException> {
+ private val result = CompletableFuture<String>()
+
+ override fun onResult(iface: String) {
+ result.complete(iface)
+ }
+
+ override fun onError(e: EthernetNetworkManagementException) {
+ result.completeExceptionally(e)
+ }
+
+ fun expectResult(expected: String) {
+ assertEquals(expected, result.get(TIMEOUT_MS, TimeUnit.MILLISECONDS))
+ }
+
+ fun expectError() {
+ // Assert that the future fails with EthernetNetworkManagementException from the
+ // completeExceptionally() call inside onUnavailable.
+ assertFailsWith(EthernetNetworkManagementException::class) {
+ try {
+ result.get()
+ } catch (e: ExecutionException) {
+ throw e.cause!!
+ }
+ }
+ }
+ }
+
@Before
fun setUp() {
setIncludeTestInterfaces(true)
+ addInterfaceStateListener(ifaceListener)
}
@After
@@ -165,19 +283,40 @@
setIncludeTestInterfaces(false)
for (iface in createdIfaces) {
iface.destroy()
+ ifaceListener.eventuallyExpect(iface, STATE_ABSENT, ROLE_NONE)
}
for (listener in addedListeners) {
em.removeInterfaceStateListener(listener)
}
+ registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
+ releaseTetheredInterface()
}
- private fun addInterfaceStateListener(executor: Executor, listener: InterfaceStateListener) {
- em.addInterfaceStateListener(executor, listener)
+ // Setting the carrier up / down relies on TUNSETCARRIER which was added in kernel version 5.0.
+ private fun assumeChangingCarrierSupported() = assumeTrue(isKernelVersionAtLeast("5.0.0"))
+
+ private fun addInterfaceStateListener(listener: EthernetStateListener) {
+ runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) {
+ em.addInterfaceStateListener(handler::post, listener)
+ }
addedListeners.add(listener)
}
- private fun createInterface(): EthernetTestInterface {
- return EthernetTestInterface(context, Handler(Looper.getMainLooper()))
+ // WARNING: setting hasCarrier to false requires kernel support. Call
+ // assumeChangingCarrierSupported() at the top of your test.
+ private fun createInterface(hasCarrier: Boolean = true): EthernetTestInterface {
+ val iface = EthernetTestInterface(
+ context,
+ handler,
+ hasCarrier
+ ).also { createdIfaces.add(it) }
+
+ // when an interface comes up, we should always see a down cb before an up cb.
+ ifaceListener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+ if (hasCarrier) {
+ ifaceListener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+ }
+ return iface
}
private fun setIncludeTestInterfaces(value: Boolean) {
@@ -189,56 +328,357 @@
private fun removeInterface(iface: EthernetTestInterface) {
iface.destroy()
createdIfaces.remove(iface)
+ ifaceListener.eventuallyExpect(iface, STATE_ABSENT, ROLE_NONE)
}
- @Test
- public fun testCallbacks() {
- val executor = HandlerExecutor(Handler(Looper.getMainLooper()))
+ private fun requestNetwork(request: NetworkRequest): TestableNetworkCallback {
+ return TestableNetworkCallback().also {
+ cm.requestNetwork(request, it)
+ registeredCallbacks.add(it)
+ }
+ }
+ private fun registerNetworkListener(request: NetworkRequest): TestableNetworkCallback {
+ return TestableNetworkCallback().also {
+ cm.registerNetworkCallback(request, it)
+ registeredCallbacks.add(it)
+ }
+ }
+
+ private fun requestTetheredInterface() = TetheredInterfaceListener().also {
+ tetheredInterfaceRequest = runAsShell(NETWORK_SETTINGS) {
+ em.requestTetheredInterface(handler::post, it)
+ }
+ }
+
+ private fun releaseTetheredInterface() {
+ runAsShell(NETWORK_SETTINGS) {
+ tetheredInterfaceRequest?.release()
+ tetheredInterfaceRequest = null
+ }
+ }
+
+ private fun releaseRequest(cb: TestableNetworkCallback) {
+ cm.unregisterNetworkCallback(cb)
+ registeredCallbacks.remove(cb)
+ }
+
+ private fun disableInterface(iface: EthernetTestInterface) = EthernetOutcomeReceiver().also {
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ em.disableInterface(iface.name, handler::post, it)
+ }
+ }
+
+ private fun enableInterface(iface: EthernetTestInterface) = EthernetOutcomeReceiver().also {
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ em.enableInterface(iface.name, handler::post, it)
+ }
+ }
+
+ private fun updateConfiguration(
+ iface: EthernetTestInterface,
+ ipConfig: IpConfiguration? = null,
+ capabilities: NetworkCapabilities? = null
+ ) = EthernetOutcomeReceiver().also {
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ em.updateConfiguration(
+ iface.name,
+ EthernetNetworkUpdateRequest.Builder()
+ .setIpConfiguration(ipConfig)
+ .setNetworkCapabilities(capabilities).build(),
+ handler::post,
+ it)
+ }
+ }
+
+ // NetworkRequest.Builder does not create a copy of the passed NetworkRequest, so in order to
+ // keep ETH_REQUEST as it is, a defensive copy is created here.
+ private fun NetworkRequest.createCopyWithEthernetSpecifier(ifaceName: String) =
+ NetworkRequest.Builder(NetworkRequest(ETH_REQUEST))
+ .setNetworkSpecifier(EthernetNetworkSpecifier(ifaceName)).build()
+
+ // It can take multiple seconds for the network to become available.
+ private fun TestableNetworkCallback.expectAvailable() =
+ expectCallback<Available>(anyNetwork(), 5000 /* ms timeout */).network
+
+ // b/233534110: eventuallyExpect<Lost>() does not advance ReadHead, use
+ // eventuallyExpect(Lost::class) instead.
+ private fun TestableNetworkCallback.eventuallyExpectLost(n: Network? = null) =
+ eventuallyExpect(Lost::class, TIMEOUT_MS) { n?.equals(it.network) ?: true }
+
+ private fun TestableNetworkCallback.assertNeverLost(n: Network? = null) =
+ assertNoCallbackThat() { it is Lost && (n?.equals(it.network) ?: true) }
+
+ private fun TestableNetworkCallback.assertNeverAvailable(n: Network? = null) =
+ assertNoCallbackThat() { it is Available && (n?.equals(it.network) ?: true) }
+
+ private fun TestableNetworkCallback.expectCapabilitiesWithInterfaceName(name: String) =
+ expectCapabilitiesThat(anyNetwork()) {
+ it.networkSpecifier == EthernetNetworkSpecifier(name)
+ }
+
+ @Test
+ fun testCallbacks() {
// If an interface exists when the callback is registered, it is reported on registration.
val iface = createInterface()
- val listener = EthernetStateListener()
- addInterfaceStateListener(executor, listener)
- listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+ val listener1 = EthernetStateListener()
+ addInterfaceStateListener(listener1)
+ validateListenerOnRegistration(listener1)
// If an interface appears, existing callbacks see it.
// TODO: fix the up/up/down/up callbacks and only send down/up.
val iface2 = createInterface()
- listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
- listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
- listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
- listener.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+ listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+ listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+ listener1.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
+ listener1.expectCallback(iface2, STATE_LINK_UP, ROLE_CLIENT)
+
+ // Register a new listener, it should see state of all existing interfaces immediately.
+ val listener2 = EthernetStateListener()
+ addInterfaceStateListener(listener2)
+ validateListenerOnRegistration(listener2)
// Removing interfaces first sends link down, then STATE_ABSENT/ROLE_NONE.
removeInterface(iface)
- listener.expectCallback(iface, STATE_LINK_DOWN, ROLE_CLIENT)
- listener.expectCallback(iface, STATE_ABSENT, ROLE_NONE)
+ for (listener in listOf(listener1, listener2)) {
+ listener.expectCallback(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+ listener.expectCallback(iface, STATE_ABSENT, ROLE_NONE)
+ }
removeInterface(iface2)
- listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
- listener.expectCallback(iface2, STATE_ABSENT, ROLE_NONE)
+ for (listener in listOf(listener1, listener2)) {
+ listener.expectCallback(iface2, STATE_LINK_DOWN, ROLE_CLIENT)
+ listener.expectCallback(iface2, STATE_ABSENT, ROLE_NONE)
+ listener.assertNoCallback()
+ }
+ }
+
+ private fun assumeNoInterfaceForTetheringAvailable() {
+ // Interfaces that have configured NetworkCapabilities will never be used for tethering,
+ // see aosp/2123900.
+ try {
+ // assumeException does not exist.
+ requestTetheredInterface().expectOnAvailable()
+ // interface used for tethering is available, throw an assumption error.
+ assumeTrue(false)
+ } catch (e: TimeoutException) {
+ // do nothing -- the TimeoutException indicates that no interface is available for
+ // tethering.
+ releaseTetheredInterface()
+ }
+ }
+
+ @Test
+ fun testCallbacks_forServerModeInterfaces() {
+ // do not run this test if an interface that can be used for tethering already exists.
+ assumeNoInterfaceForTetheringAvailable()
+
+ val iface = createInterface()
+ requestTetheredInterface().expectOnAvailable()
+
+ val listener = EthernetStateListener()
+ addInterfaceStateListener(listener)
+ // TODO(b/236895792): THIS IS A BUG! Existing server mode interfaces are not reported when
+ // an InterfaceStateListener is registered.
+ // Note: using eventuallyExpect as there may be other interfaces present.
+ // listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
+
+ releaseTetheredInterface()
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_CLIENT)
+
+ requestTetheredInterface().expectOnAvailable()
+ // This should be changed to expectCallback, once b/236895792 is fixed.
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
+
+ releaseTetheredInterface()
+ listener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+ }
+
+ /**
+ * Validate all interfaces are returned for an EthernetStateListener upon registration.
+ */
+ private fun validateListenerOnRegistration(listener: EthernetStateListener) {
+ // Get all tracked interfaces to validate on listener registration. Ordering and interface
+ // state (up/down) can't be validated for interfaces not created as part of testing.
+ val ifaces = em.getInterfaceList()
+ val polledIfaces = ArraySet<String>()
+ for (i in ifaces) {
+ val event = (listener.pollForNextCallback() as InterfaceStateChanged)
+ val iface = event.iface
+ assertTrue(polledIfaces.add(iface), "Duplicate interface $iface returned")
+ assertTrue(ifaces.contains(iface), "Untracked interface $iface returned")
+ // If the event's iface was created in the test, additional criteria can be validated.
+ createdIfaces.find { it.name.equals(iface) }?.let {
+ assertEquals(event, listener.createChangeEvent(it.name, STATE_LINK_UP, ROLE_CLIENT))
+ }
+ }
+ // Assert all callbacks are accounted for.
listener.assertNoCallback()
}
@Test
- public fun testGetInterfaceList() {
- setIncludeTestInterfaces(true)
-
+ fun testGetInterfaceList() {
// Create two test interfaces and check the return list contains the interface names.
val iface1 = createInterface()
val iface2 = createInterface()
var ifaces = em.getInterfaceList()
assertTrue(ifaces.size > 0)
- assertTrue(ifaces.contains(iface1.interfaceName))
- assertTrue(ifaces.contains(iface2.interfaceName))
+ assertTrue(ifaces.contains(iface1.name))
+ assertTrue(ifaces.contains(iface2.name))
// Remove one existing test interface and check the return list doesn't contain the
// removed interface name.
removeInterface(iface1)
ifaces = em.getInterfaceList()
- assertFalse(ifaces.contains(iface1.interfaceName))
- assertTrue(ifaces.contains(iface2.interfaceName))
+ assertFalse(ifaces.contains(iface1.name))
+ assertTrue(ifaces.contains(iface2.name))
removeInterface(iface2)
}
+
+ @Test
+ fun testNetworkRequest_withSingleExistingInterface() {
+ createInterface()
+
+ // install a listener which will later be used to verify the Lost callback
+ val listenerCb = registerNetworkListener(ETH_REQUEST)
+
+ val cb = requestNetwork(ETH_REQUEST)
+ val network = cb.expectAvailable()
+
+ cb.assertNeverLost()
+ releaseRequest(cb)
+ listenerCb.eventuallyExpectLost(network)
+ }
+
+ @Test
+ fun testNetworkRequest_beforeSingleInterfaceIsUp() {
+ val cb = requestNetwork(ETH_REQUEST)
+
+ // bring up interface after network has been requested.
+ // Note: there is no guarantee that the NetworkRequest has been processed before the
+ // interface is actually created. That being said, it takes a few seconds between calling
+ // createInterface and the interface actually being properly registered with the ethernet
+ // module, so it is extremely unlikely that the CS handler thread has not run until then.
+ val iface = createInterface()
+ val network = cb.expectAvailable()
+
+ // remove interface before network request has been removed
+ cb.assertNeverLost()
+ removeInterface(iface)
+ cb.eventuallyExpectLost()
+ }
+
+ @Test
+ fun testNetworkRequest_withMultipleInterfaces() {
+ val iface1 = createInterface()
+ val iface2 = createInterface()
+
+ val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.name))
+
+ val network = cb.expectAvailable()
+ cb.expectCapabilitiesWithInterfaceName(iface2.name)
+
+ removeInterface(iface1)
+ cb.assertNeverLost()
+ removeInterface(iface2)
+ cb.eventuallyExpectLost()
+ }
+
+ @Test
+ fun testNetworkRequest_withInterfaceBeingReplaced() {
+ val iface1 = createInterface()
+
+ val cb = requestNetwork(ETH_REQUEST)
+ val network = cb.expectAvailable()
+
+ // create another network and verify the request sticks to the current network
+ val iface2 = createInterface()
+ cb.assertNeverLost()
+
+ // remove iface1 and verify the request brings up iface2
+ removeInterface(iface1)
+ cb.eventuallyExpectLost(network)
+ val network2 = cb.expectAvailable()
+ }
+
+ @Test
+ fun testNetworkRequest_withMultipleInterfacesAndRequests() {
+ val iface1 = createInterface()
+ val iface2 = createInterface()
+
+ val cb1 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface1.name))
+ val cb2 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.name))
+ val cb3 = requestNetwork(ETH_REQUEST)
+
+ cb1.expectAvailable()
+ cb1.expectCapabilitiesWithInterfaceName(iface1.name)
+ cb2.expectAvailable()
+ cb2.expectCapabilitiesWithInterfaceName(iface2.name)
+ // this request can be matched by either network.
+ cb3.expectAvailable()
+
+ cb1.assertNeverLost()
+ cb2.assertNeverLost()
+ cb3.assertNeverLost()
+ }
+
+ @Test
+ fun testNetworkRequest_ensureProperRefcounting() {
+ // create first request before interface is up / exists; create another request after it has
+ // been created; release one of them and check that the network stays up.
+ val listener = registerNetworkListener(ETH_REQUEST)
+ val cb1 = requestNetwork(ETH_REQUEST)
+
+ val iface = createInterface()
+ val network = cb1.expectAvailable()
+
+ val cb2 = requestNetwork(ETH_REQUEST)
+ cb2.expectAvailable()
+
+ // release the first request; this used to trigger b/197548738
+ releaseRequest(cb1)
+
+ cb2.assertNeverLost()
+ releaseRequest(cb2)
+ listener.eventuallyExpectLost(network)
+ }
+
+ @Test
+ fun testNetworkRequest_forInterfaceWhileTogglingCarrier() {
+ assumeChangingCarrierSupported()
+
+ val iface = createInterface(false /* hasCarrier */)
+
+ val cb = requestNetwork(ETH_REQUEST)
+ cb.assertNeverAvailable()
+
+ iface.setCarrierEnabled(true)
+ cb.expectAvailable()
+
+ iface.setCarrierEnabled(false)
+ cb.eventuallyExpectLost()
+ }
+
+ @Test
+ fun testRemoveInterface_whileInServerMode() {
+ assumeNoInterfaceForTetheringAvailable()
+
+ val listener = EthernetStateListener()
+ addInterfaceStateListener(listener)
+
+ val iface = createInterface()
+ val ifaceName = requestTetheredInterface().expectOnAvailable()
+
+ assertEquals(iface.name, ifaceName)
+ listener.eventuallyExpect(iface, STATE_LINK_UP, ROLE_SERVER)
+
+ removeInterface(iface)
+
+ // Note: removeInterface already verifies that a STATE_ABSENT, ROLE_NONE callback is
+ // received, but it can't hurt to explicitly check for it.
+ listener.expectCallback(iface, STATE_ABSENT, ROLE_NONE)
+ releaseTetheredInterface()
+ listener.assertNoCallback()
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/EthernetNetworkUpdateRequestTest.java b/tests/cts/net/src/android/net/cts/EthernetNetworkUpdateRequestTest.java
new file mode 100644
index 0000000..c8ee0c7
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/EthernetNetworkUpdateRequestTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertThrows;
+
+import android.annotation.NonNull;
+import android.net.EthernetNetworkUpdateRequest;
+import android.net.IpConfiguration;
+import android.net.NetworkCapabilities;
+import android.net.StaticIpConfiguration;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+@RunWith(DevSdkIgnoreRunner.class)
+public class EthernetNetworkUpdateRequestTest {
+ private static final NetworkCapabilities DEFAULT_CAPS =
+ new NetworkCapabilities.Builder()
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED).build();
+ private static final StaticIpConfiguration DEFAULT_STATIC_IP_CONFIG =
+ new StaticIpConfiguration.Builder().setDomains("test").build();
+ private static final IpConfiguration DEFAULT_IP_CONFIG =
+ new IpConfiguration.Builder()
+ .setStaticIpConfiguration(DEFAULT_STATIC_IP_CONFIG).build();
+
+ private EthernetNetworkUpdateRequest createRequest(@NonNull final NetworkCapabilities nc,
+ @NonNull final IpConfiguration ipConfig) {
+ return new EthernetNetworkUpdateRequest.Builder()
+ .setNetworkCapabilities(nc)
+ .setIpConfiguration(ipConfig)
+ .build();
+ }
+
+ @Test
+ public void testGetNetworkCapabilities() {
+ final EthernetNetworkUpdateRequest r = createRequest(DEFAULT_CAPS, DEFAULT_IP_CONFIG);
+ assertEquals(DEFAULT_CAPS, r.getNetworkCapabilities());
+ }
+
+ @Test
+ public void testGetIpConfiguration() {
+ final EthernetNetworkUpdateRequest r = createRequest(DEFAULT_CAPS, DEFAULT_IP_CONFIG);
+ assertEquals(DEFAULT_IP_CONFIG, r.getIpConfiguration());
+ }
+
+ @Test
+ public void testBuilderWithRequest() {
+ final EthernetNetworkUpdateRequest r = createRequest(DEFAULT_CAPS, DEFAULT_IP_CONFIG);
+ final EthernetNetworkUpdateRequest rFromExisting =
+ new EthernetNetworkUpdateRequest.Builder(r).build();
+
+ assertNotSame(r, rFromExisting);
+ assertEquals(r.getIpConfiguration(), rFromExisting.getIpConfiguration());
+ assertEquals(r.getNetworkCapabilities(), rFromExisting.getNetworkCapabilities());
+ }
+
+ @Test
+ public void testNullIpConfigurationAndNetworkCapabilitiesThrows() {
+ assertThrows("Should not be able to build with null ip config and network capabilities.",
+ IllegalStateException.class,
+ () -> createRequest(null, null));
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 9590f88..2b1d173 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -20,10 +20,9 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
-import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.TestableNetworkCallbackKt.anyNetwork;
@@ -51,6 +50,7 @@
import android.net.TestNetworkInterface;
import android.net.VpnManager;
import android.net.cts.util.CtsNetUtils;
+import android.net.cts.util.IkeSessionTestUtils;
import android.net.ipsec.ike.IkeTunnelConnectionParams;
import android.os.Build;
import android.os.Process;
@@ -61,12 +61,7 @@
import com.android.internal.util.HexDump;
import com.android.networkstack.apishim.ConstantsShim;
-import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl;
-import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl;
import com.android.networkstack.apishim.VpnManagerShimImpl;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
-import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.networkstack.apishim.common.VpnManagerShim;
import com.android.networkstack.apishim.common.VpnProfileStateShim;
import com.android.testutils.DevSdkIgnoreRule;
@@ -87,6 +82,7 @@
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@@ -196,6 +192,7 @@
private final X509Certificate mServerRootCa;
private final CertificateAndKey mUserCertKey;
+ private final List<TestableNetworkCallback> mCallbacksToUnregister = new ArrayList<>();
public Ikev2VpnTest() throws Exception {
// Build certificates
@@ -205,6 +202,9 @@
@After
public void tearDown() {
+ for (TestableNetworkCallback callback : mCallbacksToUnregister) {
+ sCM.unregisterNetworkCallback(callback);
+ }
setAppop(AppOpsManager.OP_ACTIVATE_VPN, false);
setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
}
@@ -224,22 +224,17 @@
}
private Ikev2VpnProfile buildIkev2VpnProfileCommon(
- @NonNull Ikev2VpnProfileBuilderShim builderShim, boolean isRestrictedToTestNetworks,
+ @NonNull Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks,
boolean requiresValidation) throws Exception {
- builderShim.setBypassable(true)
+ builder.setBypassable(true)
.setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
.setProxy(TEST_PROXY_INFO)
.setMaxMtu(TEST_MTU)
.setMetered(false);
if (TestUtils.shouldTestTApis()) {
- builderShim.setRequiresInternetValidation(requiresValidation);
+ builder.setRequiresInternetValidation(requiresValidation);
}
-
- // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
- // method and does not defined in shims.
- // TODO: replace it in alternative way to remove the hidden method usage
- final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
if (isRestrictedToTestNetworks) {
builder.restrictToTestNetworks();
}
@@ -247,11 +242,31 @@
return builder.build();
}
+ private Ikev2VpnProfile buildIkev2VpnProfileIkeTunConnParams(
+ final boolean isRestrictedToTestNetworks, final boolean requiresValidation,
+ final boolean testIpv6) throws Exception {
+ final IkeTunnelConnectionParams params =
+ new IkeTunnelConnectionParams(testIpv6
+ ? IkeSessionTestUtils.IKE_PARAMS_V6 : IkeSessionTestUtils.IKE_PARAMS_V4,
+ IkeSessionTestUtils.CHILD_PARAMS);
+
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(params)
+ .setRequiresInternetValidation(requiresValidation)
+ .setProxy(TEST_PROXY_INFO)
+ .setMaxMtu(TEST_MTU)
+ .setMetered(false);
+
+ if (isRestrictedToTestNetworks) {
+ builder.restrictToTestNetworks();
+ }
+ return builder.build();
+ }
+
private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote,
boolean isRestrictedToTestNetworks, boolean requiresValidation) throws Exception {
- final Ikev2VpnProfileBuilderShim builder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(remote, TEST_IDENTITY, null)
- .setAuthPsk(TEST_PSK);
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK);
return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
requiresValidation);
}
@@ -259,8 +274,8 @@
private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
throws Exception {
- final Ikev2VpnProfileBuilderShim builder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null)
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
.setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
false /* requiresValidation */);
@@ -268,8 +283,8 @@
private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
throws Exception {
- final Ikev2VpnProfileBuilderShim builder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null)
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
.setAuthDigitalSignature(
mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
@@ -302,15 +317,8 @@
assertNull(profile.getServerRootCaCert());
assertNull(profile.getRsaPrivateKey());
assertNull(profile.getUserCert());
- final Ikev2VpnProfileShim<Ikev2VpnProfile> shim = new Ikev2VpnProfileShimImpl(profile);
- if (TestUtils.shouldTestTApis()) {
- assertEquals(requiresValidation, shim.isInternetValidationRequired());
- } else {
- try {
- shim.isInternetValidationRequired();
- fail("Only supported from API level 33");
- } catch (UnsupportedApiLevelException expected) {
- }
+ if (isAtLeastT()) {
+ assertEquals(requiresValidation, profile.isInternetValidationRequired());
}
}
@@ -320,10 +328,10 @@
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
assumeTrue(TestUtils.shouldTestTApis());
- final IkeTunnelConnectionParams expectedParams =
- new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
- final Ikev2VpnProfileBuilderShim ikeProfileBuilder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(null, null, expectedParams);
+ final IkeTunnelConnectionParams expectedParams = new IkeTunnelConnectionParams(
+ IkeSessionTestUtils.IKE_PARAMS_V6, IkeSessionTestUtils.CHILD_PARAMS);
+ final Ikev2VpnProfile.Builder ikeProfileBuilder =
+ new Ikev2VpnProfile.Builder(expectedParams);
// Verify the other Ike options could not be set with IkeTunnelConnectionParams.
final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
assertThrows(expected, () -> ikeProfileBuilder.setAuthPsk(TEST_PSK));
@@ -332,10 +340,9 @@
assertThrows(expected, () -> ikeProfileBuilder.setAuthDigitalSignature(
mUserCertKey.cert, mUserCertKey.key, mServerRootCa));
- final Ikev2VpnProfile profile = (Ikev2VpnProfile) ikeProfileBuilder.build().getProfile();
+ final Ikev2VpnProfile profile = ikeProfileBuilder.build();
- assertEquals(expectedParams,
- new Ikev2VpnProfileShimImpl(profile).getIkeTunnelConnectionParams());
+ assertEquals(expectedParams, profile.getIkeTunnelConnectionParams());
}
@Test
@@ -467,7 +474,8 @@
}
private void checkStartStopVpnProfileBuildsNetworks(@NonNull IkeTunUtils tunUtils,
- boolean testIpv6, boolean requiresValidation, boolean testSessionKey)
+ boolean testIpv6, boolean requiresValidation, boolean testSessionKey,
+ boolean testIkeTunConnParams)
throws Exception {
String serverAddr = testIpv6 ? TEST_SERVER_ADDR_V6 : TEST_SERVER_ADDR_V4;
String initResp = testIpv6 ? SUCCESSFUL_IKE_INIT_RESP_V6 : SUCCESSFUL_IKE_INIT_RESP_V4;
@@ -477,14 +485,17 @@
// Requires MANAGE_TEST_NETWORKS to provision a test-mode profile.
mCtsNetUtils.setAppopPrivileged(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, true);
- final Ikev2VpnProfile profile = buildIkev2VpnProfilePsk(serverAddr,
- true /* isRestrictedToTestNetworks */, requiresValidation);
+ final Ikev2VpnProfile profile = testIkeTunConnParams
+ ? buildIkev2VpnProfileIkeTunConnParams(true /* isRestrictedToTestNetworks */,
+ requiresValidation, testIpv6)
+ : buildIkev2VpnProfilePsk(serverAddr, true /* isRestrictedToTestNetworks */,
+ requiresValidation);
assertNull(sVpnMgr.provisionVpnProfile(profile));
final TestableNetworkCallback cb = new TestableNetworkCallback(TIMEOUT_MS);
final NetworkRequest nr = new NetworkRequest.Builder()
.clearCapabilities().addTransportType(TRANSPORT_VPN).build();
- sCM.registerNetworkCallback(nr, cb);
+ registerNetworkCallback(nr, cb);
if (testSessionKey) {
// testSessionKey will never be true if running on <T
@@ -520,7 +531,8 @@
assertFalse(profileState.isLockdownEnabled());
}
- cb.expectCapabilitiesThat(vpnNetwork, TIMEOUT_MS, caps -> caps.hasTransport(TRANSPORT_VPN)
+ cb.expectCapabilitiesThat(vpnNetwork, TIMEOUT_MS,
+ caps -> caps.hasTransport(TRANSPORT_VPN)
&& caps.hasCapability(NET_CAPABILITY_INTERNET)
&& !caps.hasCapability(NET_CAPABILITY_VALIDATED)
&& Process.myUid() == caps.getOwnerUid());
@@ -533,8 +545,10 @@
// but unexpectedly sends this callback, expecting LOST below will fail because the next
// callback will be the validated capabilities instead.
// In S and below, |requiresValidation| is ignored, so this callback is always sent
- // regardless of its value.
- if (!requiresValidation || !TestUtils.shouldTestTApis()) {
+ // regardless of its value. However, there is a race in Vpn(see b/228574221) that VPN may
+ // misuse VPN network itself as the underlying network. The fix is not available without
+ // SDK > T platform. Thus, verify this only on T+ platform.
+ if (!requiresValidation && TestUtils.shouldTestTApis()) {
cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, TIMEOUT_MS,
entry -> ((CallbackEntry.CapabilitiesChanged) entry).getCaps()
.hasCapability(NET_CAPABILITY_VALIDATED));
@@ -547,10 +561,16 @@
lost -> vpnNetwork.equals(lost.getNetwork()));
}
+ private void registerNetworkCallback(NetworkRequest request, TestableNetworkCallback callback) {
+ sCM.registerNetworkCallback(request, callback);
+ mCallbacksToUnregister.add(callback);
+ }
+
private class VerifyStartStopVpnProfileTest implements TestNetworkRunnable.Test {
private final boolean mTestIpv6Only;
private final boolean mRequiresValidation;
private final boolean mTestSessionKey;
+ private final boolean mTestIkeTunConnParams;
/**
* Constructs the test
@@ -560,10 +580,11 @@
* @param testSessionKey if true, start VPN by calling startProvisionedVpnProfileSession()
*/
VerifyStartStopVpnProfileTest(boolean testIpv6Only, boolean requiresValidation,
- boolean testSessionKey) {
+ boolean testSessionKey, boolean testIkeTunConnParams) {
mTestIpv6Only = testIpv6Only;
mRequiresValidation = requiresValidation;
mTestSessionKey = testSessionKey;
+ mTestIkeTunConnParams = testIkeTunConnParams;
}
@Override
@@ -571,8 +592,8 @@
throws Exception {
final IkeTunUtils tunUtils = new IkeTunUtils(testIface.getFileDescriptor());
- checkStartStopVpnProfileBuildsNetworks(
- tunUtils, mTestIpv6Only, mRequiresValidation, mTestSessionKey);
+ checkStartStopVpnProfileBuildsNetworks(tunUtils, mTestIpv6Only, mRequiresValidation,
+ mTestSessionKey, mTestIkeTunConnParams);
}
@Override
@@ -590,53 +611,83 @@
}
}
- @Test
- public void testStartStopVpnProfileV4() throws Exception {
+ private void doTestStartStopVpnProfile(boolean testIpv6Only, boolean requiresValidation,
+ boolean testSessionKey, boolean testIkeTunConnParams) throws Exception {
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
-
// Requires shell permission to update appops.
runWithShellPermissionIdentity(
new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
- false /* testIpv6Only */, false /* requiresValidation */,
- false /* testSessionKey */)));
+ testIpv6Only, requiresValidation, testSessionKey , testIkeTunConnParams)));
+ }
- runWithShellPermissionIdentity(
- new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
- false /* testIpv6Only */, true /* requiresValidation */,
- false /* testSessionKey */)));
+ @Test
+ public void testStartStopVpnProfileV4() throws Exception {
+ doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
+ false /* testSessionKey */, false /* testIkeTunConnParams */);
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testStartStopVpnProfileV4WithValidation() throws Exception {
+ assumeTrue(TestUtils.shouldTestTApis());
+ doTestStartStopVpnProfile(false /* testIpv6Only */, true /* requiresValidation */,
+ false /* testSessionKey */, false /* testIkeTunConnParams */);
}
@Test
public void testStartStopVpnProfileV6() throws Exception {
- assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
+ false /* testSessionKey */, false /* testIkeTunConnParams */);
+ }
- // Requires shell permission to update appops.
- runWithShellPermissionIdentity(
- new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
- true /* testIpv6Only */, false /* requiresValidation */,
- false /* testSessionKey */)));
- runWithShellPermissionIdentity(
- new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
- true /* testIpv6Only */, true /* requiresValidation */,
- false /* testSessionKey */)));
+ @Test @IgnoreUpTo(SC_V2)
+ public void testStartStopVpnProfileV6WithValidation() throws Exception {
+ assumeTrue(TestUtils.shouldTestTApis());
+ doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
+ false /* testSessionKey */, false /* testIkeTunConnParams */);
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testStartStopVpnProfileIkeTunConnParamsV4() throws Exception {
+ assumeTrue(TestUtils.shouldTestTApis());
+ doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
+ false /* testSessionKey */, true /* testIkeTunConnParams */);
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testStartStopVpnProfileIkeTunConnParamsV4WithValidation() throws Exception {
+ assumeTrue(TestUtils.shouldTestTApis());
+ doTestStartStopVpnProfile(false /* testIpv6Only */, true /* requiresValidation */,
+ false /* testSessionKey */, true /* testIkeTunConnParams */);
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testStartStopVpnProfileIkeTunConnParamsV6() throws Exception {
+ assumeTrue(TestUtils.shouldTestTApis());
+ doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
+ false /* testSessionKey */, true /* testIkeTunConnParams */);
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testStartStopVpnProfileIkeTunConnParamsV6WithValidation() throws Exception {
+ assumeTrue(TestUtils.shouldTestTApis());
+ doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
+ false /* testSessionKey */, true /* testIkeTunConnParams */);
}
@IgnoreUpTo(SC_V2)
@Test
- public void testStartProvisionedVpnProfileSession() throws Exception {
- assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
+ public void testStartProvisionedVpnV4ProfileSession() throws Exception {
assumeTrue(TestUtils.shouldTestTApis());
+ doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
+ true /* testSessionKey */, false /* testIkeTunConnParams */);
+ }
- // Requires shell permission to update appops.
- runWithShellPermissionIdentity(
- new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
- false /* testIpv6Only */, false /* requiresValidation */,
- true /* testSessionKey */)));
-
- runWithShellPermissionIdentity(
- new TestNetworkRunnable(new VerifyStartStopVpnProfileTest(
- true /* testIpv6Only */, false /* requiresValidation */,
- true /* testSessionKey */)));
+ @IgnoreUpTo(SC_V2)
+ @Test
+ public void testStartProvisionedVpnV6ProfileSession() throws Exception {
+ assumeTrue(TestUtils.shouldTestTApis());
+ doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
+ true /* testSessionKey */, false /* testIkeTunConnParams */);
}
private static class CertificateAndKey {
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 0504973..d4f3d57 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -1275,4 +1275,23 @@
matchAllCallback.expectCallback<Lost>(wifiNetwork)
wifiAgent.expectCallback<OnNetworkUnwanted>()
}
+
+ @Test
+ fun testUnregisterAgentBeforeAgentFullyConnected() {
+ val specifier = UUID.randomUUID().toString()
+ val callback = TestableNetworkCallback()
+ val transports = intArrayOf(TRANSPORT_CELLULAR)
+ // Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
+ requestNetwork(makeTestNetworkRequest(specifier = specifier), callback)
+ val nc = makeTestNetworkCapabilities(specifier, transports)
+ val agent = createNetworkAgent(realContext, initialNc = nc)
+ // Connect the agent
+ agent.register()
+ // Mark agent connected then unregister agent immediately. Verify that both available and
+ // lost callback should be sent still.
+ agent.markConnected()
+ agent.unregister()
+ callback.expectCallback<Available>(agent.network!!)
+ callback.eventuallyExpect<Lost> { it.network == agent.network }
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index fb720a7..f86c5cd 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -29,8 +29,20 @@
import static android.app.usage.NetworkStats.Bucket.STATE_FOREGROUND;
import static android.app.usage.NetworkStats.Bucket.TAG_NONE;
import static android.app.usage.NetworkStats.Bucket.UID_ALL;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+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;
import android.app.AppOpsManager;
+import android.app.Instrumentation;
import android.app.usage.NetworkStats;
import android.app.usage.NetworkStatsManager;
import android.content.Context;
@@ -40,7 +52,11 @@
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
+import android.net.NetworkStatsCollection;
+import android.net.NetworkStatsHistory;
import android.net.TrafficStats;
+import android.net.netstats.NetworkStatsDataMigrationUtils;
+import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
@@ -48,11 +64,23 @@
import android.os.SystemClock;
import android.platform.test.annotations.AppModeFull;
import android.telephony.TelephonyManager;
-import android.test.InstrumentationTestCase;
+import android.text.TextUtils;
import android.util.Log;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
import com.android.compatibility.common.util.ShellIdentityUtils;
import com.android.compatibility.common.util.SystemUtil;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.testutils.ConnectivityModuleTest;
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
import java.io.IOException;
import java.io.InputStream;
@@ -62,8 +90,18 @@
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
-public class NetworkStatsManagerTest extends InstrumentationTestCase {
+@ConnectivityModuleTest
+@AppModeFull(reason = "instant apps cannot be granted USAGE_STATS")
+@RunWith(AndroidJUnit4.class)
+public class NetworkStatsManagerTest {
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(Build.VERSION_CODES.Q);
+
private static final String LOG_TAG = "NetworkStatsManagerTest";
private static final String APPOPS_SET_SHELL_COMMAND = "appops set {0} {1} {2}";
private static final String APPOPS_GET_SHELL_COMMAND = "appops get {0} {1}";
@@ -164,9 +202,11 @@
};
private String mPkg;
+ private Context mContext;
private NetworkStatsManager mNsm;
private ConnectivityManager mCm;
private PackageManager mPm;
+ private Instrumentation mInstrumentation;
private long mStartTime;
private long mEndTime;
@@ -224,44 +264,40 @@
}
}
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- mNsm = (NetworkStatsManager) getInstrumentation().getContext()
- .getSystemService(Context.NETWORK_STATS_SERVICE);
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getContext();
+ mNsm = mContext.getSystemService(NetworkStatsManager.class);
mNsm.setPollForce(true);
- mCm = (ConnectivityManager) getInstrumentation().getContext()
- .getSystemService(Context.CONNECTIVITY_SERVICE);
+ mCm = mContext.getSystemService(ConnectivityManager.class);
+ mPm = mContext.getPackageManager();
+ mPkg = mContext.getPackageName();
- mPm = getInstrumentation().getContext().getPackageManager();
-
- mPkg = getInstrumentation().getContext().getPackageName();
-
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
mWriteSettingsMode = getAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS);
setAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS, "allow");
mUsageStatsMode = getAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS);
}
- @Override
- protected void tearDown() throws Exception {
+ @After
+ public void tearDown() throws Exception {
if (mWriteSettingsMode != null) {
setAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS, mWriteSettingsMode);
}
if (mUsageStatsMode != null) {
setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, mUsageStatsMode);
}
- super.tearDown();
}
private void setAppOpsMode(String appop, String mode) throws Exception {
final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND, mPkg, appop, mode);
- SystemUtil.runShellCommand(command);
+ SystemUtil.runShellCommand(mInstrumentation, command);
}
private String getAppOpsMode(String appop) throws Exception {
final String command = MessageFormat.format(APPOPS_GET_SHELL_COMMAND, mPkg, appop);
- String result = SystemUtil.runShellCommand(command);
+ String result = SystemUtil.runShellCommand(mInstrumentation, command);
if (result == null) {
Log.w(LOG_TAG, "App op " + appop + " could not be read.");
}
@@ -269,7 +305,7 @@
}
private boolean isInForeground() throws IOException {
- String result = SystemUtil.runShellCommand(getInstrumentation(),
+ String result = SystemUtil.runShellCommand(mInstrumentation,
"cmd activity get-uid-state " + Process.myUid());
return result.contains("FOREGROUND");
}
@@ -366,15 +402,14 @@
private String getSubscriberId(int networkIndex) {
int networkType = mNetworkInterfacesToTest[networkIndex].getNetworkType();
if (ConnectivityManager.TYPE_MOBILE == networkType) {
- TelephonyManager tm = (TelephonyManager) getInstrumentation().getContext()
- .getSystemService(Context.TELEPHONY_SERVICE);
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
return ShellIdentityUtils.invokeMethodWithShellPermissions(tm,
(telephonyManager) -> telephonyManager.getSubscriberId());
}
return "";
}
- @AppModeFull
+ @Test
public void testDeviceSummary() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
@@ -410,7 +445,7 @@
}
}
- @AppModeFull
+ @Test
public void testUserSummary() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
@@ -446,7 +481,7 @@
}
}
- @AppModeFull
+ @Test
public void testAppSummary() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
// Use tolerance value that large enough to make sure stats of at
@@ -522,7 +557,7 @@
}
}
- @AppModeFull
+ @Test
public void testAppDetails() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
// Relatively large tolerance to accommodate for history bucket size.
@@ -565,7 +600,7 @@
}
}
- @AppModeFull
+ @Test
public void testUidDetails() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
// Relatively large tolerance to accommodate for history bucket size.
@@ -619,7 +654,7 @@
}
}
- @AppModeFull
+ @Test
public void testTagDetails() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
// Relatively large tolerance to accommodate for history bucket size.
@@ -726,7 +761,7 @@
bucket.getRxBytes(), bucket.getTxBytes()));
}
- @AppModeFull
+ @Test
public void testUidTagStateDetails() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
// Relatively large tolerance to accommodate for history bucket size.
@@ -803,7 +838,7 @@
}
}
- @AppModeFull
+ @Test
public void testCallback() throws Exception {
for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
// Relatively large tolerance to accommodate for history bucket size.
@@ -825,6 +860,43 @@
// storing files of >2MB in CTS.
mNsm.unregisterUsageCallback(usageCallback);
+
+ // For T- devices, the registerUsageCallback invocation below will need a looper
+ // from the thread that calls into the API, which is not available in the test.
+ if (SdkLevel.isAtLeastT()) {
+ mNsm.registerUsageCallback(mNetworkInterfacesToTest[i].getNetworkType(),
+ getSubscriberId(i), THRESHOLD_BYTES, usageCallback);
+ mNsm.unregisterUsageCallback(usageCallback);
+ }
+ }
+ }
+
+ @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+ @Test
+ public void testDataMigrationUtils() throws Exception {
+ final List<String> prefixes = List.of(PREFIX_UID, PREFIX_XT, PREFIX_UID_TAG);
+ for (final String prefix : prefixes) {
+ final long duration = TextUtils.equals(PREFIX_XT, prefix) ? TimeUnit.HOURS.toMillis(1)
+ : TimeUnit.HOURS.toMillis(2);
+
+ final NetworkStatsCollection collection =
+ NetworkStatsDataMigrationUtils.readPlatformCollection(prefix, duration);
+
+ final long now = System.currentTimeMillis();
+ final Set<Map.Entry<NetworkStatsCollection.Key, NetworkStatsHistory>> entries =
+ collection.getEntries().entrySet();
+ for (final Map.Entry<NetworkStatsCollection.Key, NetworkStatsHistory> entry : entries) {
+ for (final NetworkStatsHistory.Entry historyEntry : entry.getValue().getEntries()) {
+ // Verify all value fields are reasonable.
+ assertTrue(historyEntry.getBucketStart() <= now);
+ assertTrue(historyEntry.getActiveTime() <= duration);
+ assertTrue(historyEntry.getRxBytes() >= 0);
+ assertTrue(historyEntry.getRxPackets() >= 0);
+ assertTrue(historyEntry.getTxBytes() >= 0);
+ assertTrue(historyEntry.getTxPackets() >= 0);
+ assertTrue(historyEntry.getOperations() >= 0);
+ }
+ }
}
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
index 391d03a..462c8a3 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
@@ -16,16 +16,11 @@
package android.net.cts
-import android.Manifest
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
import android.net.util.NetworkStackUtils
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
-import android.util.Log
import com.android.testutils.runAsShell
-import com.android.testutils.tryTest
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.Executor
-import java.util.concurrent.TimeUnit
/**
* Collection of utility methods for configuring network validation.
@@ -38,9 +33,14 @@
* Clear the test network validation URLs.
*/
@JvmStatic fun clearValidationTestUrlsDeviceConfig() {
- setHttpsUrlDeviceConfig(null)
- setHttpUrlDeviceConfig(null)
- setUrlExpirationDeviceConfig(null)
+ runAsShell(WRITE_DEVICE_CONFIG) {
+ DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, null, false)
+ DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, null, false)
+ DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.TEST_URL_EXPIRATION_TIME, null, false)
+ }
}
/**
@@ -48,71 +48,28 @@
*
* @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
*/
- @JvmStatic fun setHttpsUrlDeviceConfig(url: String?) =
- setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
+ @JvmStatic
+ fun setHttpsUrlDeviceConfig(rule: DeviceConfigRule, url: String?) =
+ rule.setConfig(NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
/**
* Set the test validation HTTP URL.
*
* @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
*/
- @JvmStatic fun setHttpUrlDeviceConfig(url: String?) =
- setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
+ @JvmStatic
+ fun setHttpUrlDeviceConfig(rule: DeviceConfigRule, url: String?) =
+ rule.setConfig(NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
/**
* Set the test validation URL expiration.
*
* @see NetworkStackUtils.TEST_URL_EXPIRATION_TIME
*/
- @JvmStatic fun setUrlExpirationDeviceConfig(timestamp: Long?) =
- setConfig(NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
-
- private fun setConfig(configKey: String, value: String?): String? {
- Log.i(TAG, "Setting config \"$configKey\" to \"$value\"")
- val readWritePermissions = arrayOf(
- Manifest.permission.READ_DEVICE_CONFIG,
- Manifest.permission.WRITE_DEVICE_CONFIG)
-
- val existingValue = runAsShell(*readWritePermissions) {
- DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, configKey)
- }
- if (existingValue == value) {
- // Already the correct value. There may be a race if a change is already in flight,
- // but if multiple threads update the config there is no way to fix that anyway.
- Log.i(TAG, "\$configKey\" already had value \"$value\"")
- return value
- }
-
- val future = CompletableFuture<String>()
- val listener = DeviceConfig.OnPropertiesChangedListener {
- // The listener receives updates for any change to any key, so don't react to
- // changes that do not affect the relevant key
- if (!it.keyset.contains(configKey)) return@OnPropertiesChangedListener
- if (it.getString(configKey, null) == value) {
- future.complete(value)
- }
- }
-
- return tryTest {
- runAsShell(*readWritePermissions) {
- DeviceConfig.addOnPropertiesChangedListener(
- NAMESPACE_CONNECTIVITY,
- inlineExecutor,
- listener)
- DeviceConfig.setProperty(
- NAMESPACE_CONNECTIVITY,
- configKey,
- value,
- false /* makeDefault */)
- // Don't drop the permission until the config is applied, just in case
- future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
- }.also {
- Log.i(TAG, "Config \"$configKey\" successfully set to \"$value\"")
- }
- } cleanup {
- DeviceConfig.removeOnPropertiesChangedListener(listener)
- }
- }
-
- private val inlineExecutor get() = Executor { r -> r.run() }
+ @JvmStatic
+ fun setUrlExpirationDeviceConfig(rule: DeviceConfigRule, timestamp: Long?) =
+ rule.setConfig(NAMESPACE_CONNECTIVITY,
+ NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index b139a9b..64cc97d 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -22,6 +22,7 @@
import android.net.Network
import android.net.NetworkAgentConfig
import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
import android.net.NetworkCapabilities.TRANSPORT_TEST
import android.net.NetworkRequest
@@ -45,17 +46,16 @@
import android.net.nsd.NsdManager.RegistrationListener
import android.net.nsd.NsdManager.ResolveListener
import android.net.nsd.NsdServiceInfo
+import android.os.Handler
import android.os.HandlerThread
+import android.os.Process.myTid
import android.platform.test.annotations.AppModeFull
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import com.android.net.module.util.ArrayTrackRecord
import com.android.net.module.util.TrackRecord
-import com.android.networkstack.apishim.ConstantsShim
import com.android.networkstack.apishim.NsdShimImpl
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.SC_V2
import com.android.testutils.TestableNetworkAgent
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.runAsShell
@@ -65,7 +65,6 @@
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.net.ServerSocket
@@ -82,6 +81,7 @@
private const val TAG = "NsdManagerTest"
private const val SERVICE_TYPE = "_nmt._tcp"
private const val TIMEOUT_MS = 2000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
private const val DBG = false
private val nsdShim = NsdShimImpl.newInstance()
@@ -89,10 +89,6 @@
@AppModeFull(reason = "Socket cannot bind in instant app mode")
@RunWith(AndroidJUnit4::class)
class NsdManagerTest {
- // NsdManager is not updatable before S, so tests do not need to be backwards compatible
- @get:Rule
- val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
-
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
@@ -118,12 +114,20 @@
private interface NsdEvent
private open class NsdRecord<T : NsdEvent> private constructor(
- private val history: ArrayTrackRecord<T>
+ private val history: ArrayTrackRecord<T>,
+ private val expectedThreadId: Int? = null
) : TrackRecord<T> by history {
- constructor() : this(ArrayTrackRecord())
+ constructor(expectedThreadId: Int? = null) : this(ArrayTrackRecord(), expectedThreadId)
val nextEvents = history.newReadHead()
+ override fun add(e: T): Boolean {
+ if (expectedThreadId != null) {
+ assertEquals(expectedThreadId, myTid(), "Callback is running on the wrong thread")
+ }
+ return history.add(e)
+ }
+
inline fun <reified V : NsdEvent> expectCallbackEventually(
crossinline predicate: (V) -> Boolean = { true }
): V = nextEvents.poll(TIMEOUT_MS) { e -> e is V && predicate(e) } as V?
@@ -136,10 +140,15 @@
nextEvent.javaClass.simpleName)
return nextEvent
}
+
+ inline fun assertNoCallback(timeoutMs: Long = NO_CALLBACK_TIMEOUT_MS) {
+ val cb = nextEvents.poll(timeoutMs)
+ assertNull(cb, "Expected no callback but got $cb")
+ }
}
- private class NsdRegistrationRecord : RegistrationListener,
- NsdRecord<NsdRegistrationRecord.RegistrationEvent>() {
+ private class NsdRegistrationRecord(expectedThreadId: Int? = null) : RegistrationListener,
+ NsdRecord<NsdRegistrationRecord.RegistrationEvent>(expectedThreadId) {
sealed class RegistrationEvent : NsdEvent {
abstract val serviceInfo: NsdServiceInfo
@@ -176,8 +185,8 @@
}
}
- private class NsdDiscoveryRecord : DiscoveryListener,
- NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>() {
+ private class NsdDiscoveryRecord(expectedThreadId: Int? = null) :
+ DiscoveryListener, NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>(expectedThreadId) {
sealed class DiscoveryEvent : NsdEvent {
data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int)
: DiscoveryEvent()
@@ -249,9 +258,11 @@
fun setUp() {
handlerThread.start()
- runAsShell(MANAGE_TEST_NETWORKS) {
- testNetwork1 = createTestNetwork()
- testNetwork2 = createTestNetwork()
+ if (TestUtils.shouldTestTApis()) {
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ testNetwork1 = createTestNetwork()
+ testNetwork2 = createTestNetwork()
+ }
}
}
@@ -290,9 +301,11 @@
@After
fun tearDown() {
- runAsShell(MANAGE_TEST_NETWORKS) {
- testNetwork1.close(cm)
- testNetwork2.close(cm)
+ if (TestUtils.shouldTestTApis()) {
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ testNetwork1.close(cm)
+ testNetwork2.close(cm)
+ }
}
handlerThread.quitSafely()
}
@@ -393,14 +406,17 @@
si2.serviceName = serviceName
si2.port = localPort
val registrationRecord2 = NsdRegistrationRecord()
- val registeredInfo2 = registerService(registrationRecord2, si2)
+ nsdManager.registerService(si2, NsdManager.PROTOCOL_DNS_SD, registrationRecord2)
+ val registeredInfo2 = registrationRecord2.expectCallback<ServiceRegistered>().serviceInfo
// Expect a service record to be discovered (and filter the ones
// that are unrelated to this test)
val foundInfo2 = discoveryRecord.waitForServiceDiscovered(registeredInfo2.serviceName)
// Resolve the service
- val resolvedService2 = resolveService(foundInfo2)
+ val resolveRecord2 = NsdResolveRecord()
+ nsdManager.resolveService(foundInfo2, resolveRecord2)
+ val resolvedService2 = resolveRecord2.expectCallback<ServiceResolved>().serviceInfo
// Check that the resolved service doesn't have any TXT records
assertEquals(0, resolvedService2.attributes.size)
@@ -416,7 +432,7 @@
@Test
fun testNsdManager_DiscoverOnNetwork() {
// This test requires shims supporting T+ APIs (discovering on specific network)
- assumeTrue(ConstantsShim.VERSION > SC_V2)
+ assumeTrue(TestUtils.shouldTestTApis())
val si = NsdServiceInfo()
si.serviceType = SERVICE_TYPE
@@ -450,16 +466,19 @@
@Test
fun testNsdManager_DiscoverWithNetworkRequest() {
// This test requires shims supporting T+ APIs (discovering on network request)
- assumeTrue(ConstantsShim.VERSION > SC_V2)
+ assumeTrue(TestUtils.shouldTestTApis())
val si = NsdServiceInfo()
si.serviceType = SERVICE_TYPE
si.serviceName = this.serviceName
si.port = 12345 // Test won't try to connect so port does not matter
- val registrationRecord = NsdRegistrationRecord()
- val registeredInfo1 = registerService(registrationRecord, si)
- val discoveryRecord = NsdDiscoveryRecord()
+ val handler = Handler(handlerThread.looper)
+ val executor = Executor { handler.post(it) }
+
+ val registrationRecord = NsdRegistrationRecord(expectedThreadId = handlerThread.threadId)
+ val registeredInfo1 = registerService(registrationRecord, si, executor)
+ val discoveryRecord = NsdDiscoveryRecord(expectedThreadId = handlerThread.threadId)
tryTest {
val specifier = TestNetworkSpecifier(testNetwork1.iface.interfaceName)
@@ -469,7 +488,7 @@
.addTransportType(TRANSPORT_TEST)
.setNetworkSpecifier(specifier)
.build(),
- Executor { it.run() }, discoveryRecord)
+ executor, discoveryRecord)
val discoveryStarted = discoveryRecord.expectCallback<DiscoveryStarted>()
assertEquals(SERVICE_TYPE, discoveryStarted.serviceType)
@@ -485,7 +504,7 @@
assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceLost1.serviceInfo))
registrationRecord.expectCallback<ServiceUnregistered>()
- val registeredInfo2 = registerService(registrationRecord, si)
+ val registeredInfo2 = registerService(registrationRecord, si, executor)
val serviceDiscovered2 = discoveryRecord.expectCallback<ServiceFound>()
assertEquals(registeredInfo2.serviceName, serviceDiscovered2.serviceInfo.serviceName)
assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered2.serviceInfo))
@@ -513,9 +532,42 @@
}
@Test
+ fun testNsdManager_DiscoverWithNetworkRequest_NoMatchingNetwork() {
+ // This test requires shims supporting T+ APIs (discovering on network request)
+ assumeTrue(TestUtils.shouldTestTApis())
+
+ val si = NsdServiceInfo()
+ si.serviceType = SERVICE_TYPE
+ si.serviceName = this.serviceName
+ si.port = 12345 // Test won't try to connect so port does not matter
+
+ val handler = Handler(handlerThread.looper)
+ val executor = Executor { handler.post(it) }
+
+ val discoveryRecord = NsdDiscoveryRecord(expectedThreadId = handlerThread.threadId)
+ val specifier = TestNetworkSpecifier(testNetwork1.iface.interfaceName)
+
+ tryTest {
+ nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+ NetworkRequest.Builder()
+ .removeCapability(NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ // Specified network does not have this capability
+ .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ .setNetworkSpecifier(specifier)
+ .build(),
+ executor, discoveryRecord)
+ discoveryRecord.expectCallback<DiscoveryStarted>()
+ } cleanup {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+ discoveryRecord.expectCallback<DiscoveryStopped>()
+ }
+ }
+
+ @Test
fun testNsdManager_ResolveOnNetwork() {
// This test requires shims supporting T+ APIs (NsdServiceInfo.network)
- assumeTrue(ConstantsShim.VERSION > SC_V2)
+ assumeTrue(TestUtils.shouldTestTApis())
val si = NsdServiceInfo()
si.serviceType = SERVICE_TYPE
@@ -556,12 +608,99 @@
}
}
+ @Test
+ fun testNsdManager_RegisterOnNetwork() {
+ // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
+ assumeTrue(TestUtils.shouldTestTApis())
+
+ val si = NsdServiceInfo()
+ si.serviceType = SERVICE_TYPE
+ si.serviceName = this.serviceName
+ si.network = testNetwork1.network
+ si.port = 12345 // Test won't try to connect so port does not matter
+
+ // Register service on testNetwork1
+ val registrationRecord = NsdRegistrationRecord()
+ registerService(registrationRecord, si)
+ val discoveryRecord = NsdDiscoveryRecord()
+ val discoveryRecord2 = NsdDiscoveryRecord()
+ val discoveryRecord3 = NsdDiscoveryRecord()
+
+ tryTest {
+ // Discover service on testNetwork1.
+ nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, discoveryRecord)
+ // Expect that service is found on testNetwork1
+ val foundInfo = discoveryRecord.waitForServiceDiscovered(
+ serviceName, testNetwork1.network)
+ assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
+
+ // Discover service on testNetwork2.
+ nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork2.network, Executor { it.run() }, discoveryRecord2)
+ // Expect that discovery is started then no other callbacks.
+ discoveryRecord2.expectCallback<DiscoveryStarted>()
+ discoveryRecord2.assertNoCallback()
+
+ // Discover service on all networks (not specify any network).
+ nsdShim.discoverServices(nsdManager, SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+ null as Network? /* network */, Executor { it.run() }, discoveryRecord3)
+ // Expect that service is found on testNetwork1
+ val foundInfo3 = discoveryRecord3.waitForServiceDiscovered(
+ serviceName, testNetwork1.network)
+ assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo3))
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(discoveryRecord2)
+ discoveryRecord2.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
+ @Test
+ fun testNsdManager_RegisterServiceNameWithNonStandardCharacters() {
+ val serviceNames = "^Nsd.Test|Non-#AsCiI\\Characters&\\ufffe テスト 測試"
+ val si = NsdServiceInfo().apply {
+ serviceType = SERVICE_TYPE
+ serviceName = serviceNames
+ port = 12345 // Test won't try to connect so port does not matter
+ }
+
+ // Register the service name which contains non-standard characters.
+ val registrationRecord = NsdRegistrationRecord()
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
+ registrationRecord.expectCallback<ServiceRegistered>()
+
+ tryTest {
+ // Discover that service name.
+ val discoveryRecord = NsdDiscoveryRecord()
+ nsdManager.discoverServices(
+ SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord
+ )
+ val foundInfo = discoveryRecord.waitForServiceDiscovered(serviceNames)
+
+ // Expect that resolving the service name works properly even service name contains
+ // non-standard characters.
+ val resolveRecord = NsdResolveRecord()
+ nsdManager.resolveService(foundInfo, resolveRecord)
+ val resolvedCb = resolveRecord.expectCallback<ServiceResolved>()
+ assertEquals(foundInfo.serviceName, resolvedCb.serviceInfo.serviceName)
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ } cleanup {
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
/**
* Register a service and return its registration record.
*/
- private fun registerService(record: NsdRegistrationRecord, si: NsdServiceInfo): NsdServiceInfo {
- nsdShim.registerService(nsdManager, si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() },
- record)
+ private fun registerService(
+ record: NsdRegistrationRecord,
+ si: NsdServiceInfo,
+ executor: Executor = Executor { it.run() }
+ ): NsdServiceInfo {
+ nsdShim.registerService(nsdManager, si, NsdManager.PROTOCOL_DNS_SD, executor, record)
// We may not always get the name that we tried to register;
// This events tells us the name that was registered.
val cb = record.expectCallback<ServiceRegistered>()
diff --git a/tests/cts/net/src/android/net/cts/RateLimitTest.java b/tests/cts/net/src/android/net/cts/RateLimitTest.java
index 423f213..28cec1a 100644
--- a/tests/cts/net/src/android/net/cts/RateLimitTest.java
+++ b/tests/cts/net/src/android/net/cts/RateLimitTest.java
@@ -304,7 +304,7 @@
// If this value is too low, this test might become flaky because of the burst value that
// allows to send at a higher data rate for a short period of time. The faster the data rate
// and the longer the test, the less this test will be affected.
- final long dataLimitInBytesPerSecond = 1_000_000; // 1MB/s
+ final long dataLimitInBytesPerSecond = 2_000_000; // 2MB/s
long resultInBytesPerSecond = runIngressDataRateMeasurement(Duration.ofSeconds(1));
assertGreaterThan("Failed initial test with rate limit disabled", resultInBytesPerSecond,
dataLimitInBytesPerSecond);
@@ -315,9 +315,9 @@
waitForTcPoliceFilterInstalled(Duration.ofSeconds(1));
resultInBytesPerSecond = runIngressDataRateMeasurement(Duration.ofSeconds(10));
- // Add 1% tolerance to reduce test flakiness. Burst size is constant at 128KiB.
+ // Add 10% tolerance to reduce test flakiness. Burst size is constant at 128KiB.
assertLessThan("Failed test with rate limit enabled", resultInBytesPerSecond,
- (long) (dataLimitInBytesPerSecond * 1.01));
+ (long) (dataLimitInBytesPerSecond * 1.1));
ConnectivitySettingsManager.setIngressRateLimitInBytesPerSecond(mContext, -1);
diff --git a/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java b/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java
index b4ebcdb..244bfc5 100644
--- a/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java
@@ -16,44 +16,73 @@
package android.net.cts.util;
+import static android.net.ipsec.ike.SaProposal.DH_GROUP_4096_BIT_MODP;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_CBC;
+import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12;
+import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128;
import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_128;
-import static android.net.ipsec.ike.SaProposal.KEY_LEN_UNUSED;
+import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_256;
+import static android.net.ipsec.ike.SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC;
+import android.net.InetAddresses;
import android.net.ipsec.ike.ChildSaProposal;
import android.net.ipsec.ike.IkeFqdnIdentification;
+import android.net.ipsec.ike.IkeIpv4AddrIdentification;
+import android.net.ipsec.ike.IkeIpv6AddrIdentification;
import android.net.ipsec.ike.IkeSaProposal;
import android.net.ipsec.ike.IkeSessionParams;
-import android.net.ipsec.ike.SaProposal;
import android.net.ipsec.ike.TunnelModeChildSessionParams;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
/** Shared testing parameters and util methods for testing IKE */
public class IkeSessionTestUtils {
- private static final String TEST_CLIENT_ADDR = "test.client.com";
- private static final String TEST_SERVER_ADDR = "test.server.com";
- private static final String TEST_SERVER = "2001:0db8:85a3:0000:0000:8a2e:0370:7334";
+ private static final String TEST_SERVER_ADDR_V4 = "192.0.2.2";
+ private static final String TEST_SERVER_ADDR_V6 = "2001:db8::2";
+ private static final String TEST_IDENTITY = "client.cts.android.com";
+ private static final byte[] TEST_PSK = "ikeAndroidPsk".getBytes();
+ public static final IkeSessionParams IKE_PARAMS_V4 = getTestIkeSessionParams(false);
+ public static final IkeSessionParams IKE_PARAMS_V6 = getTestIkeSessionParams(true);
- public static final IkeSaProposal SA_PROPOSAL = new IkeSaProposal.Builder()
- .addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES, KEY_LEN_UNUSED)
- .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96)
- .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC)
- .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
- .build();
- public static final ChildSaProposal CHILD_PROPOSAL = new ChildSaProposal.Builder()
- .addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_128)
- .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE)
- .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP)
- .build();
+ public static final TunnelModeChildSessionParams CHILD_PARAMS = getChildSessionParams();
- public static final IkeSessionParams IKE_PARAMS =
- new IkeSessionParams.Builder()
- .setServerHostname(TEST_SERVER)
- .addSaProposal(SA_PROPOSAL)
- .setLocalIdentification(new IkeFqdnIdentification(TEST_CLIENT_ADDR))
- .setRemoteIdentification(new IkeFqdnIdentification(TEST_SERVER_ADDR))
- .setAuthPsk("psk".getBytes())
- .build();
- public static final TunnelModeChildSessionParams CHILD_PARAMS =
- new TunnelModeChildSessionParams.Builder()
- .addSaProposal(CHILD_PROPOSAL)
- .build();
+ private static TunnelModeChildSessionParams getChildSessionParams() {
+ final TunnelModeChildSessionParams.Builder childOptionsBuilder =
+ new TunnelModeChildSessionParams.Builder()
+ .addSaProposal(getChildSaProposals());
+
+ return childOptionsBuilder.build();
+ }
+
+ private static IkeSessionParams getTestIkeSessionParams(boolean testIpv6) {
+ final String testServer = testIpv6 ? TEST_SERVER_ADDR_V6 : TEST_SERVER_ADDR_V4;
+ final InetAddress addr = InetAddresses.parseNumericAddress(testServer);
+ final IkeSessionParams.Builder ikeOptionsBuilder =
+ new IkeSessionParams.Builder()
+ .setServerHostname(testServer)
+ .setLocalIdentification(new IkeFqdnIdentification(TEST_IDENTITY))
+ .setRemoteIdentification(testIpv6
+ ? new IkeIpv6AddrIdentification((Inet6Address) addr)
+ : new IkeIpv4AddrIdentification((Inet4Address) addr))
+ .setAuthPsk(TEST_PSK)
+ .addSaProposal(getIkeSaProposals());
+
+ return ikeOptionsBuilder.build();
+ }
+
+ private static IkeSaProposal getIkeSaProposals() {
+ return new IkeSaProposal.Builder()
+ .addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_256)
+ .addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128)
+ .addDhGroup(DH_GROUP_4096_BIT_MODP)
+ .addPseudorandomFunction(PSEUDORANDOM_FUNCTION_AES128_XCBC).build();
+ }
+
+ private static ChildSaProposal getChildSaProposals() {
+ return new ChildSaProposal.Builder()
+ .addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_128)
+ .build();
+ }
}
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index e9c4e5a..6096a8b 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -56,7 +56,7 @@
defaults: ["CtsTetheringTestDefaults"],
min_sdk_version: "30",
- target_sdk_version: "30",
+ target_sdk_version: "33",
static_libs: [
"TetheringIntegrationTestsLatestSdkLib",
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 97c1265..b3684ac 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -71,7 +71,7 @@
"net-tests-utils",
],
libs: [
- "service-connectivity",
+ "service-connectivity-for-tests",
"services.core",
"services.net",
],
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 80338aa..efc24d3 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -47,6 +47,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.connectivity.resources.R
+import com.android.server.BpfNetMaps
import com.android.server.ConnectivityService
import com.android.server.NetworkAgentWrapper
import com.android.server.TestNetIdManager
@@ -208,6 +209,7 @@
doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any())
doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties
doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager()
+ doReturn(mock(BpfNetMaps::class.java)).`when`(deps).getBpfNetMaps(any())
doAnswer { inv ->
object : MultinetworkPolicyTracker(inv.getArgument(0), inv.getArgument(1),
inv.getArgument(2)) {
diff --git a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
index c7cf040..361c968 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
@@ -22,8 +22,8 @@
import android.net.INetworkMonitorCallbacks
import android.net.Network
import android.net.metrics.IpConnectivityLog
-import android.net.util.SharedLog
import android.os.IBinder
+import com.android.net.module.util.SharedLog
import com.android.networkstack.netlink.TcpSocketTracker
import com.android.server.NetworkStackService
import com.android.server.NetworkStackService.NetworkMonitorConnector
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index 2bba282..db39e6f 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -42,6 +42,9 @@
#define PLATFORM "/sys/fs/bpf/"
#define TETHERING "/sys/fs/bpf/tethering/"
+#define PRIVATE "/sys/fs/bpf/net_private/"
+#define SHARED "/sys/fs/bpf/net_shared/"
+#define NETD "/sys/fs/bpf/netd_shared/"
class BpfExistenceTest : public ::testing::Test {
};
@@ -84,6 +87,45 @@
};
static const set<string> INTRODUCED_T = {
+ SHARED "map_block_blocked_ports_map",
+ SHARED "map_clatd_clat_egress4_map",
+ SHARED "map_clatd_clat_ingress6_map",
+ SHARED "map_dscp_policy_ipv4_dscp_policies_map",
+ SHARED "map_dscp_policy_ipv4_socket_to_policies_map_A",
+ SHARED "map_dscp_policy_ipv4_socket_to_policies_map_B",
+ SHARED "map_dscp_policy_ipv6_dscp_policies_map",
+ SHARED "map_dscp_policy_ipv6_socket_to_policies_map_A",
+ SHARED "map_dscp_policy_ipv6_socket_to_policies_map_B",
+ SHARED "map_dscp_policy_switch_comp_map",
+ NETD "map_netd_app_uid_stats_map",
+ NETD "map_netd_configuration_map",
+ NETD "map_netd_cookie_tag_map",
+ NETD "map_netd_iface_index_name_map",
+ NETD "map_netd_iface_stats_map",
+ NETD "map_netd_stats_map_A",
+ NETD "map_netd_stats_map_B",
+ NETD "map_netd_uid_counterset_map",
+ NETD "map_netd_uid_owner_map",
+ NETD "map_netd_uid_permission_map",
+ SHARED "prog_clatd_schedcls_egress4_clat_ether",
+ SHARED "prog_clatd_schedcls_egress4_clat_rawip",
+ SHARED "prog_clatd_schedcls_ingress6_clat_ether",
+ SHARED "prog_clatd_schedcls_ingress6_clat_rawip",
+ NETD "prog_netd_cgroupskb_egress_stats",
+ NETD "prog_netd_cgroupskb_ingress_stats",
+ NETD "prog_netd_cgroupsock_inet_create",
+ NETD "prog_netd_schedact_ingress_account",
+ NETD "prog_netd_skfilter_allowlist_xtbpf",
+ NETD "prog_netd_skfilter_denylist_xtbpf",
+ NETD "prog_netd_skfilter_egress_xtbpf",
+ NETD "prog_netd_skfilter_ingress_xtbpf",
+};
+
+static const set<string> INTRODUCED_T_5_4 = {
+ SHARED "prog_block_bind4_block_port",
+ SHARED "prog_block_bind6_block_port",
+ SHARED "prog_dscp_policy_schedcls_set_dscp_ether",
+ SHARED "prog_dscp_policy_schedcls_set_dscp_raw_ip",
};
static const set<string> REMOVED_T = {
@@ -125,6 +167,7 @@
if (IsAtLeastT()) {
addAll(expected, INTRODUCED_T);
+ if (android::bpf::isAtLeastKernelVersion(5, 4, 0)) addAll(expected, INTRODUCED_T_5_4);
removeAll(expected, REMOVED_T);
addAll(unexpected, REMOVED_T);
diff --git a/tests/native/Android.bp b/tests/native/Android.bp
index a8d908a..7d43aa8 100644
--- a/tests/native/Android.bp
+++ b/tests/native/Android.bp
@@ -31,3 +31,10 @@
],
compile_multilib: "first",
}
+
+filegroup {
+ name: "net_native_test_config_template",
+ srcs: [
+ "NetNativeTestConfigTemplate.xml",
+ ],
+}
diff --git a/tests/native/NetNativeTestConfigTemplate.xml b/tests/native/NetNativeTestConfigTemplate.xml
new file mode 100644
index 0000000..b71e9aa
--- /dev/null
+++ b/tests/native/NetNativeTestConfigTemplate.xml
@@ -0,0 +1,31 @@
+<!-- Copyright (C) 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Configuration for {MODULE} tests">
+ <option name="test-suite-tag" value="mts" />
+ <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
+ <!-- Only run tests if the device under test is SDK version 33 (Android 13) or above. -->
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController" />
+
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+ <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+ <option name="cleanup" value="true" />
+ <option name="push" value="{MODULE}->/data/local/tmp/{MODULE}" />
+ <option name="append-bitness" value="true" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.GTest" >
+ <option name="native-test-device-path" value="/data/local/tmp" />
+ <option name="module-name" value="{MODULE}" />
+ </test>
+</configuration>
diff --git a/tests/native/connectivity_native_test.cpp b/tests/native/connectivity_native_test.cpp
index 8b089ab..3db5265 100644
--- a/tests/native/connectivity_native_test.cpp
+++ b/tests/native/connectivity_native_test.cpp
@@ -14,14 +14,15 @@
* limitations under the License.
*/
+#include <aidl/android/net/connectivity/aidl/ConnectivityNative.h>
+#include <android/binder_manager.h>
+#include <android/binder_process.h>
#include <android-modules-utils/sdk_level.h>
#include <cutils/misc.h> // FIRST_APPLICATION_UID
#include <gtest/gtest.h>
#include <netinet/in.h>
-#include <android/binder_manager.h>
-#include <android/binder_process.h>
-#include <aidl/android/net/connectivity/aidl/ConnectivityNative.h>
+#include "bpf/BpfUtils.h"
using aidl::android::net::connectivity::aidl::IConnectivityNative;
@@ -40,6 +41,10 @@
if (!android::modules::sdklevel::IsAtLeastT()) GTEST_SKIP() <<
"Should be at least T device.";
+ // Skip test case if not on 5.4 kernel which is required by bpf prog.
+ if (!android::bpf::isAtLeastKernelVersion(5, 4, 0)) GTEST_SKIP() <<
+ "Kernel should be at least 5.4.";
+
ASSERT_NE(nullptr, mService.get());
// If there are already ports being blocked on device unblockAllPortsForBind() store
diff --git a/tests/smoketest/Android.bp b/tests/smoketest/Android.bp
index df8ab74..4ab24fc 100644
--- a/tests/smoketest/Android.bp
+++ b/tests/smoketest/Android.bp
@@ -22,6 +22,6 @@
static_libs: [
"androidx.test.rules",
"mockito-target-minus-junit4",
- "service-connectivity",
+ "service-connectivity-for-tests",
],
}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 545f7b9..0908ad2 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -54,41 +54,23 @@
filegroup {
name: "non-connectivity-module-test",
srcs: [
- "java/android/app/usage/*.java",
- "java/android/net/EthernetNetworkUpdateRequestTest.java",
"java/android/net/Ikev2VpnProfileTest.java",
"java/android/net/IpMemoryStoreTest.java",
- "java/android/net/IpSecAlgorithmTest.java",
- "java/android/net/IpSecConfigTest.java",
- "java/android/net/IpSecManagerTest.java",
- "java/android/net/IpSecTransformTest.java",
- "java/android/net/KeepalivePacketDataUtilTest.java",
- "java/android/net/NetworkIdentityTest.kt",
- "java/android/net/NetworkStats*.java",
- "java/android/net/NetworkTemplateTest.kt",
"java/android/net/TelephonyNetworkSpecifierTest.java",
"java/android/net/VpnManagerTest.java",
"java/android/net/ipmemorystore/*.java",
"java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt",
- "java/android/net/nsd/*.java",
"java/com/android/internal/net/NetworkUtilsInternalTest.java",
"java/com/android/internal/net/VpnProfileTest.java",
- "java/com/android/server/IpSecServiceParameterizedTest.java",
- "java/com/android/server/IpSecServiceRefcountedResourceTest.java",
- "java/com/android/server/IpSecServiceTest.java",
"java/com/android/server/NetworkManagementServiceTest.java",
- "java/com/android/server/NsdServiceTest.java",
+ "java/com/android/server/VpnManagerServiceTest.java",
"java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
"java/com/android/server/connectivity/IpConnectivityMetricsTest.java",
"java/com/android/server/connectivity/MultipathPolicyTrackerTest.java",
"java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
"java/com/android/server/connectivity/VpnTest.java",
- "java/com/android/server/ethernet/*.java",
"java/com/android/server/net/ipmemorystore/*.java",
- "java/com/android/server/net/BpfInterfaceMapUpdaterTest.java",
- "java/com/android/server/net/IpConfigStoreTest.java",
- "java/com/android/server/net/NetworkStats*.java",
- "java/com/android/server/net/TestableUsageCallback.kt",
+ "java/com/android/server/connectivity/mdns/**/*.java",
]
}
@@ -107,7 +89,7 @@
name: "FrameworksNetTestsDefaults",
min_sdk_version: "30",
defaults: [
- "framework-connectivity-test-defaults",
+ "framework-connectivity-internal-test-defaults",
],
srcs: [
"java/**/*.java",
@@ -163,6 +145,7 @@
static_libs: [
"services.core",
"services.net",
+ "service-mdns",
],
jni_libs: [
"libandroid_net_connectivity_com_android_net_module_util_jni",
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index 887f171..54e1cd0 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -50,7 +50,7 @@
<uses-permission android:name="android.permission.NETWORK_STATS_PROVIDER" />
<uses-permission android:name="android.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE" />
- <application>
+ <application android:testOnly="true">
<uses-library android:name="android.test.runner" />
<uses-library android:name="android.net.ipsec.ike" />
<activity
diff --git a/tests/unit/AndroidTest.xml b/tests/unit/AndroidTest.xml
index 939ae49..2d32e55 100644
--- a/tests/unit/AndroidTest.xml
+++ b/tests/unit/AndroidTest.xml
@@ -15,7 +15,8 @@
-->
<configuration description="Runs Frameworks Networking Tests.">
<target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
- <option name="test-file-name" value="FrameworksNetTests.apk" />
+ <option name="test-file-name" value="FrameworksNetTests.apk" />
+ <option name="install-arg" value="-t" />
</target_preparer>
<option name="test-suite-tag" value="apct" />
diff --git a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
index 561e621..b1b76ec 100644
--- a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
+++ b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
@@ -54,7 +54,7 @@
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NetworkStatsManagerTest {
private static final String TEST_SUBSCRIBER_ID = "subid";
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index f324630..c327868 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -41,6 +41,7 @@
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.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -72,6 +73,7 @@
import androidx.test.filters.SmallTest;
+import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -82,6 +84,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.lang.ref.WeakReference;
+
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
@@ -461,4 +465,49 @@
}
fail("expected exception of type " + throwableType);
}
+
+ private static class MockContext extends BroadcastInterceptingContext {
+ MockContext(Context base) {
+ super(base);
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return mock(Context.class);
+ }
+ }
+
+ private WeakReference<Context> makeConnectivityManagerAndReturnContext() {
+ // Mockito may have an internal reference to the mock, creating MockContext for testing.
+ final Context c = new MockContext(mock(Context.class));
+
+ new ConnectivityManager(c, mService);
+
+ return new WeakReference<>(c);
+ }
+
+ private void forceGC() {
+ // First GC ensures that objects are collected for finalization, then second GC ensures
+ // they're garbage-collected after being finalized.
+ System.gc();
+ System.runFinalization();
+ System.gc();
+ }
+
+ @Test
+ public void testConnectivityManagerDoesNotLeakContext() throws Exception {
+ final WeakReference<Context> ref = makeConnectivityManagerAndReturnContext();
+
+ final int attempts = 100;
+ final long waitIntervalMs = 50;
+ for (int i = 0; i < attempts; i++) {
+ forceGC();
+ if (ref.get() == null) break;
+
+ Thread.sleep(waitIntervalMs);
+ }
+
+ assertNull("ConnectivityManager weak reference still not null after " + attempts
+ + " attempts", ref.get());
+ }
}
diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
index 8222ca1..5cb014f 100644
--- a/tests/unit/java/android/net/Ikev2VpnProfileTest.java
+++ b/tests/unit/java/android/net/Ikev2VpnProfileTest.java
@@ -17,7 +17,7 @@
package android.net;
import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V6;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
@@ -448,7 +448,7 @@
@Test
public void testConversionIsLosslessWithIkeTunConnParams() throws Exception {
final IkeTunnelConnectionParams tunnelParams =
- new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+ new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
// Config authentication related fields is not required while building with
// IkeTunnelConnectionParams.
final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams).build();
@@ -464,9 +464,9 @@
// Verify building with IkeTunnelConnectionParams
final IkeTunnelConnectionParams tunnelParams =
- new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+ new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
final IkeTunnelConnectionParams tunnelParams2 =
- new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS);
+ new IkeTunnelConnectionParams(IKE_PARAMS_V6, CHILD_PARAMS);
assertEquals(new Ikev2VpnProfile.Builder(tunnelParams).build(),
new Ikev2VpnProfile.Builder(tunnelParams2).build());
}
diff --git a/tests/unit/java/android/net/IpSecAlgorithmTest.java b/tests/unit/java/android/net/IpSecAlgorithmTest.java
index c473e82..1482055 100644
--- a/tests/unit/java/android/net/IpSecAlgorithmTest.java
+++ b/tests/unit/java/android/net/IpSecAlgorithmTest.java
@@ -47,7 +47,7 @@
/** Unit tests for {@link IpSecAlgorithm}. */
@SmallTest
@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class IpSecAlgorithmTest {
private static final byte[] KEY_MATERIAL;
diff --git a/tests/unit/java/android/net/IpSecConfigTest.java b/tests/unit/java/android/net/IpSecConfigTest.java
index b87cb48..9f83036 100644
--- a/tests/unit/java/android/net/IpSecConfigTest.java
+++ b/tests/unit/java/android/net/IpSecConfigTest.java
@@ -36,7 +36,7 @@
/** Unit tests for {@link IpSecConfig}. */
@SmallTest
@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class IpSecConfigTest {
@Test
diff --git a/tests/unit/java/android/net/IpSecManagerTest.java b/tests/unit/java/android/net/IpSecManagerTest.java
index cda8eb7..335f539 100644
--- a/tests/unit/java/android/net/IpSecManagerTest.java
+++ b/tests/unit/java/android/net/IpSecManagerTest.java
@@ -52,7 +52,7 @@
/** Unit tests for {@link IpSecManager}. */
@SmallTest
@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class IpSecManagerTest {
private static final int TEST_UDP_ENCAP_PORT = 34567;
diff --git a/tests/unit/java/android/net/IpSecTransformTest.java b/tests/unit/java/android/net/IpSecTransformTest.java
index 81375f1..c1bd719 100644
--- a/tests/unit/java/android/net/IpSecTransformTest.java
+++ b/tests/unit/java/android/net/IpSecTransformTest.java
@@ -32,7 +32,7 @@
/** Unit tests for {@link IpSecTransform}. */
@SmallTest
@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class IpSecTransformTest {
@Test
diff --git a/tests/unit/java/android/net/NetworkIdentitySetTest.kt b/tests/unit/java/android/net/NetworkIdentitySetTest.kt
new file mode 100644
index 0000000..d61ebf9
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkIdentitySetTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net
+
+import android.content.Context
+import android.net.ConnectivityManager.TYPE_MOBILE
+import android.os.Build
+import android.telephony.TelephonyManager
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import kotlin.test.assertEquals
+
+private const val TEST_IMSI1 = "testimsi1"
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkIdentitySetTest {
+ private val mockContext = mock(Context::class.java)
+
+ private fun buildMobileNetworkStateSnapshot(
+ caps: NetworkCapabilities,
+ subscriberId: String
+ ): NetworkStateSnapshot {
+ return NetworkStateSnapshot(mock(Network::class.java), caps,
+ LinkProperties(), subscriberId, TYPE_MOBILE)
+ }
+
+ @Test
+ fun testCompare() {
+ val ident1 = NetworkIdentity.buildNetworkIdentity(mockContext,
+ buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI1),
+ false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+ val ident2 = NetworkIdentity.buildNetworkIdentity(mockContext,
+ buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI1),
+ true /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+
+ // Verify that the results of comparing two empty sets are equal
+ assertEquals(0, NetworkIdentitySet.compare(NetworkIdentitySet(), NetworkIdentitySet()))
+
+ val identSet1 = NetworkIdentitySet()
+ val identSet2 = NetworkIdentitySet()
+ identSet1.add(ident1)
+ identSet2.add(ident2)
+ assertEquals(-1, NetworkIdentitySet.compare(NetworkIdentitySet(), identSet1))
+ assertEquals(1, NetworkIdentitySet.compare(identSet1, NetworkIdentitySet()))
+ assertEquals(0, NetworkIdentitySet.compare(identSet1, identSet1))
+ assertEquals(-1, NetworkIdentitySet.compare(identSet1, identSet2))
+ }
+}
diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt
index bf5568d..d84328c 100644
--- a/tests/unit/java/android/net/NetworkIdentityTest.kt
+++ b/tests/unit/java/android/net/NetworkIdentityTest.kt
@@ -47,7 +47,7 @@
private const val TEST_SUBID2 = 2
@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class NetworkIdentityTest {
private val mockContext = mock(Context::class.java)
diff --git a/tests/unit/java/android/net/NetworkStatsAccessTest.java b/tests/unit/java/android/net/NetworkStatsAccessTest.java
index 97a93ca..a74056b 100644
--- a/tests/unit/java/android/net/NetworkStatsAccessTest.java
+++ b/tests/unit/java/android/net/NetworkStatsAccessTest.java
@@ -19,6 +19,7 @@
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.when;
import android.Manifest;
@@ -66,6 +67,10 @@
when(mContext.getSystemServiceName(DevicePolicyManager.class))
.thenReturn(Context.DEVICE_POLICY_SERVICE);
when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn(mDpm);
+ if (mContext.getSystemService(DevicePolicyManager.class) == null) {
+ // Test is using mockito-extended
+ doCallRealMethod().when(mContext).getSystemService(DevicePolicyManager.class);
+ }
setHasCarrierPrivileges(false);
setIsDeviceOwner(false);
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index 32c106d..b518a61 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -37,6 +37,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
+import android.annotation.NonNull;
import android.content.res.Resources;
import android.net.NetworkStatsCollection.Key;
import android.os.Process;
@@ -76,6 +77,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Tests for {@link NetworkStatsCollection}.
@@ -534,50 +536,84 @@
assertThrows(ArithmeticException.class, () -> multiplySafeByRational(30, 3, 0));
}
+ private static void assertCollectionEntries(
+ @NonNull Map<Key, NetworkStatsHistory> expectedEntries,
+ @NonNull NetworkStatsCollection collection) {
+ final Map<Key, NetworkStatsHistory> actualEntries = collection.getEntries();
+ assertEquals(expectedEntries.size(), actualEntries.size());
+ for (Key expectedKey : expectedEntries.keySet()) {
+ final NetworkStatsHistory expectedHistory = expectedEntries.get(expectedKey);
+ final NetworkStatsHistory actualHistory = actualEntries.get(expectedKey);
+ assertNotNull(actualHistory);
+ assertEquals(expectedHistory.getEntries(), actualHistory.getEntries());
+ actualEntries.remove(expectedKey);
+ }
+ assertEquals(0, actualEntries.size());
+ }
+
@Test
- public void testBuilder() {
- final Map<Key, NetworkStatsHistory> expectedEntries = new ArrayMap<>();
- final NetworkStats.Entry entry = new NetworkStats.Entry();
- final NetworkIdentitySet ident = new NetworkIdentitySet();
- final Key key1 = new Key(ident, 0, 0, 0);
- final Key key2 = new Key(ident, 1, 0, 0);
+ public void testRemoveHistoryBefore() {
+ final NetworkIdentity testIdent = new NetworkIdentity.Builder()
+ .setSubscriberId(TEST_IMSI).build();
+ final Key key1 = new Key(Set.of(testIdent), 0, 0, 0);
+ final Key key2 = new Key(Set.of(testIdent), 1, 0, 0);
final long bucketDuration = 10;
+ // Prepare entries for testing, with different bucket start timestamps.
final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 10, 40,
4, 50, 5, 60);
- final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 10, 3,
+ final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(20, 10, 3,
41, 7, 1, 0);
+ final NetworkStatsHistory.Entry entry3 = new NetworkStatsHistory.Entry(30, 10, 1,
+ 21, 70, 4, 1);
NetworkStatsHistory history1 = new NetworkStatsHistory.Builder(10, 5)
.addEntry(entry1)
.addEntry(entry2)
.build();
-
- NetworkStatsHistory history2 = new NetworkStatsHistory(10, 5);
-
- NetworkStatsCollection actualCollection = new NetworkStatsCollection.Builder(bucketDuration)
+ NetworkStatsHistory history2 = new NetworkStatsHistory.Builder(10, 5)
+ .addEntry(entry2)
+ .addEntry(entry3)
+ .build();
+ NetworkStatsCollection collection = new NetworkStatsCollection.Builder(bucketDuration)
.addEntry(key1, history1)
.addEntry(key2, history2)
.build();
- // The builder will omit any entry with empty history. Thus, history2
- // is not expected in the result collection.
+ // Verify nothing is removed if the cutoff time is equal to bucketStart.
+ collection.removeHistoryBefore(10);
+ final Map<Key, NetworkStatsHistory> expectedEntries = new ArrayMap<>();
expectedEntries.put(key1, history1);
+ expectedEntries.put(key2, history2);
+ assertCollectionEntries(expectedEntries, collection);
- final Map<Key, NetworkStatsHistory> actualEntries = actualCollection.getEntries();
+ // Verify entry1 will be removed if its bucket start before to cutoff timestamp.
+ collection.removeHistoryBefore(11);
+ history1 = new NetworkStatsHistory.Builder(10, 5)
+ .addEntry(entry2)
+ .build();
+ history2 = new NetworkStatsHistory.Builder(10, 5)
+ .addEntry(entry2)
+ .addEntry(entry3)
+ .build();
+ final Map<Key, NetworkStatsHistory> cutoff1Entries1 = new ArrayMap<>();
+ cutoff1Entries1.put(key1, history1);
+ cutoff1Entries1.put(key2, history2);
+ assertCollectionEntries(cutoff1Entries1, collection);
- assertEquals(expectedEntries.size(), actualEntries.size());
- for (Key expectedKey : expectedEntries.keySet()) {
- final NetworkStatsHistory expectedHistory = expectedEntries.get(expectedKey);
+ // Verify entry2 will be removed if its bucket start covers by cutoff timestamp.
+ collection.removeHistoryBefore(22);
+ history2 = new NetworkStatsHistory.Builder(10, 5)
+ .addEntry(entry3)
+ .build();
+ final Map<Key, NetworkStatsHistory> cutoffEntries2 = new ArrayMap<>();
+ // History1 is not expected since the collection will omit empty entries.
+ cutoffEntries2.put(key2, history2);
+ assertCollectionEntries(cutoffEntries2, collection);
- final NetworkStatsHistory actualHistory = actualEntries.get(expectedKey);
- assertNotNull(actualHistory);
-
- assertEquals(expectedHistory.getEntries(), actualHistory.getEntries());
-
- actualEntries.remove(expectedKey);
- }
- assertEquals(0, actualEntries.size());
+ // Verify all entries will be removed if cutoff timestamp covers all.
+ collection.removeHistoryBefore(Long.MAX_VALUE);
+ assertEquals(0, collection.getEntries().size());
}
/**
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index c170605..43e331b 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -56,12 +56,11 @@
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
-import java.util.List;
import java.util.Random;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NetworkStatsHistoryTest {
private static final String TAG = "NetworkStatsHistoryTest";
@@ -271,7 +270,7 @@
}
@Test
- public void testRemove() throws Exception {
+ public void testRemoveStartingBefore() throws Exception {
stats = new NetworkStatsHistory(HOUR_IN_MILLIS);
// record some data across 24 buckets
@@ -279,28 +278,28 @@
assertEquals(24, stats.size());
// try removing invalid data; should be no change
- stats.removeBucketsBefore(0 - DAY_IN_MILLIS);
+ stats.removeBucketsStartingBefore(0 - DAY_IN_MILLIS);
assertEquals(24, stats.size());
// try removing far before buckets; should be no change
- stats.removeBucketsBefore(TEST_START - YEAR_IN_MILLIS);
+ stats.removeBucketsStartingBefore(TEST_START - YEAR_IN_MILLIS);
assertEquals(24, stats.size());
// try removing just moments into first bucket; should be no change
- // since that bucket contains data beyond the cutoff
- stats.removeBucketsBefore(TEST_START + SECOND_IN_MILLIS);
+ // since that bucket doesn't contain data starts before the cutoff
+ stats.removeBucketsStartingBefore(TEST_START);
assertEquals(24, stats.size());
// try removing single bucket
- stats.removeBucketsBefore(TEST_START + HOUR_IN_MILLIS);
+ stats.removeBucketsStartingBefore(TEST_START + HOUR_IN_MILLIS);
assertEquals(23, stats.size());
// try removing multiple buckets
- stats.removeBucketsBefore(TEST_START + (4 * HOUR_IN_MILLIS));
+ stats.removeBucketsStartingBefore(TEST_START + (4 * HOUR_IN_MILLIS));
assertEquals(20, stats.size());
// try removing all buckets
- stats.removeBucketsBefore(TEST_START + YEAR_IN_MILLIS);
+ stats.removeBucketsStartingBefore(TEST_START + YEAR_IN_MILLIS);
assertEquals(0, stats.size());
}
@@ -350,7 +349,7 @@
stats.recordData(start, end, entry);
} else {
// trim something
- stats.removeBucketsBefore(r.nextLong());
+ stats.removeBucketsStartingBefore(r.nextLong());
}
}
assertConsistent(stats);
@@ -533,40 +532,6 @@
assertEquals(512L + 4096L, stats.getTotalBytes());
}
- @Test
- public void testBuilder() {
- final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 30, 40,
- 4, 50, 5, 60);
- final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 15, 3,
- 41, 7, 1, 0);
- final NetworkStatsHistory.Entry entry3 = new NetworkStatsHistory.Entry(7, 301, 11,
- 14, 31, 2, 80);
-
- final NetworkStatsHistory statsEmpty = new NetworkStatsHistory
- .Builder(HOUR_IN_MILLIS, 10).build();
- assertEquals(0, statsEmpty.getEntries().size());
- assertEquals(HOUR_IN_MILLIS, statsEmpty.getBucketDuration());
-
- NetworkStatsHistory statsSingle = new NetworkStatsHistory
- .Builder(HOUR_IN_MILLIS, 8)
- .addEntry(entry1)
- .build();
- assertEquals(1, statsSingle.getEntries().size());
- assertEquals(HOUR_IN_MILLIS, statsSingle.getBucketDuration());
- assertEquals(entry1, statsSingle.getEntries().get(0));
-
- NetworkStatsHistory statsMultiple = new NetworkStatsHistory
- .Builder(SECOND_IN_MILLIS, 0)
- .addEntry(entry1).addEntry(entry2).addEntry(entry3)
- .build();
- final List<NetworkStatsHistory.Entry> entries = statsMultiple.getEntries();
- assertEquals(3, entries.size());
- assertEquals(SECOND_IN_MILLIS, statsMultiple.getBucketDuration());
- assertEquals(entry1, entries.get(0));
- assertEquals(entry2, entries.get(1));
- assertEquals(entry3, entries.get(2));
- }
-
private static void assertIndexBeforeAfter(
NetworkStatsHistory stats, int before, int after, long time) {
assertEquals("unexpected before", before, stats.getIndexBefore(time));
diff --git a/tests/unit/java/android/net/NetworkStatsRecorderTest.java b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
new file mode 100644
index 0000000..fad11a3
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *i
+ * 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.net;
+
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.NetworkStats;
+import android.os.DropBoxManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.FileRotator;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public final class NetworkStatsRecorderTest {
+ private static final String TAG = NetworkStatsRecorderTest.class.getSimpleName();
+
+ private static final String TEST_PREFIX = "test";
+
+ @Mock private DropBoxManager mDropBox;
+ @Mock private NetworkStats.NonMonotonicObserver mObserver;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ private NetworkStatsRecorder buildRecorder(FileRotator rotator, boolean wipeOnError) {
+ return new NetworkStatsRecorder(rotator, mObserver, mDropBox, TEST_PREFIX,
+ HOUR_IN_MILLIS, false /* includeTags */, wipeOnError);
+ }
+
+ @Test
+ public void testWipeOnError() throws Exception {
+ final FileRotator rotator = mock(FileRotator.class);
+ final NetworkStatsRecorder wipeOnErrorRecorder = buildRecorder(rotator, true);
+
+ // Assuming that the rotator gets an exception happened when read data.
+ doThrow(new IOException()).when(rotator).readMatching(any(), anyLong(), anyLong());
+ wipeOnErrorRecorder.getOrLoadPartialLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+ // Verify that the files will be deleted.
+ verify(rotator, times(1)).deleteAll();
+ reset(rotator);
+
+ final NetworkStatsRecorder noWipeOnErrorRecorder = buildRecorder(rotator, false);
+ doThrow(new IOException()).when(rotator).readMatching(any(), anyLong(), anyLong());
+ noWipeOnErrorRecorder.getOrLoadPartialLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+ // Verify that the rotator won't delete files.
+ verify(rotator, never()).deleteAll();
+ }
+}
diff --git a/tests/unit/java/android/net/NetworkStatsTest.java b/tests/unit/java/android/net/NetworkStatsTest.java
index b0cc16c..6d79869 100644
--- a/tests/unit/java/android/net/NetworkStatsTest.java
+++ b/tests/unit/java/android/net/NetworkStatsTest.java
@@ -61,7 +61,7 @@
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NetworkStatsTest {
private static final String TEST_IFACE = "test0";
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 453612f..3e9662d 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -29,12 +29,8 @@
import android.net.NetworkStats.METERED_NO
import android.net.NetworkStats.METERED_YES
import android.net.NetworkStats.ROAMING_ALL
-import android.net.NetworkTemplate.MATCH_BLUETOOTH
-import android.net.NetworkTemplate.MATCH_CARRIER
-import android.net.NetworkTemplate.MATCH_ETHERNET
import android.net.NetworkTemplate.MATCH_MOBILE
import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD
-import android.net.NetworkTemplate.MATCH_PROXY
import android.net.NetworkTemplate.MATCH_WIFI
import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
import android.net.NetworkTemplate.NETWORK_TYPE_ALL
@@ -52,11 +48,9 @@
import android.net.wifi.WifiInfo
import android.os.Build
import android.telephony.TelephonyManager
-import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_ALL
import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.SC_V2
import com.android.testutils.assertParcelSane
import org.junit.Before
import org.junit.Test
@@ -65,7 +59,6 @@
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
@@ -77,7 +70,7 @@
private const val TEST_WIFI_KEY2 = "wifiKey2"
@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class NetworkTemplateTest {
private val mockContext = mock(Context::class.java)
private val mockWifiInfo = mock(WifiInfo::class.java)
@@ -555,140 +548,4 @@
it.assertMatches(identMobileImsi3)
}
}
-
- @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
- @Test
- fun testBuilderMatchRules() {
- // Verify unknown match rules cannot construct templates.
- listOf(Integer.MIN_VALUE, -1, Integer.MAX_VALUE).forEach {
- assertFailsWith<IllegalArgumentException> {
- NetworkTemplate.Builder(it).build()
- }
- }
-
- // Verify hidden match rules cannot construct templates.
- listOf(MATCH_WIFI_WILDCARD, MATCH_MOBILE_WILDCARD, MATCH_PROXY).forEach {
- assertFailsWith<IllegalArgumentException> {
- NetworkTemplate.Builder(it).build()
- }
- }
-
- // Verify template which matches metered cellular and carrier networks with
- // the given IMSI. See buildTemplateMobileAll and buildTemplateCarrierMetered.
- listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
- NetworkTemplate.Builder(matchRule).setSubscriberIds(setOf(TEST_IMSI1))
- .setMeteredness(METERED_YES).build().let {
- val expectedTemplate = NetworkTemplate(matchRule, TEST_IMSI1,
- arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
- ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
- assertEquals(expectedTemplate, it)
- }
- }
-
- // Verify carrier template cannot be created without IMSI.
- assertFailsWith<IllegalArgumentException> {
- NetworkTemplate.Builder(MATCH_CARRIER).build()
- }
-
- // Verify template which matches metered cellular networks,
- // regardless of IMSI. See buildTemplateMobileWildcard.
- NetworkTemplate.Builder(MATCH_MOBILE).setMeteredness(METERED_YES).build().let {
- val expectedTemplate = NetworkTemplate(MATCH_MOBILE_WILDCARD, null /*subscriberId*/,
- null /*subscriberIds*/, arrayOf<String>(),
- METERED_YES, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
- assertEquals(expectedTemplate, it)
- }
-
- // Verify template which matches metered cellular networks and ratType.
- // See NetworkTemplate#buildTemplateMobileWithRatType.
- NetworkTemplate.Builder(MATCH_MOBILE).setSubscriberIds(setOf(TEST_IMSI1))
- .setMeteredness(METERED_YES).setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
- .build().let {
- val expectedTemplate = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1,
- arrayOf(TEST_IMSI1), arrayOf<String>(), METERED_YES,
- ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_UMTS,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
- assertEquals(expectedTemplate, it)
- }
-
- // Verify template which matches all wifi networks,
- // regardless of Wifi Network Key. See buildTemplateWifiWildcard and buildTemplateWifi.
- NetworkTemplate.Builder(MATCH_WIFI).build().let {
- val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
- null /*subscriberIds*/, arrayOf<String>(),
- METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
- assertEquals(expectedTemplate, it)
- }
-
- // Verify template which matches wifi networks with the given Wifi Network Key.
- // See buildTemplateWifi(wifiNetworkKey).
- NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
- val expectedTemplate = NetworkTemplate(MATCH_WIFI, null /*subscriberId*/,
- null /*subscriberIds*/, arrayOf(TEST_WIFI_KEY1),
- METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
- assertEquals(expectedTemplate, it)
- }
-
- // Verify template which matches all wifi networks with the
- // given Wifi Network Key, and IMSI. See buildTemplateWifi(wifiNetworkKey, subscriberId).
- NetworkTemplate.Builder(MATCH_WIFI).setSubscriberIds(setOf(TEST_IMSI1))
- .setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build().let {
- val expectedTemplate = NetworkTemplate(MATCH_WIFI, TEST_IMSI1,
- arrayOf(TEST_IMSI1), arrayOf(TEST_WIFI_KEY1),
- METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT)
- assertEquals(expectedTemplate, it)
- }
-
- // Verify template which matches ethernet and bluetooth networks.
- // See buildTemplateEthernet and buildTemplateBluetooth.
- listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
- NetworkTemplate.Builder(matchRule).build().let {
- val expectedTemplate = NetworkTemplate(matchRule, null /*subscriberId*/,
- null /*subscriberIds*/, arrayOf<String>(),
- METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
- assertEquals(expectedTemplate, it)
- }
- }
- }
-
- @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
- @Test
- fun testBuilderWifiNetworkKeys() {
- // Verify template builder which generates same template with the given different
- // sequence keys.
- NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
- setOf(TEST_WIFI_KEY1, TEST_WIFI_KEY2)).build().let {
- val expectedTemplate = NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(
- setOf(TEST_WIFI_KEY2, TEST_WIFI_KEY1)).build()
- assertEquals(expectedTemplate, it)
- }
-
- // Verify template which matches non-wifi networks with the given key is invalid.
- listOf(MATCH_MOBILE, MATCH_CARRIER, MATCH_ETHERNET, MATCH_BLUETOOTH, -1,
- Integer.MAX_VALUE).forEach { matchRule ->
- assertFailsWith<IllegalArgumentException> {
- NetworkTemplate.Builder(matchRule).setWifiNetworkKeys(setOf(TEST_WIFI_KEY1)).build()
- }
- }
-
- // Verify template which matches wifi networks with the given null key is invalid.
- assertFailsWith<IllegalArgumentException> {
- NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf(null)).build()
- }
-
- // Verify template which matches wifi wildcard with the given empty key set.
- NetworkTemplate.Builder(MATCH_WIFI).setWifiNetworkKeys(setOf<String>()).build().let {
- val expectedTemplate = NetworkTemplate(MATCH_WIFI_WILDCARD, null /*subscriberId*/,
- arrayOf<String>() /*subscriberIds*/, arrayOf<String>(),
- METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL,
- OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_ALL)
- assertEquals(expectedTemplate, it)
- }
- }
}
diff --git a/tests/unit/java/android/net/QosSocketFilterTest.java b/tests/unit/java/android/net/QosSocketFilterTest.java
index 91f2cdd..6820b40 100644
--- a/tests/unit/java/android/net/QosSocketFilterTest.java
+++ b/tests/unit/java/android/net/QosSocketFilterTest.java
@@ -16,8 +16,17 @@
package android.net;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_CONNECTED;
+import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import android.os.Build;
@@ -29,6 +38,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@@ -36,14 +46,14 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
public class QosSocketFilterTest {
-
+ private static final int TEST_NET_ID = 1777;
+ private final Network mNetwork = new Network(TEST_NET_ID);
@Test
public void testPortExactMatch() {
final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4");
assertTrue(QosSocketFilter.matchesAddress(
new InetSocketAddress(addressA, 10), addressB, 10, 10));
-
}
@Test
@@ -77,5 +87,90 @@
assertFalse(QosSocketFilter.matchesAddress(
new InetSocketAddress(addressA, 10), addressB, 10, 10));
}
+
+ @Test
+ public void testAddressMatchWithAnyLocalAddresses() {
+ final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
+ final InetAddress addressB = InetAddresses.parseNumericAddress("0.0.0.0");
+ assertTrue(QosSocketFilter.matchesAddress(
+ new InetSocketAddress(addressA, 10), addressB, 10, 10));
+ assertFalse(QosSocketFilter.matchesAddress(
+ new InetSocketAddress(addressB, 10), addressA, 10, 10));
+ }
+
+ @Test
+ public void testProtocolMatch() throws Exception {
+ DatagramSocket socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 0));
+ socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 10));
+ DatagramSocket socketV6 = new DatagramSocket(new InetSocketAddress("::1", 0));
+ socketV6.connect(new InetSocketAddress("::1", socketV6.getLocalPort() + 10));
+ QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+ QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+ QosSocketInfo socketInfo6 = new QosSocketInfo(mNetwork, socketV6);
+ QosSocketFilter socketFilter6 = new QosSocketFilter(socketInfo6);
+ assertTrue(socketFilter.matchesProtocol(IPPROTO_UDP));
+ assertTrue(socketFilter6.matchesProtocol(IPPROTO_UDP));
+ assertFalse(socketFilter.matchesProtocol(IPPROTO_TCP));
+ assertFalse(socketFilter6.matchesProtocol(IPPROTO_TCP));
+ socket.close();
+ socketV6.close();
+ }
+
+ @Test
+ public void testValidate() throws Exception {
+ DatagramSocket socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 0));
+ socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 7));
+ DatagramSocket socketV6 = new DatagramSocket(new InetSocketAddress("::1", 0));
+
+ QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+ QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+ QosSocketInfo socketInfo6 = new QosSocketInfo(mNetwork, socketV6);
+ QosSocketFilter socketFilter6 = new QosSocketFilter(socketInfo6);
+ assertEquals(EX_TYPE_FILTER_NONE, socketFilter.validate());
+ assertEquals(EX_TYPE_FILTER_NONE, socketFilter6.validate());
+ socket.close();
+ socketV6.close();
+ }
+
+ @Test
+ public void testValidateUnbind() throws Exception {
+ DatagramSocket socket;
+ socket = new DatagramSocket(null);
+ QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+ QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+ assertEquals(EX_TYPE_FILTER_SOCKET_NOT_BOUND, socketFilter.validate());
+ socket.close();
+ }
+
+ @Test
+ public void testValidateLocalAddressChanged() throws Exception {
+ DatagramSocket socket = new DatagramSocket(null);
+ DatagramSocket socket6 = new DatagramSocket(null);
+ QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+ QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+ QosSocketInfo socketInfo6 = new QosSocketInfo(mNetwork, socket6);
+ QosSocketFilter socketFilter6 = new QosSocketFilter(socketInfo6);
+ socket.bind(new InetSocketAddress("127.0.0.1", 0));
+ socket6.bind(new InetSocketAddress("::1", 0));
+ assertEquals(EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED, socketFilter.validate());
+ assertEquals(EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED, socketFilter6.validate());
+ socket.close();
+ socket6.close();
+ }
+
+ @Test
+ public void testValidateRemoteAddressChanged() throws Exception {
+ DatagramSocket socket;
+ socket = new DatagramSocket(new InetSocketAddress("127.0.0.1", 53137));
+ socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 11));
+ QosSocketInfo socketInfo = new QosSocketInfo(mNetwork, socket);
+ QosSocketFilter socketFilter = new QosSocketFilter(socketInfo);
+ assertEquals(EX_TYPE_FILTER_NONE, socketFilter.validate());
+ socket.disconnect();
+ assertEquals(EX_TYPE_FILTER_SOCKET_NOT_CONNECTED, socketFilter.validate());
+ socket.connect(new InetSocketAddress("127.0.0.1", socket.getLocalPort() + 13));
+ assertEquals(EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED, socketFilter.validate());
+ socket.close();
+ }
}
diff --git a/tests/unit/java/android/net/QosSocketInfoTest.java b/tests/unit/java/android/net/QosSocketInfoTest.java
new file mode 100644
index 0000000..749c182
--- /dev/null
+++ b/tests/unit/java/android/net/QosSocketInfoTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_STREAM;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class QosSocketInfoTest {
+ @Mock
+ private Network mMockNetwork = mock(Network.class);
+
+ @Test
+ public void testConstructWithSock() throws Exception {
+ ServerSocket server = new ServerSocket();
+ ServerSocket server6 = new ServerSocket();
+
+ InetSocketAddress clientAddr = new InetSocketAddress("127.0.0.1", 0);
+ InetSocketAddress serverAddr = new InetSocketAddress("127.0.0.1", 0);
+ InetSocketAddress clientAddr6 = new InetSocketAddress("::1", 0);
+ InetSocketAddress serverAddr6 = new InetSocketAddress("::1", 0);
+ server.bind(serverAddr);
+ server6.bind(serverAddr6);
+ Socket socket = new Socket(serverAddr.getAddress(), server.getLocalPort(),
+ clientAddr.getAddress(), clientAddr.getPort());
+ Socket socket6 = new Socket(serverAddr6.getAddress(), server6.getLocalPort(),
+ clientAddr6.getAddress(), clientAddr6.getPort());
+ QosSocketInfo sockInfo = new QosSocketInfo(mMockNetwork, socket);
+ QosSocketInfo sockInfo6 = new QosSocketInfo(mMockNetwork, socket6);
+ assertTrue(sockInfo.getLocalSocketAddress()
+ .equals(new InetSocketAddress(socket.getLocalAddress(), socket.getLocalPort())));
+ assertTrue(sockInfo.getRemoteSocketAddress()
+ .equals((InetSocketAddress) socket.getRemoteSocketAddress()));
+ assertEquals(SOCK_STREAM, sockInfo.getSocketType());
+ assertTrue(sockInfo6.getLocalSocketAddress()
+ .equals(new InetSocketAddress(socket6.getLocalAddress(), socket6.getLocalPort())));
+ assertTrue(sockInfo6.getRemoteSocketAddress()
+ .equals((InetSocketAddress) socket6.getRemoteSocketAddress()));
+ assertEquals(SOCK_STREAM, sockInfo6.getSocketType());
+ socket.close();
+ socket6.close();
+ server.close();
+ server6.close();
+ }
+
+ @Test
+ public void testConstructWithDatagramSock() throws Exception {
+ InetSocketAddress clientAddr = new InetSocketAddress("127.0.0.1", 0);
+ InetSocketAddress serverAddr = new InetSocketAddress("127.0.0.1", 0);
+ InetSocketAddress clientAddr6 = new InetSocketAddress("::1", 0);
+ InetSocketAddress serverAddr6 = new InetSocketAddress("::1", 0);
+ DatagramSocket socket = new DatagramSocket(null);
+ socket.setReuseAddress(true);
+ socket.bind(clientAddr);
+ socket.connect(serverAddr);
+ DatagramSocket socket6 = new DatagramSocket(null);
+ socket6.setReuseAddress(true);
+ socket6.bind(clientAddr);
+ socket6.connect(serverAddr);
+ QosSocketInfo sockInfo = new QosSocketInfo(mMockNetwork, socket);
+ QosSocketInfo sockInfo6 = new QosSocketInfo(mMockNetwork, socket6);
+ assertTrue(sockInfo.getLocalSocketAddress()
+ .equals((InetSocketAddress) socket.getLocalSocketAddress()));
+ assertTrue(sockInfo.getRemoteSocketAddress()
+ .equals((InetSocketAddress) socket.getRemoteSocketAddress()));
+ assertEquals(SOCK_DGRAM, sockInfo.getSocketType());
+ assertTrue(sockInfo6.getLocalSocketAddress()
+ .equals((InetSocketAddress) socket6.getLocalSocketAddress()));
+ assertTrue(sockInfo6.getRemoteSocketAddress()
+ .equals((InetSocketAddress) socket6.getRemoteSocketAddress()));
+ assertEquals(SOCK_DGRAM, sockInfo6.getSocketType());
+ socket.close();
+ }
+}
diff --git a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
index 743d39e..aa5a246 100644
--- a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
+++ b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
@@ -61,14 +61,6 @@
assertValues(builder.build(), 55, 1814302L, 21050L, 31001636L, 26152L)
}
- @Test
- fun testMaybeReadLegacyUid() {
- val builder = NetworkStatsCollection.Builder(BUCKET_DURATION_MS)
- NetworkStatsDataMigrationUtils.readLegacyUid(builder,
- getInputStreamForResource(R.raw.netstats_uid_v4), false /* taggedData */)
- assertValues(builder.build(), 223, 106245210L, 710722L, 1130647496L, 1103989L)
- }
-
private fun assertValues(
collection: NetworkStatsCollection,
expectedSize: Int,
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 30b8fcd..32274bc 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -51,7 +51,7 @@
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NsdManagerTest {
static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
index e5e7ebc..64355ed 100644
--- a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
@@ -42,7 +42,7 @@
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NsdServiceInfoTest {
public final static InetAddress LOCALHOST;
@@ -125,6 +125,7 @@
fullInfo.setPort(4242);
fullInfo.setHost(LOCALHOST);
fullInfo.setNetwork(new Network(123));
+ fullInfo.setInterfaceIndex(456);
checkParcelable(fullInfo);
NsdServiceInfo noHostInfo = new NsdServiceInfo();
@@ -175,6 +176,7 @@
assertEquals(original.getHost(), result.getHost());
assertTrue(original.getPort() == result.getPort());
assertEquals(original.getNetwork(), result.getNetwork());
+ assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex());
// Assert equality of attribute map.
Map<String, byte[]> originalMap = original.getAttributes();
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
index 360390d..0a6d2f2 100644
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
@@ -17,7 +17,7 @@
package com.android.internal.net;
import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS;
-import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS;
+import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS_V4;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
import static com.android.testutils.ParcelUtils.assertParcelSane;
@@ -128,7 +128,7 @@
private VpnProfile getSampleIkev2ProfileWithIkeTunConnParams(String key) {
final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */,
false /* excludesLocalRoutes */, true /* requiresPlatformValidation */,
- new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS));
+ new IkeTunnelConnectionParams(IKE_PARAMS_V4, CHILD_PARAMS));
p.name = "foo";
p.server = "bar";
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index f07a10d..433b892 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -16,53 +16,446 @@
package com.android.server;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
import static android.net.INetd.PERMISSION_INTERNET;
+import static com.android.server.BpfNetMaps.DOZABLE_MATCH;
+import static com.android.server.BpfNetMaps.HAPPY_BOX_MATCH;
+import static com.android.server.BpfNetMaps.IIF_MATCH;
+import static com.android.server.BpfNetMaps.LOCKDOWN_VPN_MATCH;
+import static com.android.server.BpfNetMaps.NO_MATCH;
+import static com.android.server.BpfNetMaps.PENALTY_BOX_MATCH;
+import static com.android.server.BpfNetMaps.POWERSAVE_MATCH;
+import static com.android.server.BpfNetMaps.RESTRICTED_MATCH;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;
import static org.mockito.Mockito.verify;
import android.net.INetd;
import android.os.Build;
+import android.os.ServiceSpecificException;
import androidx.test.filters.SmallTest;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.Struct.U32;
import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.TestBpfMap;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.List;
+
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
public final class BpfNetMapsTest {
private static final String TAG = "BpfNetMapsTest";
+
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
+
private static final int TEST_UID = 10086;
private static final int[] TEST_UIDS = {10002, 10003};
- private static final String IFNAME = "wlan0";
+ private static final String TEST_IF_NAME = "wlan0";
+ private static final int TEST_IF_INDEX = 7;
+ private static final int NO_IIF = 0;
private static final String CHAINNAME = "fw_dozable";
+ private static final U32 UID_RULES_CONFIGURATION_KEY = new U32(0);
+ private static final List<Integer> FIREWALL_CHAINS = List.of(
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_STANDBY,
+ FIREWALL_CHAIN_POWERSAVE,
+ FIREWALL_CHAIN_RESTRICTED,
+ FIREWALL_CHAIN_LOW_POWER_STANDBY,
+ FIREWALL_CHAIN_OEM_DENY_1,
+ FIREWALL_CHAIN_OEM_DENY_2,
+ FIREWALL_CHAIN_OEM_DENY_3
+ );
+
private BpfNetMaps mBpfNetMaps;
@Mock INetd mNetd;
+ private final BpfMap<U32, U32> mConfigurationMap = new TestBpfMap<>(U32.class, U32.class);
+ private final BpfMap<U32, UidOwnerValue> mUidOwnerMap =
+ new TestBpfMap<>(U32.class, UidOwnerValue.class);
@Before
- public void setUp() {
+ public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+ BpfNetMaps.setConfigurationMapForTest(mConfigurationMap);
+ BpfNetMaps.setUidOwnerMapForTest(mUidOwnerMap);
mBpfNetMaps = new BpfNetMaps(mNetd);
}
@Test
public void testBpfNetMapsBeforeT() throws Exception {
assumeFalse(SdkLevel.isAtLeastT());
- mBpfNetMaps.addUidInterfaceRules(IFNAME, TEST_UIDS);
- verify(mNetd).firewallAddUidInterfaceRules(IFNAME, TEST_UIDS);
+ mBpfNetMaps.addUidInterfaceRules(TEST_IF_NAME, TEST_UIDS);
+ verify(mNetd).firewallAddUidInterfaceRules(TEST_IF_NAME, TEST_UIDS);
mBpfNetMaps.removeUidInterfaceRules(TEST_UIDS);
verify(mNetd).firewallRemoveUidInterfaceRules(TEST_UIDS);
mBpfNetMaps.setNetPermForUids(PERMISSION_INTERNET, TEST_UIDS);
verify(mNetd).trafficSetNetPermForUids(PERMISSION_INTERNET, TEST_UIDS);
}
+
+ private void doTestIsChainEnabled(final List<Integer> enableChains) throws Exception {
+ long match = 0;
+ for (final int chain: enableChains) {
+ match |= mBpfNetMaps.getMatchByFirewallChain(chain);
+ }
+ mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(match));
+
+ for (final int chain: FIREWALL_CHAINS) {
+ final String testCase = "EnabledChains: " + enableChains + " CheckedChain: " + chain;
+ if (enableChains.contains(chain)) {
+ assertTrue("Expected isChainEnabled returns True, " + testCase,
+ mBpfNetMaps.isChainEnabled(chain));
+ } else {
+ assertFalse("Expected isChainEnabled returns False, " + testCase,
+ mBpfNetMaps.isChainEnabled(chain));
+ }
+ }
+ }
+
+ private void doTestIsChainEnabled(final int enableChain) throws Exception {
+ doTestIsChainEnabled(List.of(enableChain));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testIsChainEnabled() throws Exception {
+ doTestIsChainEnabled(FIREWALL_CHAIN_DOZABLE);
+ doTestIsChainEnabled(FIREWALL_CHAIN_STANDBY);
+ doTestIsChainEnabled(FIREWALL_CHAIN_POWERSAVE);
+ doTestIsChainEnabled(FIREWALL_CHAIN_RESTRICTED);
+ doTestIsChainEnabled(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+ doTestIsChainEnabled(FIREWALL_CHAIN_OEM_DENY_1);
+ doTestIsChainEnabled(FIREWALL_CHAIN_OEM_DENY_2);
+ doTestIsChainEnabled(FIREWALL_CHAIN_OEM_DENY_3);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testIsChainEnabledMultipleChainEnabled() throws Exception {
+ doTestIsChainEnabled(List.of(
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_STANDBY));
+ doTestIsChainEnabled(List.of(
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_STANDBY,
+ FIREWALL_CHAIN_POWERSAVE,
+ FIREWALL_CHAIN_RESTRICTED));
+ doTestIsChainEnabled(FIREWALL_CHAINS);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testIsChainEnabledInvalidChain() {
+ final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
+ assertThrows(expected, () -> mBpfNetMaps.isChainEnabled(-1 /* childChain */));
+ assertThrows(expected, () -> mBpfNetMaps.isChainEnabled(1000 /* childChain */));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
+ public void testIsChainEnabledBeforeT() {
+ assertThrows(UnsupportedOperationException.class,
+ () -> mBpfNetMaps.isChainEnabled(FIREWALL_CHAIN_DOZABLE));
+ }
+
+ private void doTestSetChildChain(final List<Integer> testChains) throws Exception {
+ long expectedMatch = 0;
+ for (final int chain: testChains) {
+ expectedMatch |= mBpfNetMaps.getMatchByFirewallChain(chain);
+ }
+
+ assertEquals(0, mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+
+ for (final int chain: testChains) {
+ mBpfNetMaps.setChildChain(chain, true /* enable */);
+ }
+ assertEquals(expectedMatch, mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+
+ for (final int chain: testChains) {
+ mBpfNetMaps.setChildChain(chain, false /* enable */);
+ }
+ assertEquals(0, mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+ }
+
+ private void doTestSetChildChain(final int testChain) throws Exception {
+ doTestSetChildChain(List.of(testChain));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testSetChildChain() throws Exception {
+ mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
+ doTestSetChildChain(FIREWALL_CHAIN_DOZABLE);
+ doTestSetChildChain(FIREWALL_CHAIN_STANDBY);
+ doTestSetChildChain(FIREWALL_CHAIN_POWERSAVE);
+ doTestSetChildChain(FIREWALL_CHAIN_RESTRICTED);
+ doTestSetChildChain(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+ doTestSetChildChain(FIREWALL_CHAIN_OEM_DENY_1);
+ doTestSetChildChain(FIREWALL_CHAIN_OEM_DENY_2);
+ doTestSetChildChain(FIREWALL_CHAIN_OEM_DENY_3);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testSetChildChainMultipleChain() throws Exception {
+ mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
+ doTestSetChildChain(List.of(
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_STANDBY));
+ doTestSetChildChain(List.of(
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_STANDBY,
+ FIREWALL_CHAIN_POWERSAVE,
+ FIREWALL_CHAIN_RESTRICTED));
+ doTestSetChildChain(FIREWALL_CHAINS);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testSetChildChainInvalidChain() {
+ final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
+ assertThrows(expected,
+ () -> mBpfNetMaps.setChildChain(-1 /* childChain */, true /* enable */));
+ assertThrows(expected,
+ () -> mBpfNetMaps.setChildChain(1000 /* childChain */, true /* enable */));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
+ public void testSetChildChainBeforeT() {
+ assertThrows(UnsupportedOperationException.class,
+ () -> mBpfNetMaps.setChildChain(FIREWALL_CHAIN_DOZABLE, true /* enable */));
+ }
+
+ private void checkUidOwnerValue(final long uid, final long expectedIif,
+ final long expectedMatch) throws Exception {
+ final UidOwnerValue config = mUidOwnerMap.getValue(new U32(uid));
+ if (expectedMatch == 0) {
+ assertNull(config);
+ } else {
+ assertEquals(expectedIif, config.iif);
+ assertEquals(expectedMatch, config.rule);
+ }
+ }
+
+ private void doTestRemoveNaughtyApp(final long iif, final long match) throws Exception {
+ mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+
+ mBpfNetMaps.removeNaughtyApp(TEST_UID);
+
+ checkUidOwnerValue(TEST_UID, iif, match & ~PENALTY_BOX_MATCH);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testRemoveNaughtyApp() throws Exception {
+ doTestRemoveNaughtyApp(NO_IIF, PENALTY_BOX_MATCH);
+
+ // PENALTY_BOX_MATCH with other matches
+ doTestRemoveNaughtyApp(NO_IIF, PENALTY_BOX_MATCH | DOZABLE_MATCH | POWERSAVE_MATCH);
+
+ // PENALTY_BOX_MATCH with IIF_MATCH
+ doTestRemoveNaughtyApp(TEST_IF_INDEX, PENALTY_BOX_MATCH | IIF_MATCH);
+
+ // PENALTY_BOX_MATCH is not enabled
+ doTestRemoveNaughtyApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testRemoveNaughtyAppMissingUid() {
+ // UidOwnerMap does not have entry for TEST_UID
+ assertThrows(ServiceSpecificException.class,
+ () -> mBpfNetMaps.removeNaughtyApp(TEST_UID));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
+ public void testRemoveNaughtyAppBeforeT() {
+ assertThrows(UnsupportedOperationException.class,
+ () -> mBpfNetMaps.removeNaughtyApp(TEST_UID));
+ }
+
+ private void doTestAddNaughtyApp(final long iif, final long match) throws Exception {
+ if (match != NO_MATCH) {
+ mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+ }
+
+ mBpfNetMaps.addNaughtyApp(TEST_UID);
+
+ checkUidOwnerValue(TEST_UID, iif, match | PENALTY_BOX_MATCH);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testAddNaughtyApp() throws Exception {
+ doTestAddNaughtyApp(NO_IIF, NO_MATCH);
+
+ // Other matches are enabled
+ doTestAddNaughtyApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+
+ // IIF_MATCH is enabled
+ doTestAddNaughtyApp(TEST_IF_INDEX, IIF_MATCH);
+
+ // PENALTY_BOX_MATCH is already enabled
+ doTestAddNaughtyApp(NO_IIF, PENALTY_BOX_MATCH | DOZABLE_MATCH);
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
+ public void testAddNaughtyAppBeforeT() {
+ assertThrows(UnsupportedOperationException.class,
+ () -> mBpfNetMaps.addNaughtyApp(TEST_UID));
+ }
+
+ private void doTestRemoveNiceApp(final long iif, final long match) throws Exception {
+ mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+
+ mBpfNetMaps.removeNiceApp(TEST_UID);
+
+ checkUidOwnerValue(TEST_UID, iif, match & ~HAPPY_BOX_MATCH);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testRemoveNiceApp() throws Exception {
+ doTestRemoveNiceApp(NO_IIF, HAPPY_BOX_MATCH);
+
+ // HAPPY_BOX_MATCH with other matches
+ doTestRemoveNiceApp(NO_IIF, HAPPY_BOX_MATCH | DOZABLE_MATCH | POWERSAVE_MATCH);
+
+ // HAPPY_BOX_MATCH with IIF_MATCH
+ doTestRemoveNiceApp(TEST_IF_INDEX, HAPPY_BOX_MATCH | IIF_MATCH);
+
+ // HAPPY_BOX_MATCH is not enabled
+ doTestRemoveNiceApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testRemoveNiceAppMissingUid() {
+ // UidOwnerMap does not have entry for TEST_UID
+ assertThrows(ServiceSpecificException.class,
+ () -> mBpfNetMaps.removeNiceApp(TEST_UID));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
+ public void testRemoveNiceAppBeforeT() {
+ assertThrows(UnsupportedOperationException.class,
+ () -> mBpfNetMaps.removeNiceApp(TEST_UID));
+ }
+
+ private void doTestAddNiceApp(final long iif, final long match) throws Exception {
+ if (match != NO_MATCH) {
+ mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+ }
+
+ mBpfNetMaps.addNiceApp(TEST_UID);
+
+ checkUidOwnerValue(TEST_UID, iif, match | HAPPY_BOX_MATCH);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testAddNiceApp() throws Exception {
+ doTestAddNiceApp(NO_IIF, NO_MATCH);
+
+ // Other matches are enabled
+ doTestAddNiceApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
+
+ // IIF_MATCH is enabled
+ doTestAddNiceApp(TEST_IF_INDEX, IIF_MATCH);
+
+ // HAPPY_BOX_MATCH is already enabled
+ doTestAddNiceApp(NO_IIF, HAPPY_BOX_MATCH | DOZABLE_MATCH);
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
+ public void testAddNiceAppBeforeT() {
+ assertThrows(UnsupportedOperationException.class,
+ () -> mBpfNetMaps.addNiceApp(TEST_UID));
+ }
+
+ private void doTestUpdateUidLockdownRule(final long iif, final long match, final boolean add)
+ throws Exception {
+ if (match != NO_MATCH) {
+ mUidOwnerMap.updateEntry(new U32(TEST_UID), new UidOwnerValue(iif, match));
+ }
+
+ mBpfNetMaps.updateUidLockdownRule(TEST_UID, add);
+
+ final long expectedMatch = add ? match | LOCKDOWN_VPN_MATCH : match & ~LOCKDOWN_VPN_MATCH;
+ checkUidOwnerValue(TEST_UID, iif, expectedMatch);
+ }
+
+ private static final boolean ADD = true;
+ private static final boolean REMOVE = false;
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testUpdateUidLockdownRuleAddLockdown() throws Exception {
+ doTestUpdateUidLockdownRule(NO_IIF, NO_MATCH, ADD);
+
+ // Other matches are enabled
+ doTestUpdateUidLockdownRule(
+ NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH, ADD);
+
+ // IIF_MATCH is enabled
+ doTestUpdateUidLockdownRule(TEST_IF_INDEX, DOZABLE_MATCH, ADD);
+
+ // LOCKDOWN_VPN_MATCH is already enabled
+ doTestUpdateUidLockdownRule(NO_IIF, LOCKDOWN_VPN_MATCH | DOZABLE_MATCH, ADD);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testUpdateUidLockdownRuleRemoveLockdown() throws Exception {
+ doTestUpdateUidLockdownRule(NO_IIF, LOCKDOWN_VPN_MATCH, REMOVE);
+
+ // LOCKDOWN_VPN_MATCH with other matches
+ doTestUpdateUidLockdownRule(
+ NO_IIF, LOCKDOWN_VPN_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH, REMOVE);
+
+ // LOCKDOWN_VPN_MATCH with IIF_MATCH
+ doTestUpdateUidLockdownRule(TEST_IF_INDEX, LOCKDOWN_VPN_MATCH | IIF_MATCH, REMOVE);
+
+ // LOCKDOWN_VPN_MATCH is not enabled
+ doTestUpdateUidLockdownRule(NO_IIF, POWERSAVE_MATCH | RESTRICTED_MATCH, REMOVE);
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
+ public void testUpdateUidLockdownRuleBeforeT() {
+ assertThrows(UnsupportedOperationException.class,
+ () -> mBpfNetMaps.updateUidLockdownRule(TEST_UID, true /* add */));
+ }
}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 4c76803..0919dfc 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -51,6 +51,17 @@
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK;
@@ -104,6 +115,10 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP;
import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_2;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_3;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_4;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
@@ -136,6 +151,8 @@
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_PROFILE;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_VPN;
import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
+import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackRegister;
+import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister;
import static com.android.testutils.ConcurrentUtils.await;
import static com.android.testutils.ConcurrentUtils.durationOf;
import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
@@ -159,8 +176,8 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -194,6 +211,7 @@
import android.app.AppOpsManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
import android.app.usage.NetworkStatsManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -338,10 +356,12 @@
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.LocationPermissionChecker;
import com.android.networkstack.apishim.NetworkAgentConfigShimImpl;
+import com.android.networkstack.apishim.api29.ConstantsShim;
import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo;
import com.android.server.ConnectivityService.NetworkRequestInfo;
import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
+import com.android.server.connectivity.ClatCoordinator;
import com.android.server.connectivity.ConnectivityFlags;
import com.android.server.connectivity.MockableSystemProperties;
import com.android.server.connectivity.Nat464Xlat;
@@ -464,6 +484,9 @@
private static final int TEST_APP_ID_2 = 104;
private static final int TEST_WORK_PROFILE_APP_UID_2 =
UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID_2);
+ private static final int TEST_APP_ID_3 = 105;
+ private static final int TEST_APP_ID_4 = 106;
+ private static final int TEST_APP_ID_5 = 107;
private static final String CLAT_PREFIX = "v4-";
private static final String MOBILE_IFNAME = "test_rmnet_data0";
@@ -539,7 +562,9 @@
@Mock NetworkPolicyManager mNetworkPolicyManager;
@Mock VpnProfileStore mVpnProfileStore;
@Mock SystemConfigManager mSystemConfigManager;
+ @Mock DevicePolicyManager mDevicePolicyManager;
@Mock Resources mResources;
+ @Mock ClatCoordinator mClatCoordinator;
@Mock PacProxyManager mPacProxyManager;
@Mock BpfNetMaps mBpfNetMaps;
@Mock CarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator;
@@ -642,7 +667,8 @@
@Override
public ComponentName startService(Intent service) {
final String action = service.getAction();
- if (!VpnConfig.SERVICE_INTERFACE.equals(action)) {
+ if (!VpnConfig.SERVICE_INTERFACE.equals(action)
+ && !ConstantsShim.ACTION_VPN_MANAGER_EVENT.equals(action)) {
fail("Attempt to start unknown service, action=" + action);
}
return new ComponentName(service.getPackage(), "com.android.test.Service");
@@ -659,6 +685,7 @@
if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager;
if (Context.ETHERNET_SERVICE.equals(name)) return mEthernetManager;
if (Context.NETWORK_POLICY_SERVICE.equals(name)) return mNetworkPolicyManager;
+ if (Context.DEVICE_POLICY_SERVICE.equals(name)) return mDevicePolicyManager;
if (Context.SYSTEM_CONFIG_SERVICE.equals(name)) return mSystemConfigManager;
if (Context.NETWORK_STATS_SERVICE.equals(name)) return mStatsManager;
if (Context.BATTERY_STATS_SERVICE.equals(name)) return mBatteryStatsManager;
@@ -687,6 +714,14 @@
doReturn(value).when(mUserManager).isManagedProfile(eq(userHandle.getIdentifier()));
}
+ public void setDeviceOwner(@NonNull final UserHandle userHandle, String value) {
+ // This relies on all contexts for a given user returning the same UM mock
+ final DevicePolicyManager dpmMock = createContextAsUser(userHandle, 0 /* flags */)
+ .getSystemService(DevicePolicyManager.class);
+ doReturn(value).when(dpmMock).getDeviceOwner();
+ doReturn(value).when(mDevicePolicyManager).getDeviceOwner();
+ }
+
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
@@ -781,6 +816,32 @@
}
}
+ // This was only added in the T SDK, but this test needs to build against the R+S SDKs, too.
+ private static int toSdkSandboxUid(int appUid) {
+ final int firstSdkSandboxUid = 20000;
+ return appUid + (firstSdkSandboxUid - Process.FIRST_APPLICATION_UID);
+ }
+
+ // This function assumes the UID range for user 0 ([1, 99999])
+ private static UidRangeParcel[] uidRangeParcelsExcludingUids(Integer... excludedUids) {
+ int start = 1;
+ Arrays.sort(excludedUids);
+ List<UidRangeParcel> parcels = new ArrayList<UidRangeParcel>();
+ for (int excludedUid : excludedUids) {
+ if (excludedUid == start) {
+ start++;
+ } else {
+ parcels.add(new UidRangeParcel(start, excludedUid - 1));
+ start = excludedUid + 1;
+ }
+ }
+ if (start <= 99999) {
+ parcels.add(new UidRangeParcel(start, 99999));
+ }
+
+ return parcels.toArray(new UidRangeParcel[0]);
+ }
+
private void waitForIdle() {
HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS);
waitForIdle(mCellNetworkAgent, TIMEOUT_MS);
@@ -2004,6 +2065,11 @@
return mBpfNetMaps;
}
+ @Override
+ public ClatCoordinator getClatCoordinator(INetd netd) {
+ return mClatCoordinator;
+ }
+
final ArrayTrackRecord<Pair<String, Long>> mRateLimitHistory = new ArrayTrackRecord<>();
final Map<String, Long> mActiveRateLimit = new HashMap<>();
@@ -5851,7 +5917,7 @@
}
/**
- * Validate the callback flow CBS request without carrier privilege.
+ * Validate the service throws if request with CBS but without carrier privilege.
*/
@Test
public void testCBSRequestWithoutCarrierPrivilege() throws Exception {
@@ -5860,10 +5926,8 @@
final TestNetworkCallback networkCallback = new TestNetworkCallback();
mServiceContext.setPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, PERMISSION_DENIED);
- // Now file the test request and expect it.
- mCm.requestNetwork(nr, networkCallback);
- networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null);
- mCm.unregisterNetworkCallback(networkCallback);
+ // Now file the test request and expect the service throws.
+ assertThrows(SecurityException.class, () -> mCm.requestNetwork(nr, networkCallback));
}
private static class TestKeepaliveCallback extends PacketKeepaliveCallback {
@@ -8989,10 +9053,16 @@
allowList);
waitForIdle();
- UidRangeParcel firstHalf = new UidRangeParcel(1, VPN_UID - 1);
- UidRangeParcel secondHalf = new UidRangeParcel(VPN_UID + 1, 99999);
+ final Set<Integer> excludedUids = new ArraySet<Integer>();
+ excludedUids.add(VPN_UID);
+ if (SdkLevel.isAtLeastT()) {
+ // On T onwards, the corresponding SDK sandbox UID should also be excluded
+ excludedUids.add(toSdkSandboxUid(VPN_UID));
+ }
+ final UidRangeParcel[] uidRangeParcels = uidRangeParcelsExcludingUids(
+ excludedUids.toArray(new Integer[0]));
InOrder inOrder = inOrder(mMockNetd);
- expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf);
+ expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
// Connect a network when lockdown is active, expect to see it blocked.
mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
@@ -9016,7 +9086,7 @@
vpnUidCallback.assertNoCallback();
vpnUidDefaultCallback.assertNoCallback();
vpnDefaultCallbackAsUid.assertNoCallback();
- expectNetworkRejectNonSecureVpn(inOrder, false, firstHalf, secondHalf);
+ expectNetworkRejectNonSecureVpn(inOrder, false, uidRangeParcels);
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
@@ -9033,13 +9103,14 @@
vpnUidDefaultCallback.assertNoCallback();
vpnDefaultCallbackAsUid.assertNoCallback();
- // The following requires that the UID of this test package is greater than VPN_UID. This
- // is always true in practice because a plain AOSP build with no apps installed has almost
- // 200 packages installed.
- final UidRangeParcel piece1 = new UidRangeParcel(1, VPN_UID - 1);
- final UidRangeParcel piece2 = new UidRangeParcel(VPN_UID + 1, uid - 1);
- final UidRangeParcel piece3 = new UidRangeParcel(uid + 1, 99999);
- expectNetworkRejectNonSecureVpn(inOrder, true, piece1, piece2, piece3);
+ excludedUids.add(uid);
+ if (SdkLevel.isAtLeastT()) {
+ // On T onwards, the corresponding SDK sandbox UID should also be excluded
+ excludedUids.add(toSdkSandboxUid(uid));
+ }
+ final UidRangeParcel[] uidRangeParcelsAlsoExcludingUs = uidRangeParcelsExcludingUids(
+ excludedUids.toArray(new Integer[0]));
+ expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcelsAlsoExcludingUs);
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
@@ -9065,12 +9136,12 @@
// Everything should now be blocked.
mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList);
waitForIdle();
- expectNetworkRejectNonSecureVpn(inOrder, false, piece1, piece2, piece3);
+ expectNetworkRejectNonSecureVpn(inOrder, false, uidRangeParcelsAlsoExcludingUs);
allowList.clear();
mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */,
allowList);
waitForIdle();
- expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf);
+ expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
defaultCallback.expectBlockedStatusCallback(true, mWiFiNetworkAgent);
assertBlockedCallbackInAnyOrder(callback, true, mWiFiNetworkAgent, mCellNetworkAgent);
vpnUidCallback.assertNoCallback();
@@ -9442,6 +9513,128 @@
b2.expectBroadcast();
}
+ @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testLockdownSetFirewallUidRule() throws Exception {
+ final Set<Range<Integer>> lockdownRange = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
+ // Enable Lockdown
+ mCm.setRequireVpnForUids(true /* requireVpn */, lockdownRange);
+ waitForIdle();
+
+ // Lockdown rule is set to apps uids
+ verify(mBpfNetMaps, times(3)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(APP1_UID, true /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(APP2_UID, true /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, true /* add */);
+
+ reset(mBpfNetMaps);
+
+ // Disable lockdown
+ mCm.setRequireVpnForUids(false /* requireVPN */, lockdownRange);
+ waitForIdle();
+
+ // Lockdown rule is removed from apps uids
+ verify(mBpfNetMaps, times(3)).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(APP1_UID, false /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(APP2_UID, false /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, false /* add */);
+
+ // Interface rules are not changed by Lockdown mode enable/disable
+ verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+ verify(mBpfNetMaps, never()).removeUidInterfaceRules(any());
+ }
+
+ private void doTestSetUidFirewallRule(final int chain, final int defaultRule) {
+ final int uid = 1001;
+ mCm.setUidFirewallRule(chain, uid, FIREWALL_RULE_ALLOW);
+ verify(mBpfNetMaps).setUidRule(chain, uid, FIREWALL_RULE_ALLOW);
+ reset(mBpfNetMaps);
+
+ mCm.setUidFirewallRule(chain, uid, FIREWALL_RULE_DENY);
+ verify(mBpfNetMaps).setUidRule(chain, uid, FIREWALL_RULE_DENY);
+ reset(mBpfNetMaps);
+
+ mCm.setUidFirewallRule(chain, uid, FIREWALL_RULE_DEFAULT);
+ verify(mBpfNetMaps).setUidRule(chain, uid, defaultRule);
+ reset(mBpfNetMaps);
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testSetUidFirewallRule() throws Exception {
+ doTestSetUidFirewallRule(FIREWALL_CHAIN_DOZABLE, FIREWALL_RULE_DENY);
+ doTestSetUidFirewallRule(FIREWALL_CHAIN_STANDBY, FIREWALL_RULE_ALLOW);
+ doTestSetUidFirewallRule(FIREWALL_CHAIN_POWERSAVE, FIREWALL_RULE_DENY);
+ doTestSetUidFirewallRule(FIREWALL_CHAIN_RESTRICTED, FIREWALL_RULE_DENY);
+ doTestSetUidFirewallRule(FIREWALL_CHAIN_LOW_POWER_STANDBY, FIREWALL_RULE_DENY);
+ doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_1, FIREWALL_RULE_ALLOW);
+ doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_2, FIREWALL_RULE_ALLOW);
+ doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_3, FIREWALL_RULE_ALLOW);
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testSetFirewallChainEnabled() throws Exception {
+ final List<Integer> firewallChains = Arrays.asList(
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_STANDBY,
+ FIREWALL_CHAIN_POWERSAVE,
+ FIREWALL_CHAIN_RESTRICTED,
+ FIREWALL_CHAIN_LOW_POWER_STANDBY,
+ FIREWALL_CHAIN_OEM_DENY_1,
+ FIREWALL_CHAIN_OEM_DENY_2,
+ FIREWALL_CHAIN_OEM_DENY_3);
+ for (final int chain: firewallChains) {
+ mCm.setFirewallChainEnabled(chain, true /* enabled */);
+ verify(mBpfNetMaps).setChildChain(chain, true /* enable */);
+ reset(mBpfNetMaps);
+
+ mCm.setFirewallChainEnabled(chain, false /* enabled */);
+ verify(mBpfNetMaps).setChildChain(chain, false /* enable */);
+ reset(mBpfNetMaps);
+ }
+ }
+
+ private void doTestReplaceFirewallChain(final int chain, final String chainName,
+ final boolean allowList) {
+ final int[] uids = new int[] {1001, 1002};
+ mCm.replaceFirewallChain(chain, uids);
+ verify(mBpfNetMaps).replaceUidChain(chainName, allowList, uids);
+ reset(mBpfNetMaps);
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testReplaceFirewallChain() {
+ doTestReplaceFirewallChain(FIREWALL_CHAIN_DOZABLE, "fw_dozable", true);
+ doTestReplaceFirewallChain(FIREWALL_CHAIN_STANDBY, "fw_standby", false);
+ doTestReplaceFirewallChain(FIREWALL_CHAIN_POWERSAVE, "fw_powersave", true);
+ doTestReplaceFirewallChain(FIREWALL_CHAIN_RESTRICTED, "fw_restricted", true);
+ doTestReplaceFirewallChain(FIREWALL_CHAIN_LOW_POWER_STANDBY, "fw_low_power_standby", true);
+ doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_1, "fw_oem_deny_1", false);
+ doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_2, "fw_oem_deny_2", false);
+ doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_3, "fw_oem_deny_3", false);
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testInvalidFirewallChain() throws Exception {
+ final int uid = 1001;
+ final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
+ assertThrows(expected,
+ () -> mCm.setUidFirewallRule(-1 /* chain */, uid, FIREWALL_RULE_ALLOW));
+ assertThrows(expected,
+ () -> mCm.setUidFirewallRule(100 /* chain */, uid, FIREWALL_RULE_ALLOW));
+ assertThrows(expected, () -> mCm.replaceFirewallChain(-1 /* chain */, new int[]{uid}));
+ assertThrows(expected, () -> mCm.replaceFirewallChain(100 /* chain */, new int[]{uid}));
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testInvalidFirewallRule() throws Exception {
+ final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
+ assertThrows(expected,
+ () -> mCm.setUidFirewallRule(FIREWALL_CHAIN_DOZABLE,
+ 1001 /* uid */, -1 /* rule */));
+ assertThrows(expected,
+ () -> mCm.setUidFirewallRule(FIREWALL_CHAIN_DOZABLE,
+ 1001 /* uid */, 100 /* rule */));
+ }
+
/**
* Test mutable and requestable network capabilities such as
* {@link NetworkCapabilities#NET_CAPABILITY_TRUSTED} and
@@ -9569,6 +9762,59 @@
return event;
}
+ private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+ if (inOrder != null) {
+ return inOrder.verify(t);
+ } else {
+ return verify(t);
+ }
+ }
+
+ private <T> T verifyNeverWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+ if (inOrder != null) {
+ return inOrder.verify(t, never());
+ } else {
+ return verify(t, never());
+ }
+ }
+
+ private void verifyClatdStart(@Nullable InOrder inOrder, @NonNull String iface, int netId,
+ @NonNull String nat64Prefix) throws Exception {
+ if (SdkLevel.isAtLeastT()) {
+ verifyWithOrder(inOrder, mClatCoordinator)
+ .clatStart(eq(iface), eq(netId), eq(new IpPrefix(nat64Prefix)));
+ } else {
+ verifyWithOrder(inOrder, mMockNetd).clatdStart(eq(iface), eq(nat64Prefix));
+ }
+ }
+
+ private void verifyNeverClatdStart(@Nullable InOrder inOrder, @NonNull String iface)
+ throws Exception {
+ if (SdkLevel.isAtLeastT()) {
+ verifyNeverWithOrder(inOrder, mClatCoordinator).clatStart(eq(iface), anyInt(), any());
+ } else {
+ verifyNeverWithOrder(inOrder, mMockNetd).clatdStart(eq(iface), anyString());
+ }
+ }
+
+ private void verifyClatdStop(@Nullable InOrder inOrder, @NonNull String iface)
+ throws Exception {
+ if (SdkLevel.isAtLeastT()) {
+ verifyWithOrder(inOrder, mClatCoordinator).clatStop();
+ } else {
+ verifyWithOrder(inOrder, mMockNetd).clatdStop(eq(iface));
+ }
+ }
+
+ private void verifyNeverClatdStop(@Nullable InOrder inOrder, @NonNull String iface)
+ throws Exception {
+ if (SdkLevel.isAtLeastT()) {
+ verifyNeverWithOrder(inOrder, mClatCoordinator).clatStop();
+ } else {
+ verifyNeverWithOrder(inOrder, mMockNetd).clatdStop(eq(iface));
+ }
+ }
+
@Test
public void testStackedLinkProperties() throws Exception {
final LinkAddress myIpv4 = new LinkAddress("1.2.3.4/24");
@@ -9601,6 +9847,7 @@
mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
reset(mMockDnsResolver);
reset(mMockNetd);
+ reset(mClatCoordinator);
// Connect with ipv6 link properties. Expect prefix discovery to be started.
mCellNetworkAgent.connect(true);
@@ -9639,8 +9886,10 @@
&& ri.iface != null && ri.iface.startsWith("v4-")));
verifyNoMoreInteractions(mMockNetd);
+ verifyNoMoreInteractions(mClatCoordinator);
verifyNoMoreInteractions(mMockDnsResolver);
reset(mMockNetd);
+ reset(mClatCoordinator);
reset(mMockDnsResolver);
doReturn(getClatInterfaceConfigParcel(myIpv4)).when(mMockNetd)
.interfaceGetCfg(CLAT_MOBILE_IFNAME);
@@ -9661,7 +9910,7 @@
CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent).getLp();
assertEquals(0, lpBeforeClat.getStackedLinks().size());
assertEquals(kNat64Prefix, lpBeforeClat.getNat64Prefix());
- verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
+ verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
// Clat iface comes up. Expect stacked link to be added.
clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
@@ -9693,6 +9942,7 @@
new int[] { TRANSPORT_CELLULAR })));
}
reset(mMockNetd);
+ reset(mClatCoordinator);
doReturn(getClatInterfaceConfigParcel(myIpv4)).when(mMockNetd)
.interfaceGetCfg(CLAT_MOBILE_IFNAME);
// Change the NAT64 prefix without first removing it.
@@ -9701,11 +9951,12 @@
cellNetId, PREFIX_OPERATION_ADDED, kOtherNat64PrefixString, 96));
networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
(lp) -> lp.getStackedLinks().size() == 0);
- verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
+ verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
assertRoutesRemoved(cellNetId, stackedDefault);
verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
- verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kOtherNat64Prefix.toString());
+ verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId,
+ kOtherNat64Prefix.toString());
networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
(lp) -> lp.getNat64Prefix().equals(kOtherNat64Prefix));
clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
@@ -9714,6 +9965,7 @@
assertRoutesAdded(cellNetId, stackedDefault);
verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
reset(mMockNetd);
+ reset(mClatCoordinator);
// Add ipv4 address, expect that clatd and prefix discovery are stopped and stacked
// linkproperties are cleaned up.
@@ -9722,7 +9974,7 @@
mCellNetworkAgent.sendLinkProperties(cellLp);
networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
assertRoutesAdded(cellNetId, ipv4Subnet);
- verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
+ verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
verify(mMockDnsResolver, times(1)).stopPrefix64Discovery(cellNetId);
// As soon as stop is called, the linkproperties lose the stacked interface.
@@ -9739,8 +9991,10 @@
networkCallback.assertNoCallback();
verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
verifyNoMoreInteractions(mMockNetd);
+ verifyNoMoreInteractions(mClatCoordinator);
verifyNoMoreInteractions(mMockDnsResolver);
reset(mMockNetd);
+ reset(mClatCoordinator);
reset(mMockDnsResolver);
doReturn(getClatInterfaceConfigParcel(myIpv4)).when(mMockNetd)
.interfaceGetCfg(CLAT_MOBILE_IFNAME);
@@ -9762,7 +10016,7 @@
mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent(
cellNetId, PREFIX_OPERATION_ADDED, kNat64PrefixString, 96));
networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent);
- verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
+ verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
// Clat iface comes up. Expect stacked link to be added.
clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true);
@@ -9779,7 +10033,7 @@
assertRoutesRemoved(cellNetId, ipv4Subnet, stackedDefault);
// Stop has no effect because clat is already stopped.
- verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME);
+ verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
(lp) -> lp.getStackedLinks().size() == 0);
verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
@@ -9792,7 +10046,9 @@
eq(Integer.toString(TRANSPORT_CELLULAR)));
verify(mMockNetd).networkDestroy(cellNetId);
verifyNoMoreInteractions(mMockNetd);
+ verifyNoMoreInteractions(mClatCoordinator);
reset(mMockNetd);
+ reset(mClatCoordinator);
// Test disconnecting a network that is running 464xlat.
@@ -9809,7 +10065,7 @@
assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default);
// Clatd is started and clat iface comes up. Expect stacked link to be added.
- verify(mMockNetd).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString());
+ verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, cellNetId, kNat64Prefix.toString());
clat = getNat464Xlat(mCellNetworkAgent);
clat.interfaceLinkStateChanged(CLAT_MOBILE_IFNAME, true /* up */);
networkCallback.expectLinkPropertiesThat(mCellNetworkAgent,
@@ -9819,16 +10075,18 @@
// assertRoutesAdded sees all calls since last mMockNetd reset, so expect IPv6 routes again.
assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default, stackedDefault);
reset(mMockNetd);
+ reset(mClatCoordinator);
// Disconnect the network. clat is stopped and the network is destroyed.
mCellNetworkAgent.disconnect();
networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
networkCallback.assertNoCallback();
- verify(mMockNetd).clatdStop(MOBILE_IFNAME);
+ verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
eq(Integer.toString(TRANSPORT_CELLULAR)));
verify(mMockNetd).networkDestroy(cellNetId);
verifyNoMoreInteractions(mMockNetd);
+ verifyNoMoreInteractions(mClatCoordinator);
mCm.unregisterNetworkCallback(networkCallback);
}
@@ -9859,7 +10117,7 @@
baseLp.addDnsServer(InetAddress.getByName("2001:4860:4860::6464"));
reset(mMockNetd, mMockDnsResolver);
- InOrder inOrder = inOrder(mMockNetd, mMockDnsResolver);
+ InOrder inOrder = inOrder(mMockNetd, mMockDnsResolver, mClatCoordinator);
// If a network already has a NAT64 prefix on connect, clatd is started immediately and
// prefix discovery is never started.
@@ -9870,7 +10128,7 @@
final Network network = mWiFiNetworkAgent.getNetwork();
int netId = network.getNetId();
callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
- inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+ verifyClatdStart(inOrder, iface, netId, pref64FromRa.toString());
inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString());
inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
callback.assertNoCallback();
@@ -9880,7 +10138,7 @@
lp.setNat64Prefix(null);
mWiFiNetworkAgent.sendLinkProperties(lp);
expectNat64PrefixChange(callback, mWiFiNetworkAgent, null);
- inOrder.verify(mMockNetd).clatdStop(iface);
+ verifyClatdStop(inOrder, iface);
inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
@@ -9889,7 +10147,7 @@
lp.setNat64Prefix(pref64FromRa);
mWiFiNetworkAgent.sendLinkProperties(lp);
expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromRa);
- inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+ verifyClatdStart(inOrder, iface, netId, pref64FromRa.toString());
inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString());
@@ -9898,22 +10156,22 @@
lp.setNat64Prefix(null);
mWiFiNetworkAgent.sendLinkProperties(lp);
expectNat64PrefixChange(callback, mWiFiNetworkAgent, null);
- inOrder.verify(mMockNetd).clatdStop(iface);
+ verifyClatdStop(inOrder, iface);
inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
makeNat64PrefixEvent(netId, PREFIX_OPERATION_ADDED, pref64FromDnsStr, 96));
expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromDns);
- inOrder.verify(mMockNetd).clatdStart(iface, pref64FromDns.toString());
+ verifyClatdStart(inOrder, iface, netId, pref64FromDns.toString());
// If an RA advertises the same prefix that was discovered by DNS, nothing happens: prefix
// discovery is not stopped, and there are no callbacks.
lp.setNat64Prefix(pref64FromDns);
mWiFiNetworkAgent.sendLinkProperties(lp);
callback.assertNoCallback();
- inOrder.verify(mMockNetd, never()).clatdStop(iface);
- inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+ verifyNeverClatdStop(inOrder, iface);
+ verifyNeverClatdStart(inOrder, iface);
inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -9922,8 +10180,8 @@
lp.setNat64Prefix(null);
mWiFiNetworkAgent.sendLinkProperties(lp);
callback.assertNoCallback();
- inOrder.verify(mMockNetd, never()).clatdStop(iface);
- inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+ verifyNeverClatdStop(inOrder, iface);
+ verifyNeverClatdStart(inOrder, iface);
inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -9932,14 +10190,14 @@
lp.setNat64Prefix(pref64FromRa);
mWiFiNetworkAgent.sendLinkProperties(lp);
expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromRa);
- inOrder.verify(mMockNetd).clatdStop(iface);
+ verifyClatdStop(inOrder, iface);
inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
// Stopping prefix discovery results in a prefix removed notification.
mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
makeNat64PrefixEvent(netId, PREFIX_OPERATION_REMOVED, pref64FromDnsStr, 96));
- inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+ verifyClatdStart(inOrder, iface, netId, pref64FromRa.toString());
inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString());
inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
@@ -9947,9 +10205,9 @@
lp.setNat64Prefix(newPref64FromRa);
mWiFiNetworkAgent.sendLinkProperties(lp);
expectNat64PrefixChange(callback, mWiFiNetworkAgent, newPref64FromRa);
- inOrder.verify(mMockNetd).clatdStop(iface);
+ verifyClatdStop(inOrder, iface);
inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
- inOrder.verify(mMockNetd).clatdStart(iface, newPref64FromRa.toString());
+ verifyClatdStart(inOrder, iface, netId, newPref64FromRa.toString());
inOrder.verify(mMockDnsResolver).setPrefix64(netId, newPref64FromRa.toString());
inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
@@ -9959,8 +10217,8 @@
mWiFiNetworkAgent.sendLinkProperties(lp);
callback.assertNoCallback();
assertEquals(newPref64FromRa, mCm.getLinkProperties(network).getNat64Prefix());
- inOrder.verify(mMockNetd, never()).clatdStop(iface);
- inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+ verifyNeverClatdStop(inOrder, iface);
+ verifyNeverClatdStart(inOrder, iface);
inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -9972,20 +10230,20 @@
lp.setNat64Prefix(null);
mWiFiNetworkAgent.sendLinkProperties(lp);
expectNat64PrefixChange(callback, mWiFiNetworkAgent, null);
- inOrder.verify(mMockNetd).clatdStop(iface);
+ verifyClatdStop(inOrder, iface);
inOrder.verify(mMockDnsResolver).setPrefix64(netId, "");
inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
mService.mResolverUnsolEventCallback.onNat64PrefixEvent(
makeNat64PrefixEvent(netId, PREFIX_OPERATION_ADDED, pref64FromDnsStr, 96));
expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromDns);
- inOrder.verify(mMockNetd).clatdStart(iface, pref64FromDns.toString());
+ verifyClatdStart(inOrder, iface, netId, pref64FromDns.toString());
inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), any());
lp.setNat64Prefix(pref64FromDns);
mWiFiNetworkAgent.sendLinkProperties(lp);
callback.assertNoCallback();
- inOrder.verify(mMockNetd, never()).clatdStop(iface);
- inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+ verifyNeverClatdStop(inOrder, iface);
+ verifyNeverClatdStart(inOrder, iface);
inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -9998,7 +10256,7 @@
callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
b.expectBroadcast();
- inOrder.verify(mMockNetd).clatdStop(iface);
+ verifyClatdStop(inOrder, iface);
inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString());
@@ -10248,7 +10506,7 @@
verify(mBpfNetMaps, times(2)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture());
assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID);
assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID);
- assertTrue(mService.mPermissionMonitor.getVpnUidRanges("tun0").equals(vpnRange));
+ assertTrue(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0").equals(vpnRange));
mMockVpn.disconnect();
waitForIdle();
@@ -10256,38 +10514,70 @@
// Disconnected VPN should have interface rules removed
verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
- assertNull(mService.mPermissionMonitor.getVpnUidRanges("tun0"));
+ assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0"));
+ }
+
+ private void checkInterfaceFilteringRuleWithNullInterface(final LinkProperties lp,
+ final int uid) throws Exception {
+ // The uid range needs to cover the test app so the network is visible to it.
+ final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
+ mMockVpn.establish(lp, uid, vpnRange);
+ assertVpnUidRangesUpdated(true, vpnRange, uid);
+
+ if (SdkLevel.isAtLeastT()) {
+ // On T and above, VPN should have rules for null interface. Null Interface is a
+ // wildcard and this accepts traffic from all the interfaces.
+ // There are two expected invocations, one during the VPN initial
+ // connection, one during the VPN LinkProperties update.
+ ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
+ verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
+ eq(null) /* iface */, uidCaptor.capture());
+ if (uid == VPN_UID) {
+ assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID);
+ assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID);
+ } else {
+ assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
+ assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+ }
+ assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
+ vpnRange);
+
+ mMockVpn.disconnect();
+ waitForIdle();
+
+ // Disconnected VPN should have interface rules removed
+ verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
+ if (uid == VPN_UID) {
+ assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
+ } else {
+ assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+ }
+ assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
+ } else {
+ // Before T, rules are not configured for null interface.
+ verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+ }
}
@Test
- public void testLegacyVpnDoesNotResultInInterfaceFilteringRule() throws Exception {
+ public void testLegacyVpnInterfaceFilteringRule() throws Exception {
LinkProperties lp = new LinkProperties();
lp.setInterfaceName("tun0");
lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
- // The uid range needs to cover the test app so the network is visible to it.
- final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
- mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
- assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
-
- // Legacy VPN should not have interface rules set up
- verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+ // Legacy VPN should have interface filtering with null interface.
+ checkInterfaceFilteringRuleWithNullInterface(lp, Process.SYSTEM_UID);
}
@Test
- public void testLocalIpv4OnlyVpnDoesNotResultInInterfaceFilteringRule()
- throws Exception {
+ public void testLocalIpv4OnlyVpnInterfaceFilteringRule() throws Exception {
LinkProperties lp = new LinkProperties();
lp.setInterfaceName("tun0");
lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0"));
lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE));
- // The uid range needs to cover the test app so the network is visible to it.
- final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
- mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
- assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
-
- // IPv6 unreachable route should not be misinterpreted as a default route
- verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+ // VPN that does not provide a default route should have interface filtering with null
+ // interface.
+ checkInterfaceFilteringRuleWithNullInterface(lp, VPN_UID);
}
@Test
@@ -10343,19 +10633,6 @@
}
@Test
- public void testStartVpnProfileFromDiffPackage() throws Exception {
- final String notMyVpnPkg = "com.not.my.vpn";
- assertThrows(
- SecurityException.class, () -> mVpnManagerService.startVpnProfile(notMyVpnPkg));
- }
-
- @Test
- public void testStopVpnProfileFromDiffPackage() throws Exception {
- final String notMyVpnPkg = "com.not.my.vpn";
- assertThrows(SecurityException.class, () -> mVpnManagerService.stopVpnProfile(notMyVpnPkg));
- }
-
- @Test
public void testUidUpdateChangesInterfaceFilteringRule() throws Exception {
LinkProperties lp = new LinkProperties();
lp.setInterfaceName("tun0");
@@ -11492,6 +11769,12 @@
mCm.unregisterNetworkCallback(networkCallback);
}
+ private void verifyDump(String[] args) {
+ final StringWriter stringWriter = new StringWriter();
+ mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), args);
+ assertFalse(stringWriter.toString().isEmpty());
+ }
+
@Test
public void testDumpDoesNotCrash() {
mServiceContext.setPermission(DUMP, PERMISSION_GRANTED);
@@ -11504,11 +11787,26 @@
.addTransportType(TRANSPORT_WIFI).build();
mCm.registerNetworkCallback(genericRequest, genericNetworkCallback);
mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
- final StringWriter stringWriter = new StringWriter();
- mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), new String[0]);
+ verifyDump(new String[0]);
- assertFalse(stringWriter.toString().isEmpty());
+ // Verify dump with arguments.
+ final String dumpPrio = "--dump-priority";
+ final String[] dumpArgs = {dumpPrio};
+ verifyDump(dumpArgs);
+
+ final String[] highDumpArgs = {dumpPrio, "HIGH"};
+ verifyDump(highDumpArgs);
+
+ final String[] normalDumpArgs = {dumpPrio, "NORMAL"};
+ verifyDump(normalDumpArgs);
+
+ // Invalid args should do dumpNormal w/o exception
+ final String[] unknownDumpArgs = {dumpPrio, "UNKNOWN"};
+ verifyDump(unknownDumpArgs);
+
+ final String[] invalidDumpArgs = {"UNKNOWN"};
+ verifyDump(invalidDumpArgs);
}
@Test
@@ -11852,16 +12150,14 @@
mQosCallbackMockHelper.registerQosCallback(
mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
- final NetworkAgentWrapper.CallbackType.OnQosCallbackRegister cbRegister1 =
- (NetworkAgentWrapper.CallbackType.OnQosCallbackRegister)
- wrapper.getCallbackHistory().poll(1000, x -> true);
+ final OnQosCallbackRegister cbRegister1 =
+ (OnQosCallbackRegister) wrapper.getCallbackHistory().poll(1000, x -> true);
assertNotNull(cbRegister1);
final int registerCallbackId = cbRegister1.mQosCallbackId;
mService.unregisterQosCallback(mQosCallbackMockHelper.mCallback);
- final NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister cbUnregister;
- cbUnregister = (NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister)
- wrapper.getCallbackHistory().poll(1000, x -> true);
+ final OnQosCallbackUnregister cbUnregister =
+ (OnQosCallbackUnregister) wrapper.getCallbackHistory().poll(1000, x -> true);
assertNotNull(cbUnregister);
assertEquals(registerCallbackId, cbUnregister.mQosCallbackId);
assertNull(wrapper.getCallbackHistory().poll(200, x -> true));
@@ -11940,6 +12236,86 @@
&& session.getSessionType() == QosSession.TYPE_NR_BEARER));
}
+ @Test @IgnoreUpTo(SC_V2)
+ public void testQosCallbackAvailableOnValidationError() throws Exception {
+ mQosCallbackMockHelper = new QosCallbackMockHelper();
+ final NetworkAgentWrapper wrapper = mQosCallbackMockHelper.mAgentWrapper;
+ final int sessionId = 10;
+ final int qosCallbackId = 1;
+
+ doReturn(QosCallbackException.EX_TYPE_FILTER_NONE)
+ .when(mQosCallbackMockHelper.mFilter).validate();
+ mQosCallbackMockHelper.registerQosCallback(
+ mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
+ OnQosCallbackRegister cbRegister1 =
+ (OnQosCallbackRegister) wrapper.getCallbackHistory().poll(1000, x -> true);
+ assertNotNull(cbRegister1);
+ final int registerCallbackId = cbRegister1.mQosCallbackId;
+
+ waitForIdle();
+
+ doReturn(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED)
+ .when(mQosCallbackMockHelper.mFilter).validate();
+ final EpsBearerQosSessionAttributes attributes = new EpsBearerQosSessionAttributes(
+ 1, 2, 3, 4, 5, new ArrayList<>());
+ mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+ .sendQosSessionAvailable(qosCallbackId, sessionId, attributes);
+ waitForIdle();
+
+ final NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister cbUnregister;
+ cbUnregister = (NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister)
+ wrapper.getCallbackHistory().poll(1000, x -> true);
+ assertNotNull(cbUnregister);
+ assertEquals(registerCallbackId, cbUnregister.mQosCallbackId);
+ waitForIdle();
+ verify(mQosCallbackMockHelper.mCallback)
+ .onError(eq(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED));
+ }
+
+ @Test @IgnoreUpTo(SC_V2)
+ public void testQosCallbackLostOnValidationError() throws Exception {
+ mQosCallbackMockHelper = new QosCallbackMockHelper();
+ final int sessionId = 10;
+ final int qosCallbackId = 1;
+
+ doReturn(QosCallbackException.EX_TYPE_FILTER_NONE)
+ .when(mQosCallbackMockHelper.mFilter).validate();
+ mQosCallbackMockHelper.registerQosCallback(
+ mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback);
+ waitForIdle();
+ EpsBearerQosSessionAttributes attributes =
+ sendQosSessionEvent(qosCallbackId, sessionId, true);
+ waitForIdle();
+
+ verify(mQosCallbackMockHelper.mCallback).onQosEpsBearerSessionAvailable(argThat(session ->
+ session.getSessionId() == sessionId
+ && session.getSessionType() == QosSession.TYPE_EPS_BEARER), eq(attributes));
+
+ doReturn(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED)
+ .when(mQosCallbackMockHelper.mFilter).validate();
+
+ sendQosSessionEvent(qosCallbackId, sessionId, false);
+ waitForIdle();
+ verify(mQosCallbackMockHelper.mCallback)
+ .onError(eq(QosCallbackException.EX_TYPE_FILTER_SOCKET_REMOTE_ADDRESS_CHANGED));
+ }
+
+ private EpsBearerQosSessionAttributes sendQosSessionEvent(
+ int qosCallbackId, int sessionId, boolean available) {
+ if (available) {
+ final EpsBearerQosSessionAttributes attributes = new EpsBearerQosSessionAttributes(
+ 1, 2, 3, 4, 5, new ArrayList<>());
+ mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+ .sendQosSessionAvailable(qosCallbackId, sessionId, attributes);
+ return attributes;
+ } else {
+ mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent()
+ .sendQosSessionLost(qosCallbackId, sessionId, QosSession.TYPE_EPS_BEARER);
+ return null;
+ }
+
+ }
+
@Test
public void testQosCallbackTooManyRequests() throws Exception {
mQosCallbackMockHelper = new QosCallbackMockHelper();
@@ -14463,7 +14839,7 @@
profileNetworkPreferenceBuilder.setPreference(
PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(
- NetworkCapabilities.NET_ENTERPRISE_ID_2);
+ NET_ENTERPRISE_ID_2);
registerDefaultNetworkCallbacks();
testPreferenceForUserNetworkUpDownForGivenPreference(
profileNetworkPreferenceBuilder.build(), true,
@@ -14488,6 +14864,393 @@
}
/**
+ * Make sure per-profile networking preference throws exception when default preference
+ * is set along with enterprise preference.
+ */
+ @Test
+ public void testPreferenceWithInvalidPreferenceDefaultAndEnterpriseTogether()
+ throws Exception {
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ mServiceContext.setWorkProfile(testHandle, true);
+
+ final int testWorkProfileAppUid1 =
+ UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID);
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder1 =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder1.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder1.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ profileNetworkPreferenceBuilder1.setIncludedUids(new int[]{testWorkProfileAppUid1});
+
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_DEFAULT);
+ final TestOnCompleteListener listener = new TestOnCompleteListener();
+ Assert.assertThrows(IllegalArgumentException.class,
+ () -> mCm.setProfileNetworkPreferences(
+ testHandle, List.of(profileNetworkPreferenceBuilder1.build(),
+ profileNetworkPreferenceBuilder2.build()),
+ r -> r.run(), listener));
+ Assert.assertThrows(IllegalArgumentException.class,
+ () -> mCm.setProfileNetworkPreferences(
+ testHandle, List.of(profileNetworkPreferenceBuilder2.build(),
+ profileNetworkPreferenceBuilder1.build()),
+ r -> r.run(), listener));
+ }
+
+ /**
+ * Make sure per profile network preferences behave as expected when two slices with
+ * two different apps within same user profile is configured
+ * Make sure per profile network preferences overrides with latest preference when
+ * same user preference is set twice
+ */
+ @Test
+ public void testSetPreferenceWithOverridingPreference()
+ throws Exception {
+ final InOrder inOrder = inOrder(mMockNetd);
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ mServiceContext.setWorkProfile(testHandle, true);
+ registerDefaultNetworkCallbacks();
+
+ final TestNetworkCallback appCb1 = new TestNetworkCallback();
+ final TestNetworkCallback appCb2 = new TestNetworkCallback();
+ final TestNetworkCallback appCb3 = new TestNetworkCallback();
+
+ final int testWorkProfileAppUid1 =
+ UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID);
+ final int testWorkProfileAppUid2 =
+ UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_2);
+ final int testWorkProfileAppUid3 =
+ UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_3);
+
+ registerDefaultNetworkCallbackAsUid(appCb1, testWorkProfileAppUid1);
+ registerDefaultNetworkCallbackAsUid(appCb2, testWorkProfileAppUid2);
+ registerDefaultNetworkCallbackAsUid(appCb3, testWorkProfileAppUid3);
+
+ // Connect both a regular cell agent and an enterprise network first.
+ mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+ mCellNetworkAgent.connect(true);
+
+ final TestNetworkAgentWrapper workAgent1 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_1);
+ final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_2);
+ workAgent1.connect(true);
+ workAgent2.connect(true);
+
+ mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+ mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+ appCb1.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+ appCb2.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+ appCb3.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+ verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+ mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+ verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+ workAgent1.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+ verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+ workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+
+ final TestOnCompleteListener listener = new TestOnCompleteListener();
+
+ // Set preferences for testHandle to map testWorkProfileAppUid1 to
+ // NET_ENTERPRISE_ID_1 and testWorkProfileAppUid2 to NET_ENTERPRISE_ID_2.
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder1 =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder1.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder1.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ profileNetworkPreferenceBuilder1.setIncludedUids(new int[]{testWorkProfileAppUid1});
+
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_2);
+ profileNetworkPreferenceBuilder2.setIncludedUids(new int[]{testWorkProfileAppUid2});
+
+ mCm.setProfileNetworkPreferences(testHandle,
+ List.of(profileNetworkPreferenceBuilder1.build(),
+ profileNetworkPreferenceBuilder2.build()),
+ r -> r.run(), listener);
+ listener.expectOnComplete();
+ verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ workAgent2.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+ PREFERENCE_ORDER_PROFILE));
+ verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ workAgent1.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+ PREFERENCE_ORDER_PROFILE));
+
+ assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+ appCb1.expectAvailableCallbacksValidated(workAgent1);
+ appCb2.expectAvailableCallbacksValidated(workAgent2);
+
+ // Set preferences for testHandle to map testWorkProfileAppUid3 to
+ // to NET_ENTERPRISE_ID_1.
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder3 =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder3.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder3.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ profileNetworkPreferenceBuilder3.setIncludedUids(new int[]{testWorkProfileAppUid3});
+
+ mCm.setProfileNetworkPreferences(testHandle,
+ List.of(profileNetworkPreferenceBuilder3.build()),
+ r -> r.run(), listener);
+ listener.expectOnComplete();
+ verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ workAgent1.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+ PREFERENCE_ORDER_PROFILE));
+ verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
+ workAgent2.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+ PREFERENCE_ORDER_PROFILE));
+ verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
+ workAgent1.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+ PREFERENCE_ORDER_PROFILE));
+
+ assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+ appCb3.expectAvailableCallbacksValidated(workAgent1);
+ appCb2.expectAvailableCallbacksValidated(mCellNetworkAgent);
+ appCb1.expectAvailableCallbacksValidated(mCellNetworkAgent);
+
+ // Set the preferences for testHandle to default.
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_DEFAULT);
+
+ mCm.setProfileNetworkPreferences(testHandle,
+ List.of(profileNetworkPreferenceBuilder.build()),
+ r -> r.run(), listener);
+ listener.expectOnComplete();
+ verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
+ workAgent1.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+ PREFERENCE_ORDER_PROFILE));
+
+ assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, appCb1, appCb2);
+ appCb3.expectAvailableCallbacksValidated(mCellNetworkAgent);
+ workAgent2.disconnect();
+ mCellNetworkAgent.disconnect();
+
+ mCm.unregisterNetworkCallback(appCb1);
+ mCm.unregisterNetworkCallback(appCb2);
+ mCm.unregisterNetworkCallback(appCb3);
+ // Other callbacks will be unregistered by tearDown()
+ }
+
+ /**
+ * Make sure per profile network preferences behave as expected when multiple slices with
+ * multiple different apps within same user profile is configured.
+ */
+ @Test
+ public void testSetPreferenceWithMultiplePreferences()
+ throws Exception {
+ final InOrder inOrder = inOrder(mMockNetd);
+
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ mServiceContext.setWorkProfile(testHandle, true);
+ registerDefaultNetworkCallbacks();
+
+ final TestNetworkCallback appCb1 = new TestNetworkCallback();
+ final TestNetworkCallback appCb2 = new TestNetworkCallback();
+ final TestNetworkCallback appCb3 = new TestNetworkCallback();
+ final TestNetworkCallback appCb4 = new TestNetworkCallback();
+ final TestNetworkCallback appCb5 = new TestNetworkCallback();
+
+ final int testWorkProfileAppUid1 =
+ UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID);
+ final int testWorkProfileAppUid2 =
+ UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_2);
+ final int testWorkProfileAppUid3 =
+ UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_3);
+ final int testWorkProfileAppUid4 =
+ UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_4);
+ final int testWorkProfileAppUid5 =
+ UserHandle.getUid(testHandle.getIdentifier(), TEST_APP_ID_5);
+
+ registerDefaultNetworkCallbackAsUid(appCb1, testWorkProfileAppUid1);
+ registerDefaultNetworkCallbackAsUid(appCb2, testWorkProfileAppUid2);
+ registerDefaultNetworkCallbackAsUid(appCb3, testWorkProfileAppUid3);
+ registerDefaultNetworkCallbackAsUid(appCb4, testWorkProfileAppUid4);
+ registerDefaultNetworkCallbackAsUid(appCb5, testWorkProfileAppUid5);
+
+ // Connect both a regular cell agent and an enterprise network first.
+ mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+ mCellNetworkAgent.connect(true);
+
+ final TestNetworkAgentWrapper workAgent1 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_1);
+ final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_2);
+ final TestNetworkAgentWrapper workAgent3 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_3);
+ final TestNetworkAgentWrapper workAgent4 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_4);
+ final TestNetworkAgentWrapper workAgent5 = makeEnterpriseNetworkAgent(NET_ENTERPRISE_ID_5);
+
+ workAgent1.connect(true);
+ workAgent2.connect(true);
+ workAgent3.connect(true);
+ workAgent4.connect(true);
+ workAgent5.connect(true);
+
+ mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+ mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+ appCb1.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+ appCb2.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+ appCb3.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+ appCb4.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+ appCb5.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+ verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+ mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
+ verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+ workAgent1.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+ verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+ workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+ verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+ workAgent3.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+ verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+ workAgent4.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+ verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
+ workAgent5.getNetwork().netId, INetd.PERMISSION_SYSTEM));
+
+ final TestOnCompleteListener listener = new TestOnCompleteListener();
+
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder1 =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder1.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder1.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ profileNetworkPreferenceBuilder1.setIncludedUids(new int[]{testWorkProfileAppUid1});
+
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder2.setPreference(
+ PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+ profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_2);
+ profileNetworkPreferenceBuilder2.setIncludedUids(new int[]{testWorkProfileAppUid2});
+
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder3 =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder3.setPreference(
+ PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder3.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_3);
+ profileNetworkPreferenceBuilder3.setIncludedUids(new int[]{testWorkProfileAppUid3});
+
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder4 =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder4.setPreference(
+ PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+ profileNetworkPreferenceBuilder4.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_4);
+ profileNetworkPreferenceBuilder4.setIncludedUids(new int[]{testWorkProfileAppUid4});
+
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder5 =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder5.setPreference(
+ PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder5.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_5);
+ profileNetworkPreferenceBuilder5.setIncludedUids(new int[]{testWorkProfileAppUid5});
+
+ mCm.setProfileNetworkPreferences(testHandle,
+ List.of(profileNetworkPreferenceBuilder1.build(),
+ profileNetworkPreferenceBuilder2.build(),
+ profileNetworkPreferenceBuilder3.build(),
+ profileNetworkPreferenceBuilder4.build(),
+ profileNetworkPreferenceBuilder5.build()),
+ r -> r.run(), listener);
+
+ listener.expectOnComplete();
+
+ verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ workAgent1.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+ PREFERENCE_ORDER_PROFILE));
+ verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ workAgent2.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+ PREFERENCE_ORDER_PROFILE));
+ verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ workAgent3.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+ PREFERENCE_ORDER_PROFILE));
+ verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ workAgent4.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder4.build()),
+ PREFERENCE_ORDER_PROFILE));
+ verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ workAgent5.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder5.build()),
+ PREFERENCE_ORDER_PROFILE));
+
+ assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
+ appCb1.expectAvailableCallbacksValidated(workAgent1);
+ appCb2.expectAvailableCallbacksValidated(workAgent2);
+ appCb3.expectAvailableCallbacksValidated(workAgent3);
+ appCb4.expectAvailableCallbacksValidated(workAgent4);
+ appCb5.expectAvailableCallbacksValidated(workAgent5);
+
+ workAgent1.disconnect();
+ workAgent2.disconnect();
+ workAgent3.disconnect();
+ workAgent4.disconnect();
+ workAgent5.disconnect();
+
+ appCb1.expectCallback(CallbackEntry.LOST, workAgent1);
+ appCb2.expectCallback(CallbackEntry.LOST, workAgent2);
+ appCb3.expectCallback(CallbackEntry.LOST, workAgent3);
+ appCb4.expectCallback(CallbackEntry.LOST, workAgent4);
+ appCb5.expectCallback(CallbackEntry.LOST, workAgent5);
+
+ appCb1.expectAvailableCallbacksValidated(mCellNetworkAgent);
+ appCb2.assertNoCallback();
+ appCb3.expectAvailableCallbacksValidated(mCellNetworkAgent);
+ appCb4.assertNoCallback();
+ appCb5.expectAvailableCallbacksValidated(mCellNetworkAgent);
+
+ verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ mCellNetworkAgent.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder1.build()),
+ PREFERENCE_ORDER_PROFILE));
+ verify(mMockNetd, never()).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ mCellNetworkAgent.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder2.build()),
+ PREFERENCE_ORDER_PROFILE));
+ verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ mCellNetworkAgent.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder3.build()),
+ PREFERENCE_ORDER_PROFILE));
+ verify(mMockNetd, never()).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ mCellNetworkAgent.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder4.build()),
+ PREFERENCE_ORDER_PROFILE));
+ verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
+ mCellNetworkAgent.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreferenceBuilder5.build()),
+ PREFERENCE_ORDER_PROFILE));
+
+ mSystemDefaultNetworkCallback.assertNoCallback();
+ mDefaultNetworkCallback.assertNoCallback();
+
+ // Set the preferences for testHandle to default.
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_DEFAULT);
+
+ mCm.setProfileNetworkPreferences(testHandle,
+ List.of(profileNetworkPreferenceBuilder.build()),
+ r -> r.run(), listener);
+ listener.expectOnComplete();
+ assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, appCb1, appCb3,
+ appCb5);
+ appCb2.expectAvailableCallbacksValidated(mCellNetworkAgent);
+ appCb4.expectAvailableCallbacksValidated(mCellNetworkAgent);
+ mCellNetworkAgent.disconnect();
+
+ mCm.unregisterNetworkCallback(appCb1);
+ mCm.unregisterNetworkCallback(appCb2);
+ mCm.unregisterNetworkCallback(appCb3);
+ mCm.unregisterNetworkCallback(appCb4);
+ mCm.unregisterNetworkCallback(appCb5);
+ // Other callbacks will be unregistered by tearDown()
+ }
+
+ /**
* Test that, in a given networking context, calling setPreferenceForUser to set per-profile
* defaults on then off works as expected.
*/
@@ -14657,12 +15420,42 @@
public void testProfileNetworkPrefWrongProfile() throws Exception {
final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
mServiceContext.setWorkProfile(testHandle, false);
- assertThrows("Should not be able to set a user pref for a non-work profile",
+ mServiceContext.setDeviceOwner(testHandle, null);
+ assertThrows("Should not be able to set a user pref for a non-work profile "
+ + "and non device owner",
IllegalArgumentException.class , () ->
mCm.setProfileNetworkPreference(testHandle,
PROFILE_NETWORK_PREFERENCE_ENTERPRISE, null, null));
}
+ /**
+ * Make sure requests for per-profile default networking for a device owner is
+ * accepted on T and not accepted on S
+ */
+ @Test
+ public void testProfileNetworkDeviceOwner() throws Exception {
+ final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
+ mServiceContext.setWorkProfile(testHandle, false);
+ mServiceContext.setDeviceOwner(testHandle, "deviceOwnerPackage");
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ final TestOnCompleteListener listener = new TestOnCompleteListener();
+ if (SdkLevel.isAtLeastT()) {
+ mCm.setProfileNetworkPreferences(testHandle,
+ List.of(profileNetworkPreferenceBuilder.build()),
+ r -> r.run(), listener);
+ } else {
+ // S should not allow setting preference on device owner
+ assertThrows("Should not be able to set a user pref for a non-work profile on S",
+ IllegalArgumentException.class , () ->
+ mCm.setProfileNetworkPreferences(testHandle,
+ List.of(profileNetworkPreferenceBuilder.build()),
+ r -> r.run(), listener));
+ }
+ }
+
@Test
public void testSubIdsClearedWithoutNetworkFactoryPermission() throws Exception {
mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED);
@@ -14822,6 +15615,27 @@
}
@Test
+ public void testAutomotiveEthernetAllowedUids() throws Exception {
+ mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
+ mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
+
+ // In this test the automotive feature will be enabled.
+ mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true);
+
+ // Simulate a restricted ethernet network.
+ final NetworkCapabilities.Builder agentNetCaps = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+
+ mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET,
+ new LinkProperties(), agentNetCaps.build());
+ validateAllowedUids(mEthernetNetworkAgent, TRANSPORT_ETHERNET, agentNetCaps, true);
+ }
+
+ @Test
public void testCbsAllowedUids() throws Exception {
mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED);
mServiceContext.setPermission(MANAGE_TEST_NETWORKS, PERMISSION_GRANTED);
@@ -14830,6 +15644,24 @@
doReturn(true).when(mCarrierPrivilegeAuthenticator)
.hasCarrierPrivilegeForNetworkCapabilities(eq(TEST_PACKAGE_UID), any());
+ // Simulate a restricted telephony network. The telephony factory is entitled to set
+ // the access UID to the service package on any of its restricted networks.
+ final NetworkCapabilities.Builder agentNetCaps = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ .setNetworkSpecifier(new TelephonyNetworkSpecifier(1 /* subid */));
+
+ mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR,
+ new LinkProperties(), agentNetCaps.build());
+ validateAllowedUids(mCellNetworkAgent, TRANSPORT_CELLULAR, agentNetCaps, false);
+ }
+
+ private void validateAllowedUids(final TestNetworkAgentWrapper testAgent,
+ @NetworkCapabilities.Transport final int transportUnderTest,
+ final NetworkCapabilities.Builder ncb, final boolean forAutomotive) throws Exception {
final ArraySet<Integer> serviceUidSet = new ArraySet<>();
serviceUidSet.add(TEST_PACKAGE_UID);
final ArraySet<Integer> nonServiceUidSet = new ArraySet<>();
@@ -14840,40 +15672,34 @@
final TestNetworkCallback cb = new TestNetworkCallback();
- // Simulate a restricted telephony network. The telephony factory is entitled to set
- // the access UID to the service package on any of its restricted networks.
- final NetworkCapabilities.Builder ncb = new NetworkCapabilities.Builder()
- .addTransportType(TRANSPORT_CELLULAR)
- .addCapability(NET_CAPABILITY_INTERNET)
- .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
- .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
- .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
- .setNetworkSpecifier(new TelephonyNetworkSpecifier(1 /* subid */));
-
+ /* Test setting UIDs */
// Cell gets to set the service UID as access UID
mCm.requestNetwork(new NetworkRequest.Builder()
- .addTransportType(TRANSPORT_CELLULAR)
+ .addTransportType(transportUnderTest)
.removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
.build(), cb);
- mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR,
- new LinkProperties(), ncb.build());
- mCellNetworkAgent.connect(true);
- cb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+ testAgent.connect(true);
+ cb.expectAvailableThenValidatedCallbacks(testAgent);
ncb.setAllowedUids(serviceUidSet);
- mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+ testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
if (SdkLevel.isAtLeastT()) {
- cb.expectCapabilitiesThat(mCellNetworkAgent,
+ cb.expectCapabilitiesThat(testAgent,
caps -> caps.getAllowedUids().equals(serviceUidSet));
} else {
// S must ignore access UIDs.
cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
}
+ /* Test setting UIDs is rejected when expected */
+ if (forAutomotive) {
+ mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, false);
+ }
+
// ...but not to some other UID. Rejection sets UIDs to the empty set
ncb.setAllowedUids(nonServiceUidSet);
- mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+ testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
if (SdkLevel.isAtLeastT()) {
- cb.expectCapabilitiesThat(mCellNetworkAgent,
+ cb.expectCapabilitiesThat(testAgent,
caps -> caps.getAllowedUids().isEmpty());
} else {
// S must ignore access UIDs.
@@ -14882,18 +15708,18 @@
// ...and also not to multiple UIDs even including the service UID
ncb.setAllowedUids(serviceUidSetPlus);
- mCellNetworkAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+ testAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
- mCellNetworkAgent.disconnect();
- cb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+ testAgent.disconnect();
+ cb.expectCallback(CallbackEntry.LOST, testAgent);
mCm.unregisterNetworkCallback(cb);
// Must be unset before touching the transports, because remove and add transport types
// check the specifier on the builder immediately, contradicting normal builder semantics
// TODO : fix the builder
ncb.setNetworkSpecifier(null);
- ncb.removeTransportType(TRANSPORT_CELLULAR);
+ ncb.removeTransportType(transportUnderTest);
ncb.addTransportType(TRANSPORT_WIFI);
// Wifi does not get to set access UID, even to the correct UID
mCm.requestNetwork(new NetworkRequest.Builder()
diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
index 45f3d3c..9401d47 100644
--- a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
@@ -89,7 +89,7 @@
public class IpSecServiceParameterizedTest {
@Rule
public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(
- Build.VERSION_CODES.R /* ignoreClassUpTo */);
+ Build.VERSION_CODES.S_V2 /* ignoreClassUpTo */);
private static final int TEST_SPI = 0xD1201D;
@@ -783,6 +783,23 @@
}
@Test
+ public void testSetNetworkForTunnelInterfaceFailsForNullLp() throws Exception {
+ final IpSecTunnelInterfaceResponse createTunnelResp =
+ createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
+ final Network newFakeNetwork = new Network(1000);
+ final int tunnelIfaceResourceId = createTunnelResp.resourceId;
+
+ try {
+ mIpSecService.setNetworkForTunnelInterface(
+ tunnelIfaceResourceId, newFakeNetwork, BLESSED_PACKAGE);
+ fail(
+ "Expected an IllegalArgumentException for underlying network with null"
+ + " LinkProperties");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test
public void testSetNetworkForTunnelInterfaceFailsForInvalidResourceId() throws Exception {
final IpSecTunnelInterfaceResponse createTunnelResp =
createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE);
diff --git a/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
index 5c7ca6f..8595ab9 100644
--- a/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java
@@ -54,7 +54,7 @@
/** Unit tests for {@link IpSecService.RefcountedResource}. */
@SmallTest
@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class IpSecServiceRefcountedResourceTest {
Context mMockContext;
IpSecService.Dependencies mMockDeps;
diff --git a/tests/unit/java/com/android/server/IpSecServiceTest.java b/tests/unit/java/com/android/server/IpSecServiceTest.java
index 7e6b157..6955620 100644
--- a/tests/unit/java/com/android/server/IpSecServiceTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceTest.java
@@ -75,7 +75,7 @@
/** Unit tests for {@link IpSecService}. */
@SmallTest
@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class IpSecServiceTest {
private static final int DROID_SPI = 0xD1201D;
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 3c228d0..9365bee 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -19,10 +19,13 @@
import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
@@ -36,6 +39,12 @@
import android.compat.testing.PlatformCompatChangeRule;
import android.content.ContentResolver;
import android.content.Context;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.mdns.aidl.DiscoveryInfo;
+import android.net.mdns.aidl.GetAddressInfo;
+import android.net.mdns.aidl.IMDnsEventListener;
+import android.net.mdns.aidl.ResolutionInfo;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
import android.net.nsd.MDnsManager;
@@ -63,6 +72,7 @@
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.mockito.AdditionalAnswers;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -74,7 +84,7 @@
// - test NSD_ON ENABLE/DISABLED listening
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NsdServiceTest {
static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
@@ -114,6 +124,10 @@
doReturn(MDnsManager.MDNS_SERVICE).when(mContext)
.getSystemServiceName(MDnsManager.class);
doReturn(mMockMDnsM).when(mContext).getSystemService(MDnsManager.MDNS_SERVICE);
+ if (mContext.getSystemService(MDnsManager.class) == null) {
+ // Test is using mockito-extended
+ doCallRealMethod().when(mContext).getSystemService(MDnsManager.class);
+ }
doReturn(true).when(mMockMDnsM).registerService(
anyInt(), anyString(), anyString(), anyInt(), any(), anyInt());
doReturn(true).when(mMockMDnsM).stopOperation(anyInt());
@@ -275,6 +289,105 @@
verify(mMockMDnsM, never()).stopDaemon();
}
+ @Test
+ public void testDiscoverOnTetheringDownstream() throws Exception {
+ NsdService service = makeService();
+ NsdManager client = connectClient(service);
+
+ final String serviceType = "a_type";
+ final String serviceName = "a_name";
+ final String domainName = "mytestdevice.local";
+ final int interfaceIdx = 123;
+ final NsdManager.DiscoveryListener discListener = mock(NsdManager.DiscoveryListener.class);
+ client.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discListener);
+ waitForIdle();
+
+ final ArgumentCaptor<IMDnsEventListener> listenerCaptor =
+ ArgumentCaptor.forClass(IMDnsEventListener.class);
+ verify(mMockMDnsM).registerEventListener(listenerCaptor.capture());
+ final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(serviceType),
+ eq(0) /* interfaceIdx */);
+ // NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
+ // this needs to use a timeout
+ verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(serviceType);
+
+ final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
+ discIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_FOUND,
+ serviceName,
+ serviceType,
+ domainName,
+ interfaceIdx,
+ INetd.LOCAL_NET_ID); // LOCAL_NET_ID (99) used on tethering downstreams
+ final IMDnsEventListener eventListener = listenerCaptor.getValue();
+ eventListener.onServiceDiscoveryStatus(discoveryInfo);
+ waitForIdle();
+
+ final ArgumentCaptor<NsdServiceInfo> discoveredInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(discoveredInfoCaptor.capture());
+ final NsdServiceInfo foundInfo = discoveredInfoCaptor.getValue();
+ assertEquals(serviceName, foundInfo.getServiceName());
+ assertEquals(serviceType, foundInfo.getServiceType());
+ assertNull(foundInfo.getHost());
+ assertNull(foundInfo.getNetwork());
+ assertEquals(interfaceIdx, foundInfo.getInterfaceIndex());
+
+ // After discovering the service, verify resolving it
+ final NsdManager.ResolveListener resolveListener = mock(NsdManager.ResolveListener.class);
+ client.resolveService(foundInfo, resolveListener);
+ waitForIdle();
+
+ final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(serviceName), eq(serviceType),
+ eq("local.") /* domain */, eq(interfaceIdx));
+
+ final int servicePort = 10123;
+ final String serviceFullName = serviceName + "." + serviceType;
+ final ResolutionInfo resolutionInfo = new ResolutionInfo(
+ resolvIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_RESOLVED,
+ null /* serviceName */,
+ null /* serviceType */,
+ null /* domain */,
+ serviceFullName,
+ domainName,
+ servicePort,
+ new byte[0] /* txtRecord */,
+ interfaceIdx);
+
+ doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
+ eventListener.onServiceResolutionStatus(resolutionInfo);
+ waitForIdle();
+
+ final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(domainName),
+ eq(interfaceIdx));
+
+ final String serviceAddress = "192.0.2.123";
+ final GetAddressInfo addressInfo = new GetAddressInfo(
+ getAddrIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
+ serviceFullName,
+ serviceAddress,
+ interfaceIdx,
+ INetd.LOCAL_NET_ID);
+ eventListener.onGettingServiceAddressStatus(addressInfo);
+ waitForIdle();
+
+ final ArgumentCaptor<NsdServiceInfo> resInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ verify(resolveListener, timeout(TIMEOUT_MS)).onServiceResolved(resInfoCaptor.capture());
+ final NsdServiceInfo resolvedService = resInfoCaptor.getValue();
+ assertEquals(serviceName, resolvedService.getServiceName());
+ assertEquals("." + serviceType, resolvedService.getServiceType());
+ assertEquals(InetAddresses.parseNumericAddress(serviceAddress), resolvedService.getHost());
+ assertEquals(servicePort, resolvedService.getPort());
+ assertNull(resolvedService.getNetwork());
+ assertEquals(interfaceIdx, resolvedService.getInterfaceIndex());
+ }
+
private void waitForIdle() {
HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
}
diff --git a/tests/unit/java/com/android/server/VpnManagerServiceTest.java b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
new file mode 100644
index 0000000..c814cc5
--- /dev/null
+++ b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.os.Build.VERSION_CODES.R;
+
+import static com.android.testutils.ContextUtils.mockService;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.UserIdInt;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.connectivity.Vpn;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@IgnoreUpTo(R) // VpnManagerService is not available before R
+@SmallTest
+public class VpnManagerServiceTest extends VpnTestBase {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+ private static final int TIMEOUT_MS = 2_000;
+
+ @Mock Context mContext;
+ @Mock Context mSystemContext;
+ @Mock Context mUserAllContext;
+ private HandlerThread mHandlerThread;
+ @Mock private Vpn mVpn;
+ @Mock private INetworkManagementService mNms;
+ @Mock private ConnectivityManager mCm;
+ @Mock private UserManager mUserManager;
+ @Mock private INetd mNetd;
+ @Mock private PackageManager mPackageManager;
+
+ private VpnManagerServiceDependencies mDeps;
+ private VpnManagerService mService;
+ private BroadcastReceiver mUserPresentReceiver;
+ private BroadcastReceiver mIntentReceiver;
+ private final String mNotMyVpnPkg = "com.not.my.vpn";
+
+ class VpnManagerServiceDependencies extends VpnManagerService.Dependencies {
+ @Override
+ public HandlerThread makeHandlerThread() {
+ return mHandlerThread;
+ }
+
+ @Override
+ public INetworkManagementService getINetworkManagementService() {
+ return mNms;
+ }
+
+ @Override
+ public INetd getNetd() {
+ return mNetd;
+ }
+
+ @Override
+ public Vpn createVpn(Looper looper, Context context, INetworkManagementService nms,
+ INetd netd, @UserIdInt int userId) {
+ return mVpn;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mHandlerThread = new HandlerThread("TestVpnManagerService");
+ mDeps = new VpnManagerServiceDependencies();
+ doReturn(mUserAllContext).when(mContext).createContextAsUser(UserHandle.ALL, 0);
+ doReturn(mSystemContext).when(mContext).createContextAsUser(UserHandle.SYSTEM, 0);
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+ setMockedPackages(mPackageManager, sPackages);
+
+ mockService(mContext, ConnectivityManager.class, Context.CONNECTIVITY_SERVICE, mCm);
+ mockService(mContext, UserManager.class, Context.USER_SERVICE, mUserManager);
+ doReturn(SYSTEM_USER).when(mUserManager).getUserInfo(eq(SYSTEM_USER_ID));
+
+ mService = new VpnManagerService(mContext, mDeps);
+ mService.systemReady();
+
+ final ArgumentCaptor<BroadcastReceiver> intentReceiverCaptor =
+ ArgumentCaptor.forClass(BroadcastReceiver.class);
+ final ArgumentCaptor<BroadcastReceiver> userPresentReceiverCaptor =
+ ArgumentCaptor.forClass(BroadcastReceiver.class);
+ verify(mSystemContext).registerReceiver(
+ userPresentReceiverCaptor.capture(), any(), any(), any());
+ verify(mUserAllContext, times(2)).registerReceiver(
+ intentReceiverCaptor.capture(), any(), any(), any());
+ mUserPresentReceiver = userPresentReceiverCaptor.getValue();
+ mIntentReceiver = intentReceiverCaptor.getValue();
+
+ // Add user to create vpn in mVpn
+ onUserStarted(SYSTEM_USER_ID);
+ assertNotNull(mService.mVpns.get(SYSTEM_USER_ID));
+ }
+
+ @Test
+ public void testUpdateAppExclusionList() {
+ // Start vpn
+ mService.startVpnProfile(TEST_VPN_PKG);
+ verify(mVpn).startVpnProfile(eq(TEST_VPN_PKG));
+
+ // Remove package due to package replaced.
+ onPackageRemoved(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
+ verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
+
+ // Add package due to package replaced.
+ onPackageAdded(PKGS[0], PKG_UIDS[0], true /* isReplacing */);
+ verify(mVpn, never()).refreshPlatformVpnAppExclusionList();
+
+ // Remove package
+ onPackageRemoved(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
+ verify(mVpn).refreshPlatformVpnAppExclusionList();
+
+ // Add the package back
+ onPackageAdded(PKGS[0], PKG_UIDS[0], false /* isReplacing */);
+ verify(mVpn, times(2)).refreshPlatformVpnAppExclusionList();
+ }
+
+ @Test
+ public void testStartVpnProfileFromDiffPackage() {
+ assertThrows(
+ SecurityException.class, () -> mService.startVpnProfile(mNotMyVpnPkg));
+ }
+
+ @Test
+ public void testStopVpnProfileFromDiffPackage() {
+ assertThrows(SecurityException.class, () -> mService.stopVpnProfile(mNotMyVpnPkg));
+ }
+
+ @Test
+ public void testGetProvisionedVpnProfileStateFromDiffPackage() {
+ assertThrows(SecurityException.class, () ->
+ mService.getProvisionedVpnProfileState(mNotMyVpnPkg));
+ }
+
+ @Test
+ public void testGetProvisionedVpnProfileState() {
+ mService.getProvisionedVpnProfileState(TEST_VPN_PKG);
+ verify(mVpn).getProvisionedVpnProfileState(TEST_VPN_PKG);
+ }
+
+ private Intent buildIntent(String action, String packageName, int userId, int uid,
+ boolean isReplacing) {
+ final Intent intent = new Intent(action);
+ intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
+ intent.putExtra(Intent.EXTRA_UID, uid);
+ intent.putExtra(Intent.EXTRA_REPLACING, isReplacing);
+ if (packageName != null) {
+ intent.setData(Uri.fromParts("package" /* scheme */, packageName, null /* fragment */));
+ }
+
+ return intent;
+ }
+
+ private void sendIntent(Intent intent) {
+ final Handler h = mHandlerThread.getThreadHandler();
+
+ // Send in handler thread.
+ h.post(() -> mIntentReceiver.onReceive(mContext, intent));
+ HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+ }
+
+ private void onUserStarted(int userId) {
+ sendIntent(buildIntent(Intent.ACTION_USER_STARTED,
+ null /* packageName */, userId, -1 /* uid */, false /* isReplacing */));
+ }
+
+ private void onPackageAdded(String packageName, int userId, int uid, boolean isReplacing) {
+ sendIntent(buildIntent(Intent.ACTION_PACKAGE_ADDED, packageName, userId, uid, isReplacing));
+ }
+
+ private void onPackageAdded(String packageName, int uid, boolean isReplacing) {
+ onPackageAdded(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
+ }
+
+ private void onPackageRemoved(String packageName, int userId, int uid, boolean isReplacing) {
+ sendIntent(buildIntent(Intent.ACTION_PACKAGE_REMOVED, packageName, userId, uid,
+ isReplacing));
+ }
+
+ private void onPackageRemoved(String packageName, int uid, boolean isReplacing) {
+ onPackageRemoved(packageName, UserHandle.USER_SYSTEM, uid, isReplacing);
+ }
+
+ @Test
+ public void testReceiveIntentFromNonHandlerThread() {
+ assertThrows(IllegalStateException.class, () ->
+ mIntentReceiver.onReceive(mContext, buildIntent(Intent.ACTION_PACKAGE_REMOVED,
+ PKGS[0], UserHandle.USER_SYSTEM, PKG_UIDS[0], true /* isReplacing */)));
+
+ assertThrows(IllegalStateException.class, () ->
+ mUserPresentReceiver.onReceive(mContext, new Intent(Intent.ACTION_USER_PRESENT)));
+ }
+}
diff --git a/tests/unit/java/com/android/server/VpnTestBase.java b/tests/unit/java/com/android/server/VpnTestBase.java
new file mode 100644
index 0000000..6113872
--- /dev/null
+++ b/tests/unit/java/com/android/server/VpnTestBase.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import static android.content.pm.UserInfo.FLAG_ADMIN;
+import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
+import static android.content.pm.UserInfo.FLAG_PRIMARY;
+import static android.content.pm.UserInfo.FLAG_RESTRICTED;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/** Common variables or methods shared between VpnTest and VpnManagerServiceTest. */
+public class VpnTestBase {
+ protected static final String TEST_VPN_PKG = "com.testvpn.vpn";
+ /**
+ * Names and UIDs for some fake packages. Important points:
+ * - UID is ordered increasing.
+ * - One pair of packages have consecutive UIDs.
+ */
+ protected static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
+ protected static final int[] PKG_UIDS = {10066, 10077, 10078, 10400};
+ // Mock packages
+ protected static final Map<String, Integer> sPackages = new ArrayMap<>();
+ static {
+ for (int i = 0; i < PKGS.length; i++) {
+ sPackages.put(PKGS[i], PKG_UIDS[i]);
+ }
+ sPackages.put(TEST_VPN_PKG, Process.myUid());
+ }
+
+ // Mock users
+ protected static final int SYSTEM_USER_ID = 0;
+ protected static final UserInfo SYSTEM_USER = new UserInfo(0, "system", UserInfo.FLAG_PRIMARY);
+ protected static final UserInfo PRIMARY_USER = new UserInfo(27, "Primary",
+ FLAG_ADMIN | FLAG_PRIMARY);
+ protected static final UserInfo SECONDARY_USER = new UserInfo(15, "Secondary", FLAG_ADMIN);
+ protected static final UserInfo RESTRICTED_PROFILE_A = new UserInfo(40, "RestrictedA",
+ FLAG_RESTRICTED);
+ protected static final UserInfo RESTRICTED_PROFILE_B = new UserInfo(42, "RestrictedB",
+ FLAG_RESTRICTED);
+ protected static final UserInfo MANAGED_PROFILE_A = new UserInfo(45, "ManagedA",
+ FLAG_MANAGED_PROFILE);
+ static {
+ RESTRICTED_PROFILE_A.restrictedProfileParentId = PRIMARY_USER.id;
+ RESTRICTED_PROFILE_B.restrictedProfileParentId = SECONDARY_USER.id;
+ MANAGED_PROFILE_A.profileGroupId = PRIMARY_USER.id;
+ }
+
+ // Populate a fake packageName-to-UID mapping.
+ protected void setMockedPackages(PackageManager mockPm, final Map<String, Integer> packages) {
+ try {
+ doAnswer(invocation -> {
+ final String appName = (String) invocation.getArguments()[0];
+ final int userId = (int) invocation.getArguments()[1];
+
+ final Integer appId = packages.get(appName);
+ if (appId == null) {
+ throw new PackageManager.NameNotFoundException(appName);
+ }
+
+ return UserHandle.getUid(userId, appId);
+ }).when(mockPm).getPackageUidAsUser(anyString(), anyInt());
+ } catch (Exception e) {
+ }
+ }
+
+ protected List<Integer> toList(int[] arr) {
+ return Arrays.stream(arr).boxed().collect(Collectors.toList());
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
index c3d64cb..feee293 100644
--- a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -30,12 +30,16 @@
import static com.android.testutils.MiscAsserts.assertThrows;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
import android.annotation.NonNull;
import android.net.INetd;
@@ -46,6 +50,7 @@
import androidx.test.filters.SmallTest;
+import com.android.internal.util.IndentingPrintWriter;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.bpf.ClatEgress4Key;
import com.android.net.module.util.bpf.ClatEgress4Value;
@@ -53,6 +58,7 @@
import com.android.net.module.util.bpf.ClatIngress6Value;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.TestBpfMap;
import org.junit.Before;
import org.junit.Test;
@@ -64,6 +70,7 @@
import java.io.FileDescriptor;
import java.io.IOException;
+import java.io.StringWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.util.Objects;
@@ -101,17 +108,17 @@
private static final int RAW_SOCK_FD = 535;
private static final int PACKET_SOCK_FD = 536;
private static final long RAW_SOCK_COOKIE = 27149;
- private static final ParcelFileDescriptor TUN_PFD = new ParcelFileDescriptor(
- new FileDescriptor());
- private static final ParcelFileDescriptor RAW_SOCK_PFD = new ParcelFileDescriptor(
- new FileDescriptor());
- private static final ParcelFileDescriptor PACKET_SOCK_PFD = new ParcelFileDescriptor(
- new FileDescriptor());
+ private static final ParcelFileDescriptor TUN_PFD = spy(new ParcelFileDescriptor(
+ new FileDescriptor()));
+ private static final ParcelFileDescriptor RAW_SOCK_PFD = spy(new ParcelFileDescriptor(
+ new FileDescriptor()));
+ private static final ParcelFileDescriptor PACKET_SOCK_PFD = spy(new ParcelFileDescriptor(
+ new FileDescriptor()));
private static final String EGRESS_PROG_PATH =
- "/sys/fs/bpf/prog_clatd_schedcls_egress4_clat_rawip";
+ "/sys/fs/bpf/net_shared/prog_clatd_schedcls_egress4_clat_rawip";
private static final String INGRESS_PROG_PATH =
- "/sys/fs/bpf/prog_clatd_schedcls_ingress6_clat_ether";
+ "/sys/fs/bpf/net_shared/prog_clatd_schedcls_ingress6_clat_ether";
private static final ClatEgress4Key EGRESS_KEY = new ClatEgress4Key(STACKED_IFINDEX,
INET4_LOCAL4);
private static final ClatEgress4Value EGRESS_VALUE = new ClatEgress4Value(BASE_IFINDEX,
@@ -121,10 +128,13 @@
private static final ClatIngress6Value INGRESS_VALUE = new ClatIngress6Value(STACKED_IFINDEX,
INET4_LOCAL4);
+ private final TestBpfMap<ClatIngress6Key, ClatIngress6Value> mIngressMap =
+ spy(new TestBpfMap<>(ClatIngress6Key.class, ClatIngress6Value.class));
+ private final TestBpfMap<ClatEgress4Key, ClatEgress4Value> mEgressMap =
+ spy(new TestBpfMap<>(ClatEgress4Key.class, ClatEgress4Value.class));
+
@Mock private INetd mNetd;
@Spy private TestDependencies mDeps = new TestDependencies();
- @Mock private IBpfMap<ClatIngress6Key, ClatIngress6Value> mIngressMap;
- @Mock private IBpfMap<ClatEgress4Key, ClatEgress4Value> mEgressMap;
/**
* The dependency injection class is used to mock the JNI functions and system functions
@@ -505,4 +515,207 @@
// Expected mtu is that CLAT_MAX_MTU(65536) minus MTU_DELTA(28).
assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
}
+
+ @Test
+ public void testDump() throws Exception {
+ final ClatCoordinator coordinator = makeClatCoordinator();
+ final StringWriter stringWriter = new StringWriter();
+ final IndentingPrintWriter ipw = new IndentingPrintWriter(stringWriter, " ");
+ coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX);
+ coordinator.dump(ipw);
+
+ final String[] dumpStrings = stringWriter.toString().split("\n");
+ assertEquals(6, dumpStrings.length);
+ assertEquals("CLAT tracker: iface: test0 (1000), v4iface: v4-test0 (1001), "
+ + "v4: /192.0.0.46, v6: /2001:db8:0:b11::464, pfx96: /64:ff9b::, "
+ + "pid: 10483, cookie: 27149", dumpStrings[0].trim());
+ assertEquals("Forwarding rules:", dumpStrings[1].trim());
+ assertEquals("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif",
+ dumpStrings[2].trim());
+ assertEquals("1000 /64:ff9b::/96 /2001:db8:0:b11::464 -> /192.0.0.46 1001",
+ dumpStrings[3].trim());
+ assertEquals("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif",
+ dumpStrings[4].trim());
+ assertEquals("1001 /192.0.0.46 -> /2001:db8:0:b11::464 /64:ff9b::/96 1000 ether",
+ dumpStrings[5].trim());
+ }
+
+ @Test
+ public void testNotStartClatWithInvalidPrefix() throws Exception {
+ final ClatCoordinator coordinator = makeClatCoordinator();
+ final IpPrefix invalidPrefix = new IpPrefix("2001:db8::/64");
+ assertThrows(IOException.class,
+ () -> coordinator.clatStart(BASE_IFACE, NETID, invalidPrefix));
+ }
+
+ private void assertStartClat(final TestDependencies deps) throws Exception {
+ final ClatCoordinator coordinator = new ClatCoordinator(deps);
+ assertNotNull(coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
+ }
+
+ private void assertNotStartClat(final TestDependencies deps) {
+ // Expect that the injection function of TestDependencies causes clatStart() failed.
+ final ClatCoordinator coordinator = new ClatCoordinator(deps);
+ assertThrows(IOException.class,
+ () -> coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
+ }
+
+ private void checkNotStartClat(final TestDependencies deps, final boolean needToCloseTunFd,
+ final boolean needToClosePacketSockFd, final boolean needToCloseRawSockFd)
+ throws Exception {
+ // [1] Expect that modified TestDependencies can't start clatd.
+ // Use precise check to make sure that there is no unexpected file descriptor closing.
+ clearInvocations(TUN_PFD, RAW_SOCK_PFD, PACKET_SOCK_PFD);
+ assertNotStartClat(deps);
+ if (needToCloseTunFd) {
+ verify(TUN_PFD).close();
+ } else {
+ verify(TUN_PFD, never()).close();
+ }
+ if (needToClosePacketSockFd) {
+ verify(PACKET_SOCK_PFD).close();
+ } else {
+ verify(PACKET_SOCK_PFD, never()).close();
+ }
+ if (needToCloseRawSockFd) {
+ verify(RAW_SOCK_PFD).close();
+ } else {
+ verify(RAW_SOCK_PFD, never()).close();
+ }
+
+ // [2] Expect that unmodified TestDependencies can start clatd.
+ // Used to make sure that the above modified TestDependencies has really broken the
+ // clatd starting.
+ assertStartClat(new TestDependencies());
+ }
+
+ // The following testNotStartClat* tests verifies bunches of code for unwinding the
+ // failure if any.
+ @Test
+ public void testNotStartClatWithNativeFailureSelectIpv4Address() throws Exception {
+ class FailureDependencies extends TestDependencies {
+ @Override
+ public String selectIpv4Address(@NonNull String v4addr, int prefixlen)
+ throws IOException {
+ throw new IOException();
+ }
+ }
+ checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
+ false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+ }
+
+ @Test
+ public void testNotStartClatWithNativeFailureGenerateIpv6Address() throws Exception {
+ class FailureDependencies extends TestDependencies {
+ @Override
+ public String generateIpv6Address(@NonNull String iface, @NonNull String v4,
+ @NonNull String prefix64) throws IOException {
+ throw new IOException();
+ }
+ }
+ checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
+ false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+ }
+
+ @Test
+ public void testNotStartClatWithNativeFailureCreateTunInterface() throws Exception {
+ class FailureDependencies extends TestDependencies {
+ @Override
+ public int createTunInterface(@NonNull String tuniface) throws IOException {
+ throw new IOException();
+ }
+ }
+ checkNotStartClat(new FailureDependencies(), false /* needToCloseTunFd */,
+ false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+ }
+
+ @Test
+ public void testNotStartClatWithNativeFailureDetectMtu() throws Exception {
+ class FailureDependencies extends TestDependencies {
+ @Override
+ public int detectMtu(@NonNull String platSubnet, int platSuffix, int mark)
+ throws IOException {
+ throw new IOException();
+ }
+ }
+ checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+ false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+ }
+
+ @Test
+ public void testNotStartClatWithNativeFailureOpenPacketSocket() throws Exception {
+ class FailureDependencies extends TestDependencies {
+ @Override
+ public int openPacketSocket() throws IOException {
+ throw new IOException();
+ }
+ }
+ checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+ false /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+ }
+
+ @Test
+ public void testNotStartClatWithNativeFailureOpenRawSocket6() throws Exception {
+ class FailureDependencies extends TestDependencies {
+ @Override
+ public int openRawSocket6(int mark) throws IOException {
+ throw new IOException();
+ }
+ }
+ checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+ true /* needToClosePacketSockFd */, false /* needToCloseRawSockFd */);
+ }
+
+ @Test
+ public void testNotStartClatWithNativeFailureAddAnycastSetsockopt() throws Exception {
+ class FailureDependencies extends TestDependencies {
+ @Override
+ public void addAnycastSetsockopt(@NonNull FileDescriptor sock, String v6,
+ int ifindex) throws IOException {
+ throw new IOException();
+ }
+ }
+ checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+ true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
+ }
+
+ @Test
+ public void testNotStartClatWithNativeFailureTagSocketAsClat() throws Exception {
+ class FailureDependencies extends TestDependencies {
+ @Override
+ public long tagSocketAsClat(@NonNull FileDescriptor sock) throws IOException {
+ throw new IOException();
+ }
+ }
+ checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+ true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
+ }
+
+ @Test
+ public void testNotStartClatWithNativeFailureConfigurePacketSocket() throws Exception {
+ class FailureDependencies extends TestDependencies {
+ @Override
+ public void configurePacketSocket(@NonNull FileDescriptor sock, String v6,
+ int ifindex) throws IOException {
+ throw new IOException();
+ }
+ }
+ checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+ true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
+ }
+
+ @Test
+ public void testNotStartClatWithNativeFailureStartClatd() throws Exception {
+ class FailureDependencies extends TestDependencies {
+ @Override
+ public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+ @NonNull FileDescriptor writesock6, @NonNull String iface,
+ @NonNull String pfx96, @NonNull String v4, @NonNull String v6)
+ throws IOException {
+ throw new IOException();
+ }
+ }
+ checkNotStartClat(new FailureDependencies(), true /* needToCloseTunFd */,
+ true /* needToClosePacketSockFd */, true /* needToCloseRawSockFd */);
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
index ec51537..d1bf40e 100644
--- a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
@@ -27,6 +27,7 @@
import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
@@ -184,7 +185,7 @@
(int) setting);
}
- private void testGetMultipathPreference(
+ private void prepareGetMultipathPreferenceTest(
long usedBytesToday, long subscriptionQuota, long policyWarning, long policyLimit,
long defaultGlobalSetting, long defaultResSetting, boolean roaming) {
@@ -286,7 +287,7 @@
@Test
public void testGetMultipathPreference_SubscriptionQuota() {
- testGetMultipathPreference(
+ prepareGetMultipathPreferenceTest(
DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
DataUnit.MEGABYTES.toBytes(14) /* subscriptionQuota */,
DataUnit.MEGABYTES.toBytes(100) /* policyWarning */,
@@ -301,7 +302,7 @@
@Test
public void testGetMultipathPreference_UserWarningQuota() {
- testGetMultipathPreference(
+ prepareGetMultipathPreferenceTest(
DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
OPPORTUNISTIC_QUOTA_UNKNOWN,
// Remaining days are 29 days from Apr. 2nd to May 1st.
@@ -320,7 +321,7 @@
@Test
public void testGetMultipathPreference_SnoozedWarningQuota() {
- testGetMultipathPreference(
+ prepareGetMultipathPreferenceTest(
DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
OPPORTUNISTIC_QUOTA_UNKNOWN,
POLICY_SNOOZED /* policyWarning */,
@@ -339,7 +340,7 @@
@Test
public void testGetMultipathPreference_SnoozedBothQuota() {
- testGetMultipathPreference(
+ prepareGetMultipathPreferenceTest(
DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
OPPORTUNISTIC_QUOTA_UNKNOWN,
// 29 days from Apr. 2nd to May 1st
@@ -356,7 +357,7 @@
@Test
public void testGetMultipathPreference_SettingChanged() {
- testGetMultipathPreference(
+ prepareGetMultipathPreferenceTest(
DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
OPPORTUNISTIC_QUOTA_UNKNOWN,
WARNING_DISABLED,
@@ -381,7 +382,7 @@
@Test
public void testGetMultipathPreference_ResourceChanged() {
- testGetMultipathPreference(
+ prepareGetMultipathPreferenceTest(
DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
OPPORTUNISTIC_QUOTA_UNKNOWN,
WARNING_DISABLED,
@@ -404,4 +405,45 @@
verify(mStatsManager, times(1)).registerUsageCallback(
any(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any());
}
+
+ @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+ @Test
+ public void testOnThresholdReached() {
+ prepareGetMultipathPreferenceTest(
+ DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
+ DataUnit.MEGABYTES.toBytes(14) /* subscriptionQuota */,
+ DataUnit.MEGABYTES.toBytes(100) /* policyWarning */,
+ LIMIT_DISABLED,
+ DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
+ 2_500_000 /* defaultResSetting */,
+ false /* roaming */);
+
+ final ArgumentCaptor<NetworkStatsManager.UsageCallback> usageCallbackCaptor =
+ ArgumentCaptor.forClass(NetworkStatsManager.UsageCallback.class);
+ final ArgumentCaptor<NetworkTemplate> networkTemplateCaptor =
+ ArgumentCaptor.forClass(NetworkTemplate.class);
+ // Verify the callback is registered with quota - used = 14 - 2 = 12MB.
+ verify(mStatsManager, times(1)).registerUsageCallback(
+ networkTemplateCaptor.capture(), eq(DataUnit.MEGABYTES.toBytes(12)), any(),
+ usageCallbackCaptor.capture());
+
+ // Capture arguments for later use.
+ final NetworkStatsManager.UsageCallback usageCallback = usageCallbackCaptor.getValue();
+ final NetworkTemplate template = networkTemplateCaptor.getValue();
+ assertNotNull(usageCallback);
+ assertNotNull(template);
+
+ // Decrease quota from 14 to 11, and trigger the event.
+ // TODO: Mock daily and monthly used bytes instead of changing subscription to simulate
+ // remaining quota changed.
+ when(mNPMI.getSubscriptionOpportunisticQuota(TEST_NETWORK, QUOTA_TYPE_MULTIPATH))
+ .thenReturn(DataUnit.MEGABYTES.toBytes(11));
+ usageCallback.onThresholdReached(template);
+
+ // Callback must have been re-registered with new remaining quota = 11 - 2 = 9MB.
+ verify(mStatsManager, times(1))
+ .unregisterUsageCallback(eq(usageCallback));
+ verify(mStatsManager, times(1)).registerUsageCallback(
+ eq(template), eq(DataUnit.MEGABYTES.toBytes(9)), any(), eq(usageCallback));
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
index aa4c4e3..06e0d6d 100644
--- a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
+++ b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java
@@ -21,6 +21,8 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.inOrder;
@@ -44,8 +46,11 @@
import android.os.Handler;
import android.os.test.TestLooper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
+import com.android.modules.utils.build.SdkLevel;
import com.android.server.ConnectivityService;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -75,13 +80,20 @@
@Mock IDnsResolver mDnsResolver;
@Mock INetd mNetd;
@Mock NetworkAgentInfo mNai;
+ @Mock ClatCoordinator mClatCoordinator;
TestLooper mLooper;
Handler mHandler;
NetworkAgentConfig mAgentConfig = new NetworkAgentConfig();
Nat464Xlat makeNat464Xlat(boolean isCellular464XlatEnabled) {
- return new Nat464Xlat(mNai, mNetd, mDnsResolver, new ConnectivityService.Dependencies()) {
+ final ConnectivityService.Dependencies deps = new ConnectivityService.Dependencies() {
+ @Override public ClatCoordinator getClatCoordinator(INetd netd) {
+ return mClatCoordinator;
+ }
+ };
+
+ return new Nat464Xlat(mNai, mNetd, mDnsResolver, deps) {
@Override protected int getNetId() {
return NETID;
}
@@ -208,6 +220,39 @@
}
}
+ private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+ if (inOrder != null) {
+ return inOrder.verify(t);
+ } else {
+ return verify(t);
+ }
+ }
+
+ private void verifyClatdStart(@Nullable InOrder inOrder) throws Exception {
+ if (SdkLevel.isAtLeastT()) {
+ verifyWithOrder(inOrder, mClatCoordinator)
+ .clatStart(eq(BASE_IFACE), eq(NETID), eq(new IpPrefix(NAT64_PREFIX)));
+ } else {
+ verifyWithOrder(inOrder, mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+ }
+ }
+
+ private void verifyNeverClatdStart() throws Exception {
+ if (SdkLevel.isAtLeastT()) {
+ verify(mClatCoordinator, never()).clatStart(anyString(), anyInt(), any());
+ } else {
+ verify(mNetd, never()).clatdStart(anyString(), anyString());
+ }
+ }
+
+ private void verifyClatdStop(@Nullable InOrder inOrder) throws Exception {
+ if (SdkLevel.isAtLeastT()) {
+ verifyWithOrder(inOrder, mClatCoordinator).clatStop();
+ } else {
+ verifyWithOrder(inOrder, mNetd).clatdStop(eq(BASE_IFACE));
+ }
+ }
+
private void checkNormalStartAndStop(boolean dueToDisconnect) throws Exception {
Nat464Xlat nat = makeNat464Xlat(true);
ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
@@ -219,7 +264,7 @@
// Start clat.
nat.start();
- verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+ verifyClatdStart(null /* inOrder */);
// Stacked interface up notification arrives.
nat.interfaceLinkStateChanged(STACKED_IFACE, true);
@@ -235,7 +280,7 @@
makeClatUnnecessary(dueToDisconnect);
nat.stop();
- verify(mNetd).clatdStop(eq(BASE_IFACE));
+ verifyClatdStop(null /* inOrder */);
verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
assertTrue(c.getValue().getStackedLinks().isEmpty());
assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE));
@@ -262,7 +307,7 @@
private void checkStartStopStart(boolean interfaceRemovedFirst) throws Exception {
Nat464Xlat nat = makeNat464Xlat(true);
ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
- InOrder inOrder = inOrder(mNetd, mConnectivity);
+ InOrder inOrder = inOrder(mNetd, mConnectivity, mClatCoordinator);
mNai.linkProperties.addLinkAddress(V6ADDR);
@@ -270,7 +315,7 @@
nat.start();
- inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+ verifyClatdStart(inOrder);
// Stacked interface up notification arrives.
nat.interfaceLinkStateChanged(STACKED_IFACE, true);
@@ -284,7 +329,7 @@
// ConnectivityService stops clat (Network disconnects, IPv4 addr appears, ...).
nat.stop();
- inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE));
+ verifyClatdStop(inOrder);
inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
assertTrue(c.getValue().getStackedLinks().isEmpty());
@@ -306,7 +351,7 @@
nat.start();
- inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+ verifyClatdStart(inOrder);
if (!interfaceRemovedFirst) {
// Stacked interface removed notification arrives and is ignored.
@@ -328,7 +373,7 @@
// ConnectivityService stops clat again.
nat.stop();
- inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE));
+ verifyClatdStop(inOrder);
inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture());
assertTrue(c.getValue().getStackedLinks().isEmpty());
@@ -357,7 +402,7 @@
nat.start();
- verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+ verifyClatdStart(null /* inOrder */);
// Stacked interface up notification arrives.
nat.interfaceLinkStateChanged(STACKED_IFACE, true);
@@ -373,7 +418,7 @@
nat.interfaceRemoved(STACKED_IFACE);
mLooper.dispatchNext();
- verify(mNetd).clatdStop(eq(BASE_IFACE));
+ verifyClatdStop(null /* inOrder */);
verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture());
verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
assertTrue(c.getValue().getStackedLinks().isEmpty());
@@ -395,13 +440,13 @@
nat.start();
- verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+ verifyClatdStart(null /* inOrder */);
// ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
makeClatUnnecessary(dueToDisconnect);
nat.stop();
- verify(mNetd).clatdStop(eq(BASE_IFACE));
+ verifyClatdStop(null /* inOrder */);
verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
assertIdle(nat);
@@ -437,13 +482,13 @@
nat.start();
- verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
+ verifyClatdStart(null /* inOrder */);
// ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
makeClatUnnecessary(dueToDisconnect);
nat.stop();
- verify(mNetd).clatdStop(eq(BASE_IFACE));
+ verifyClatdStop(null /* inOrder */);
verify(mDnsResolver).stopPrefix64Discovery(eq(NETID));
assertIdle(nat);
@@ -518,7 +563,7 @@
mNai.linkProperties.setNat64Prefix(nat64Prefix);
nat.setNat64PrefixFromRa(nat64Prefix);
nat.update();
- verify(mNetd, never()).clatdStart(anyString(), anyString());
+ verifyNeverClatdStart();
assertIdle(nat);
} else {
// Prefix discovery is started.
@@ -529,7 +574,7 @@
mNai.linkProperties.setNat64Prefix(nat64Prefix);
nat.setNat64PrefixFromRa(nat64Prefix);
nat.update();
- verify(mNetd).clatdStart(BASE_IFACE, NAT64_PREFIX);
+ verifyClatdStart(null /* inOrder */);
assertStarting(nat);
}
}
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index fb821c3..354e79a 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -695,7 +695,8 @@
mNetdMonitor.expectNetworkPerm(PERMISSION_SYSTEM, new UserHandle[]{MOCK_USER1},
SYSTEM_APPID1);
- final List<PackageInfo> pkgs = List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID21,
+ final List<PackageInfo> pkgs = List.of(
+ buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID21,
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
buildPackageInfo(SYSTEM_PACKAGE2, SYSTEM_APP_UID21, CHANGE_NETWORK_STATE));
doReturn(pkgs).when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS),
@@ -761,9 +762,10 @@
MOCK_APPID1);
}
- @Test
- public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
- doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+ private void doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
+ throws Exception {
+ doReturn(List.of(
+ buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
@@ -771,15 +773,15 @@
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID11);
mPermissionMonitor.startMonitoring();
- // Every app on user 0 except MOCK_UID12 are under VPN.
+ // Every app on user 0 except MOCK_UID12 is subject to the VPN.
final Set<UidRange> vpnRange1 = Set.of(
new UidRange(0, MOCK_UID12 - 1),
new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1));
final Set<UidRange> vpnRange2 = Set.of(new UidRange(MOCK_UID12, MOCK_UID12));
// When VPN is connected, expect a rule to be set up for user app MOCK_UID11
- mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange1, VPN_UID);
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
+ mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange1, VPN_UID);
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
reset(mBpfNetMaps);
@@ -787,28 +789,40 @@
mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID11}));
mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_UID11);
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
reset(mBpfNetMaps);
// During VPN uid update (vpnRange1 -> vpnRange2), ConnectivityService first deletes the
// old UID rules then adds the new ones. Expect netd to be updated
- mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange1, VPN_UID);
+ mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange1, VPN_UID);
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID11}));
- mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange2, VPN_UID);
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID12}));
+ mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange2, VPN_UID);
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID12}));
reset(mBpfNetMaps);
// When VPN is disconnected, expect rules to be torn down
- mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange2, VPN_UID);
+ mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange2, VPN_UID);
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID12}));
- assertNull(mPermissionMonitor.getVpnUidRanges("tun0"));
+ assertNull(mPermissionMonitor.getVpnInterfaceUidRanges(ifName));
}
@Test
- public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
- doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+ public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
+ doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
+ }
+
+ @Test
+ public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdatesWithWildcard()
+ throws Exception {
+ doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
+ }
+
+ private void doTestUidFilteringDuringPackageInstallAndUninstall(@Nullable String ifName) throws
+ Exception {
+ doReturn(List.of(
+ buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
@@ -818,12 +832,12 @@
mPermissionMonitor.startMonitoring();
final Set<UidRange> vpnRange = Set.of(UidRange.createForUser(MOCK_USER1),
UidRange.createForUser(MOCK_USER2));
- mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange, VPN_UID);
+ mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange, VPN_UID);
// Newly-installed package should have uid rules added
addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID21}));
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID21}));
// Removed package should have its uid rules removed
mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
@@ -831,6 +845,162 @@
verify(mBpfNetMaps, never()).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID21}));
}
+ @Test
+ public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
+ doTestUidFilteringDuringPackageInstallAndUninstall("tun0");
+ }
+
+ @Test
+ public void testUidFilteringDuringPackageInstallAndUninstallWithWildcard() throws Exception {
+ doTestUidFilteringDuringPackageInstallAndUninstall(null /* ifName */);
+ }
+
+ @Test
+ public void testLockdownUidFilteringWithLockdownEnableDisable() {
+ doReturn(List.of(
+ buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+ CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+ buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+ buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
+ buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+ .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+ mPermissionMonitor.startMonitoring();
+ // Every app on user 0 except MOCK_UID12 is subject to the VPN.
+ final UidRange[] lockdownRange = {
+ new UidRange(0, MOCK_UID12 - 1),
+ new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1)
+ };
+
+ // Add Lockdown uid range, expect a rule to be set up for MOCK_UID11 and VPN_UID
+ mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+ verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, true /* add */);
+ assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
+
+ reset(mBpfNetMaps);
+
+ // Remove Lockdown uid range, expect rules to be torn down
+ mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+ verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, false /* add */);
+ assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+ }
+
+ @Test
+ public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
+ doReturn(List.of(
+ buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+ CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+ buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+ buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+ .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+ mPermissionMonitor.startMonitoring();
+ // MOCK_UID11 is subject to the VPN.
+ final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
+ final UidRange[] lockdownRange = {range};
+
+ // Add Lockdown uid range at 1st time, expect a rule to be set up
+ mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+ verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+ assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
+
+ reset(mBpfNetMaps);
+
+ // Add Lockdown uid range at 2nd time, expect a rule not to be set up because the uid
+ // already has the rule
+ mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+ verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(), anyBoolean());
+ assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
+
+ reset(mBpfNetMaps);
+
+ // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because we added
+ // the range 2 times.
+ mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+ verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(), anyBoolean());
+ assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
+
+ reset(mBpfNetMaps);
+
+ // Remove Lockdown uid range at 2nd time, expect a rule to be torn down because we added
+ // twice and we removed twice.
+ mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+ verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
+ assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+ }
+
+ @Test
+ public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
+ doReturn(List.of(
+ buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+ CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+ buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+ buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+ .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+ mPermissionMonitor.startMonitoring();
+ // MOCK_UID11 is subject to the VPN.
+ final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
+ final UidRange[] lockdownRangeDuplicates = {range, range};
+ final UidRange[] lockdownRange = {range};
+
+ // Add Lockdown uid ranges which contains duplicated uid ranges
+ mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRangeDuplicates);
+ verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+ assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
+
+ reset(mBpfNetMaps);
+
+ // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because uid
+ // ranges we added contains duplicated uid ranges.
+ mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+ verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(), anyBoolean());
+ assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
+
+ reset(mBpfNetMaps);
+
+ // Remove Lockdown uid range at 2nd time, expect a rule to be torn down.
+ mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+ verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
+ assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+ }
+
+ @Test
+ public void testLockdownUidFilteringWithInstallAndUnInstall() {
+ doReturn(List.of(
+ buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+ NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+ buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+ .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+ doReturn(List.of(MOCK_USER1, MOCK_USER2)).when(mUserManager).getUserHandles(eq(true));
+
+ mPermissionMonitor.startMonitoring();
+ final UidRange[] lockdownRange = {
+ UidRange.createForUser(MOCK_USER1),
+ UidRange.createForUser(MOCK_USER2)
+ };
+ mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+
+ reset(mBpfNetMaps);
+
+ // Installing package should add Lockdown rules
+ addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
+ verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID21, true /* add */);
+
+ reset(mBpfNetMaps);
+
+ // Uninstalling package should remove Lockdown rules
+ mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
+ verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+ verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
+ }
// Normal package add/remove operations will trigger multiple intent for uids corresponding to
// each user. To simulate generic package operations, the onPackageAdded/Removed will need to be
@@ -1153,7 +1323,8 @@
public void testOnExternalApplicationsAvailable() throws Exception {
// Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
// and have different uids. There has no permission for both uids.
- doReturn(List.of(buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+ doReturn(List.of(
+ buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12)))
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
mPermissionMonitor.startMonitoring();
@@ -1211,7 +1382,8 @@
throws Exception {
// Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
// storage and shared on MOCK_UID11. There has no permission for MOCK_UID11.
- doReturn(List.of(buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+ doReturn(List.of(
+ buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
buildPackageInfo(MOCK_PACKAGE2, MOCK_UID11)))
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
mPermissionMonitor.startMonitoring();
@@ -1237,7 +1409,8 @@
// Initial the permission state. MOCK_PACKAGE1 is installed on external storage and
// MOCK_PACKAGE2 is installed on device. These two packages are shared on MOCK_UID11.
// MOCK_UID11 has NETWORK and INTERNET permissions.
- doReturn(List.of(buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+ doReturn(List.of(
+ buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
buildPackageInfo(MOCK_PACKAGE2, MOCK_UID11, CHANGE_NETWORK_STATE, INTERNET)))
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
mPermissionMonitor.startMonitoring();
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 33c0868..5c1992d 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -20,28 +20,32 @@
import static android.Manifest.permission.CONTROL_VPN;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.content.pm.UserInfo.FLAG_ADMIN;
-import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
-import static android.content.pm.UserInfo.FLAG_PRIMARY;
-import static android.content.pm.UserInfo.FLAG_RESTRICTED;
import static android.net.ConnectivityManager.NetworkCallback;
import static android.net.INetd.IF_STATE_DOWN;
import static android.net.INetd.IF_STATE_UP;
+import static android.net.RouteInfo.RTN_UNREACHABLE;
+import static android.net.VpnManager.TYPE_VPN_PLATFORM;
+import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE;
+import static android.os.Build.VERSION_CODES.S_V2;
import static android.os.UserHandle.PER_USER_RANGE;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.testutils.Cleanup.testAndCleanup;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import static com.android.testutils.MiscAsserts.assertThrows;
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.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
@@ -54,6 +58,8 @@
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -65,6 +71,7 @@
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
@@ -77,26 +84,40 @@
import android.net.InetAddresses;
import android.net.InterfaceConfigurationParcel;
import android.net.IpPrefix;
+import android.net.IpSecConfig;
import android.net.IpSecManager;
+import android.net.IpSecTransform;
import android.net.IpSecTunnelInterfaceResponse;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.LocalSocket;
import android.net.Network;
+import android.net.NetworkAgent;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo.DetailedState;
import android.net.RouteInfo;
import android.net.UidRangeParcel;
import android.net.VpnManager;
+import android.net.VpnProfileState;
import android.net.VpnService;
import android.net.VpnTransportInfo;
+import android.net.ipsec.ike.ChildSessionCallback;
+import android.net.ipsec.ike.ChildSessionConfiguration;
import android.net.ipsec.ike.IkeSessionCallback;
+import android.net.ipsec.ike.IkeSessionConfiguration;
+import android.net.ipsec.ike.IkeSessionConnectionInfo;
+import android.net.ipsec.ike.IkeTrafficSelector;
+import android.net.ipsec.ike.exceptions.IkeException;
+import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
+import android.net.ipsec.ike.exceptions.IkeNonProtocolException;
import android.net.ipsec.ike.exceptions.IkeProtocolException;
+import android.net.ipsec.ike.exceptions.IkeTimeoutException;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.INetworkManagementService;
import android.os.ParcelFileDescriptor;
+import android.os.PowerWhitelistManager;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
@@ -105,6 +126,7 @@
import android.security.Credentials;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.Pair;
import android.util.Range;
import androidx.test.filters.SmallTest;
@@ -113,12 +135,18 @@
import com.android.internal.net.LegacyVpnInfo;
import com.android.internal.net.VpnConfig;
import com.android.internal.net.VpnProfile;
+import com.android.internal.util.HexDump;
import com.android.modules.utils.build.SdkLevel;
+import com.android.server.DeviceIdleInternal;
import com.android.server.IpSecService;
+import com.android.server.VpnTestBase;
+import com.android.server.vcn.util.PersistableBundleUtils;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
+import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.AdditionalAnswers;
@@ -134,7 +162,9 @@
import java.io.FileWriter;
import java.io.IOException;
import java.net.Inet4Address;
+import java.net.Inet6Address;
import java.net.InetAddress;
+import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -142,7 +172,11 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@@ -154,50 +188,53 @@
*/
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
-public class VpnTest {
+@IgnoreUpTo(S_V2)
+public class VpnTest extends VpnTestBase {
private static final String TAG = "VpnTest";
- // Mock users
- static final UserInfo primaryUser = new UserInfo(27, "Primary", FLAG_ADMIN | FLAG_PRIMARY);
- static final UserInfo secondaryUser = new UserInfo(15, "Secondary", FLAG_ADMIN);
- static final UserInfo restrictedProfileA = new UserInfo(40, "RestrictedA", FLAG_RESTRICTED);
- static final UserInfo restrictedProfileB = new UserInfo(42, "RestrictedB", FLAG_RESTRICTED);
- static final UserInfo managedProfileA = new UserInfo(45, "ManagedA", FLAG_MANAGED_PROFILE);
- static {
- restrictedProfileA.restrictedProfileParentId = primaryUser.id;
- restrictedProfileB.restrictedProfileParentId = secondaryUser.id;
- managedProfileA.profileGroupId = primaryUser.id;
- }
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
static final Network EGRESS_NETWORK = new Network(101);
static final String EGRESS_IFACE = "wlan0";
- static final String TEST_VPN_PKG = "com.testvpn.vpn";
+ private static final String TEST_VPN_CLIENT = "2.4.6.8";
private static final String TEST_VPN_SERVER = "1.2.3.4";
private static final String TEST_VPN_IDENTITY = "identity";
private static final byte[] TEST_VPN_PSK = "psk".getBytes();
+ private static final int IP4_PREFIX_LEN = 32;
+ private static final int MIN_PORT = 0;
+ private static final int MAX_PORT = 65535;
+
+ private static final InetAddress TEST_VPN_CLIENT_IP =
+ InetAddresses.parseNumericAddress(TEST_VPN_CLIENT);
+ private static final InetAddress TEST_VPN_SERVER_IP =
+ InetAddresses.parseNumericAddress(TEST_VPN_SERVER);
+ private static final InetAddress TEST_VPN_CLIENT_IP_2 =
+ InetAddresses.parseNumericAddress("192.0.2.200");
+ private static final InetAddress TEST_VPN_SERVER_IP_2 =
+ InetAddresses.parseNumericAddress("192.0.2.201");
+ private static final InetAddress TEST_VPN_INTERNAL_IP =
+ InetAddresses.parseNumericAddress("198.51.100.10");
+ private static final InetAddress TEST_VPN_INTERNAL_DNS =
+ InetAddresses.parseNumericAddress("8.8.8.8");
+
+ private static final IkeTrafficSelector IN_TS =
+ new IkeTrafficSelector(MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP, TEST_VPN_INTERNAL_IP);
+ private static final IkeTrafficSelector OUT_TS =
+ new IkeTrafficSelector(MIN_PORT, MAX_PORT,
+ InetAddresses.parseNumericAddress("0.0.0.0"),
+ InetAddresses.parseNumericAddress("255.255.255.255"));
+
private static final Network TEST_NETWORK = new Network(Integer.MAX_VALUE);
+ private static final Network TEST_NETWORK_2 = new Network(Integer.MAX_VALUE - 1);
private static final String TEST_IFACE_NAME = "TEST_IFACE";
private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345;
private static final long TEST_TIMEOUT_MS = 500L;
-
- /**
- * Names and UIDs for some fake packages. Important points:
- * - UID is ordered increasing.
- * - One pair of packages have consecutive UIDs.
- */
- static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"};
- static final int[] PKG_UIDS = {66, 77, 78, 400};
-
- // Mock packages
- static final Map<String, Integer> mPackages = new ArrayMap<>();
- static {
- for (int i = 0; i < PKGS.length; i++) {
- mPackages.put(PKGS[i], PKG_UIDS[i]);
- }
- }
- private static final Range<Integer> PRI_USER_RANGE = uidRangeForUser(primaryUser.id);
+ private static final String PRIMARY_USER_APP_EXCLUDE_KEY =
+ "VPNAPPEXCLUDED_27_com.testvpn.vpn";
+ static final String PKGS_BYTES = getPackageByteString(List.of(PKGS));
+ private static final Range<Integer> PRIMARY_USER_RANGE = uidRangeForUser(PRIMARY_USER.id);
@Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext;
@Mock private UserManager mUserManager;
@@ -207,14 +244,21 @@
@Mock private AppOpsManager mAppOps;
@Mock private NotificationManager mNotificationManager;
@Mock private Vpn.SystemServices mSystemServices;
+ @Mock private Vpn.IkeSessionWrapper mIkeSessionWrapper;
@Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator;
+ @Mock private NetworkAgent mMockNetworkAgent;
@Mock private ConnectivityManager mConnectivityManager;
@Mock private IpSecService mIpSecService;
@Mock private VpnProfileStore mVpnProfileStore;
+ @Mock private ScheduledThreadPoolExecutor mExecutor;
+ @Mock private ScheduledFuture mScheduledFuture;
+ @Mock DeviceIdleInternal mDeviceIdleInternal;
private final VpnProfile mVpnProfile;
private IpSecManager mIpSecManager;
+ private TestDeps mTestDeps;
+
public VpnTest() throws Exception {
// Build an actual VPN profile that is capable of being converted to and from an
// Ikev2VpnProfile
@@ -229,9 +273,10 @@
MockitoAnnotations.initMocks(this);
mIpSecManager = new IpSecManager(mContext, mIpSecService);
+ mTestDeps = spy(new TestDeps());
when(mContext.getPackageManager()).thenReturn(mPackageManager);
- setMockedPackages(mPackages);
+ setMockedPackages(sPackages);
when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG);
when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG);
@@ -269,6 +314,33 @@
// itself, so set the default value of Context#checkCallingOrSelfPermission to
// PERMISSION_DENIED.
doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any());
+
+ // Set up mIkev2SessionCreator and mExecutor
+ resetIkev2SessionCreator(mIkeSessionWrapper);
+ resetExecutor(mScheduledFuture);
+ }
+
+ private void resetIkev2SessionCreator(Vpn.IkeSessionWrapper ikeSession) {
+ reset(mIkev2SessionCreator);
+ when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
+ .thenReturn(ikeSession);
+ }
+
+ private void resetExecutor(ScheduledFuture scheduledFuture) {
+ doAnswer(
+ (invocation) -> {
+ ((Runnable) invocation.getArgument(0)).run();
+ return null;
+ })
+ .when(mExecutor)
+ .execute(any());
+ when(mExecutor.schedule(
+ any(Runnable.class), anyLong(), any())).thenReturn(mScheduledFuture);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
}
private <T> void mockService(Class<T> clazz, String name, T service) {
@@ -296,73 +368,99 @@
return new Range<Integer>(start, stop);
}
+ private static String getPackageByteString(List<String> packages) {
+ try {
+ return HexDump.toHexString(
+ PersistableBundleUtils.toDiskStableBytes(PersistableBundleUtils.fromList(
+ packages, PersistableBundleUtils.STRING_SERIALIZER)),
+ true /* upperCase */);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
@Test
public void testRestrictedProfilesAreAddedToVpn() {
- setMockedUsers(primaryUser, secondaryUser, restrictedProfileA, restrictedProfileB);
+ setMockedUsers(PRIMARY_USER, SECONDARY_USER, RESTRICTED_PROFILE_A, RESTRICTED_PROFILE_B);
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
// Assume the user can have restricted profiles.
doReturn(true).when(mUserManager).canHaveRestrictedProfile();
final Set<Range<Integer>> ranges =
- vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, null, null);
+ vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id, null, null);
- assertEquals(rangeSet(PRI_USER_RANGE, uidRangeForUser(restrictedProfileA.id)), ranges);
+ assertEquals(rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id)),
+ ranges);
}
@Test
public void testManagedProfilesAreNotAddedToVpn() {
- setMockedUsers(primaryUser, managedProfileA);
+ setMockedUsers(PRIMARY_USER, MANAGED_PROFILE_A);
- final Vpn vpn = createVpn(primaryUser.id);
- final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
- null, null);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(
+ PRIMARY_USER.id, null, null);
- assertEquals(rangeSet(PRI_USER_RANGE), ranges);
+ assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
}
@Test
public void testAddUserToVpnOnlyAddsOneUser() {
- setMockedUsers(primaryUser, restrictedProfileA, managedProfileA);
+ setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A, MANAGED_PROFILE_A);
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
final Set<Range<Integer>> ranges = new ArraySet<>();
- vpn.addUserToRanges(ranges, primaryUser.id, null, null);
+ vpn.addUserToRanges(ranges, PRIMARY_USER.id, null, null);
- assertEquals(rangeSet(PRI_USER_RANGE), ranges);
+ assertEquals(rangeSet(PRIMARY_USER_RANGE), ranges);
}
@Test
public void testUidAllowAndDenylist() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
- final Range<Integer> user = PRI_USER_RANGE;
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final Range<Integer> user = PRIMARY_USER_RANGE;
final int userStart = user.getLower();
final int userStop = user.getUpper();
final String[] packages = {PKGS[0], PKGS[1], PKGS[2]};
// Allowed list
- final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
+ final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
Arrays.asList(packages), null /* disallowedApplications */);
assertEquals(rangeSet(
uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]),
- uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2])),
+ uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2]),
+ uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0]),
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[0])),
+ uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[1]),
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[2]))),
allow);
// Denied list
final Set<Range<Integer>> disallow =
- vpn.createUserAndRestrictedProfilesRanges(primaryUser.id,
+ vpn.createUserAndRestrictedProfilesRanges(PRIMARY_USER.id,
null /* allowedApplications */, Arrays.asList(packages));
assertEquals(rangeSet(
uidRange(userStart, userStart + PKG_UIDS[0] - 1),
uidRange(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
/* Empty range between UIDS[1] and UIDS[2], should be excluded, */
- uidRange(userStart + PKG_UIDS[2] + 1, userStop)),
+ uidRange(userStart + PKG_UIDS[2] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+ uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+ uidRange(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)),
disallow);
}
+ private void verifyPowerSaveTempWhitelistApp(String packageName) {
+ verify(mDeviceIdleInternal).addPowerSaveTempWhitelistApp(anyInt(), eq(packageName),
+ anyLong(), anyInt(), eq(false), eq(PowerWhitelistManager.REASON_VPN),
+ eq("VpnManager event"));
+ }
+
@Test
public void testGetAlwaysAndOnGetLockDown() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
// Default state.
assertFalse(vpn.getAlwaysOn());
@@ -386,8 +484,8 @@
@Test
public void testLockdownChangingPackage() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
- final Range<Integer> user = PRI_USER_RANGE;
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final Range<Integer> user = PRIMARY_USER_RANGE;
final int userStart = user.getLower();
final int userStop = user.getUpper();
// Set always-on without lockdown.
@@ -397,25 +495,31 @@
assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null));
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+ new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
}));
// Switch to another app.
assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null));
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+ new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
}));
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(userStart, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+ new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
}));
}
@Test
public void testLockdownAllowlist() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
- final Range<Integer> user = PRI_USER_RANGE;
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final Range<Integer> user = PRIMARY_USER_RANGE;
final int userStart = user.getLower();
final int userStop = user.getUpper();
// Set always-on with lockdown and allow app PKGS[2] from lockdown.
@@ -423,17 +527,25 @@
PKGS[1], true, Collections.singletonList(PKGS[2])));
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop)
+ new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[1]) - 1),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
}));
// Change allowed app list to PKGS[3].
assertTrue(vpn.setAlwaysOnPackage(
PKGS[1], true, Collections.singletonList(PKGS[3])));
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop)
+ new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
}));
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+ new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
}));
// Change the VPN app.
@@ -441,32 +553,52 @@
PKGS[0], true, Collections.singletonList(PKGS[3])));
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1)
+ new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1),
+ new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1),
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
}));
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(userStart, userStart + PKG_UIDS[0] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1)
+ new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
+ new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1))
}));
// Remove the list of allowed packages.
assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null));
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop)
+ new UidRangeParcel(userStart + PKG_UIDS[3] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[3] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[3] + 1), userStop)
}));
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStop),
+ new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
}));
// Add the list of allowed packages.
assertTrue(vpn.setAlwaysOnPackage(
PKGS[0], true, Collections.singletonList(PKGS[1])));
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
- new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStop)
+ new UidRangeParcel(userStart + PKG_UIDS[0] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1), userStop),
}));
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+ new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
}));
// Try allowing a package with a comma, should be rejected.
@@ -479,19 +611,27 @@
PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app")));
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop)
+ new UidRangeParcel(userStart + PKG_UIDS[1] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[1] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[1] + 1), userStop)
}));
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[2] - 1),
- new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop)
+ new UidRangeParcel(userStart + PKG_UIDS[2] + 1,
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[0] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[0] + 1),
+ Process.toSdkSandboxUid(userStart + PKG_UIDS[2] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(userStart + PKG_UIDS[2] + 1), userStop)
}));
}
@Test
public void testLockdownRuleRepeatability() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
- new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())};
+ new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
// Given legacy lockdown is already enabled,
vpn.setLockdown(true);
verify(mConnectivityManager, times(1)).setRequireVpnForUids(true,
@@ -522,13 +662,16 @@
@Test
public void testLockdownRuleReversibility() throws Exception {
doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
final UidRangeParcel[] entireUser = {
- new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())
+ new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())
};
final UidRangeParcel[] exceptPkg0 = {
new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1),
- new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1, entireUser[0].stop)
+ new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1,
+ Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] - 1)),
+ new UidRangeParcel(Process.toSdkSandboxUid(entireUser[0].start + PKG_UIDS[0] + 1),
+ entireUser[0].stop),
};
final InOrder order = inOrder(mConnectivityManager);
@@ -571,17 +714,17 @@
@Test
public void testIsAlwaysOnPackageSupported() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
ApplicationInfo appInfo = new ApplicationInfo();
- when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(primaryUser.id)))
+ when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(PRIMARY_USER.id)))
.thenReturn(appInfo);
ServiceInfo svcInfo = new ServiceInfo();
ResolveInfo resInfo = new ResolveInfo();
resInfo.serviceInfo = svcInfo;
when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA),
- eq(primaryUser.id)))
+ eq(PRIMARY_USER.id)))
.thenReturn(Collections.singletonList(resInfo));
// null package name should return false
@@ -605,9 +748,9 @@
@Test
public void testNotificationShownForAlwaysOnApp() throws Exception {
- final UserHandle userHandle = UserHandle.of(primaryUser.id);
- final Vpn vpn = createVpn(primaryUser.id);
- setMockedUsers(primaryUser);
+ final UserHandle userHandle = UserHandle.of(PRIMARY_USER.id);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ setMockedUsers(PRIMARY_USER);
final InOrder order = inOrder(mNotificationManager);
@@ -640,15 +783,15 @@
*/
@Test
public void testGetProfileNameForPackage() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
- setMockedUsers(primaryUser);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ setMockedUsers(PRIMARY_USER);
- final String expected = Credentials.PLATFORM_VPN + primaryUser.id + "_" + TEST_VPN_PKG;
+ final String expected = Credentials.PLATFORM_VPN + PRIMARY_USER.id + "_" + TEST_VPN_PKG;
assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG));
}
private Vpn createVpnAndSetupUidChecks(String... grantedOps) throws Exception {
- return createVpnAndSetupUidChecks(primaryUser, grantedOps);
+ return createVpnAndSetupUidChecks(PRIMARY_USER, grantedOps);
}
private Vpn createVpnAndSetupUidChecks(UserInfo user, String... grantedOps) throws Exception {
@@ -696,6 +839,118 @@
}
}
+ private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
+ final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+ .thenReturn(mVpnProfile.encode());
+ when(mVpnProfileStore.get(PRIMARY_USER_APP_EXCLUDE_KEY))
+ .thenReturn(HexDump.hexStringToByteArray(PKGS_BYTES));
+
+ vpn.startVpnProfile(TEST_VPN_PKG);
+ verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
+ vpn.mNetworkAgent = mMockNetworkAgent;
+ return vpn;
+ }
+
+ @Test
+ public void testSetAndGetAppExclusionList() throws Exception {
+ final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+ verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any());
+ vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
+ verify(mVpnProfileStore)
+ .put(eq(PRIMARY_USER_APP_EXCLUDE_KEY),
+ eq(HexDump.hexStringToByteArray(PKGS_BYTES)));
+ assertEquals(vpn.createUserAndRestrictedProfilesRanges(
+ PRIMARY_USER.id, null, Arrays.asList(PKGS)),
+ vpn.mNetworkCapabilities.getUids());
+ assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+ }
+
+ @Test
+ public void testRefreshPlatformVpnAppExclusionList_updatesExcludedUids() throws Exception {
+ final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+ vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
+ verify(mMockNetworkAgent).sendNetworkCapabilities(any());
+ assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+
+ reset(mMockNetworkAgent);
+
+ // Remove one of the package
+ List<Integer> newExcludedUids = toList(PKG_UIDS);
+ newExcludedUids.remove((Integer) PKG_UIDS[0]);
+ sPackages.remove(PKGS[0]);
+ vpn.refreshPlatformVpnAppExclusionList();
+
+ // List in keystore is not changed, but UID for the removed packages is no longer exempted.
+ assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+ assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+ vpn.mNetworkCapabilities.getUids());
+ ArgumentCaptor<NetworkCapabilities> ncCaptor =
+ ArgumentCaptor.forClass(NetworkCapabilities.class);
+ verify(mMockNetworkAgent).sendNetworkCapabilities(ncCaptor.capture());
+ assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+ ncCaptor.getValue().getUids());
+
+ reset(mMockNetworkAgent);
+
+ // Add the package back
+ newExcludedUids.add(PKG_UIDS[0]);
+ sPackages.put(PKGS[0], PKG_UIDS[0]);
+ vpn.refreshPlatformVpnAppExclusionList();
+
+ // List in keystore is not changed and the uid list should be updated in the net cap.
+ assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
+ assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+ vpn.mNetworkCapabilities.getUids());
+ verify(mMockNetworkAgent).sendNetworkCapabilities(ncCaptor.capture());
+ assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+ ncCaptor.getValue().getUids());
+ }
+
+ private Set<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedList) {
+ final SortedSet<Integer> list = new TreeSet<>();
+
+ final int userBase = userId * UserHandle.PER_USER_RANGE;
+ for (int uid : excludedList) {
+ final int applicationUid = UserHandle.getUid(userId, uid);
+ list.add(applicationUid);
+ list.add(Process.toSdkSandboxUid(applicationUid)); // Add Sdk Sandbox UID
+ }
+
+ final int minUid = userBase;
+ final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
+ final Set<Range<Integer>> ranges = new ArraySet<>();
+
+ // Iterate the list to create the ranges between each uid.
+ int start = minUid;
+ for (int uid : list) {
+ if (uid == start) {
+ start++;
+ } else {
+ ranges.add(new Range<>(start, uid - 1));
+ start = uid + 1;
+ }
+ }
+
+ // Create the range between last uid and max uid.
+ if (start <= maxUid) {
+ ranges.add(new Range<>(start, maxUid));
+ }
+
+ return ranges;
+ }
+
+ @Test
+ public void testSetAndGetAppExclusionListRestrictedUser() throws Exception {
+ final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+ // Mock it to restricted profile
+ when(mUserManager.getUserInfo(anyInt())).thenReturn(RESTRICTED_PROFILE_A);
+ // Restricted users cannot configure VPNs
+ assertThrows(SecurityException.class,
+ () -> vpn.setAppExclusionList(TEST_VPN_PKG, new ArrayList<>()));
+ assertThrows(SecurityException.class, () -> vpn.getAppExclusionList(TEST_VPN_PKG));
+ }
+
@Test
public void testProvisionVpnProfilePreconsented() throws Exception {
final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
@@ -739,7 +994,7 @@
public void testProvisionVpnProfileRestrictedUser() throws Exception {
final Vpn vpn =
createVpnAndSetupUidChecks(
- restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
try {
vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile);
@@ -762,7 +1017,7 @@
public void testDeleteVpnProfileRestrictedUser() throws Exception {
final Vpn vpn =
createVpnAndSetupUidChecks(
- restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
try {
vpn.deleteVpnProfile(TEST_VPN_PKG);
@@ -783,6 +1038,30 @@
verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
}
+ private void verifyPlatformVpnIsActivated(String packageName) {
+ verify(mAppOps).noteOpNoThrow(
+ eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
+ eq(Process.myUid()),
+ eq(packageName),
+ eq(null) /* attributionTag */,
+ eq(null) /* message */);
+ verify(mAppOps).startOp(
+ eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
+ eq(Process.myUid()),
+ eq(packageName),
+ eq(null) /* attributionTag */,
+ eq(null) /* message */);
+ }
+
+ private void verifyPlatformVpnIsDeactivated(String packageName) {
+ // Add a small delay to double confirm that finishOp is only called once.
+ verify(mAppOps, after(100)).finishOp(
+ eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
+ eq(Process.myUid()),
+ eq(packageName),
+ eq(null) /* attributionTag */);
+ }
+
@Test
public void testStartVpnProfile() throws Exception {
final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
@@ -793,13 +1072,7 @@
vpn.startVpnProfile(TEST_VPN_PKG);
verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
- verify(mAppOps)
- .noteOpNoThrow(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
- eq(Process.myUid()),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
+ verifyPlatformVpnIsActivated(TEST_VPN_PKG);
}
@Test
@@ -811,7 +1084,7 @@
vpn.startVpnProfile(TEST_VPN_PKG);
- // Verify that the the ACTIVATE_VPN appop was checked, but no error was thrown.
+ // Verify that the ACTIVATE_VPN appop was checked, but no error was thrown.
verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(),
TEST_VPN_PKG, null /* attributionTag */, null /* message */);
}
@@ -867,7 +1140,7 @@
public void testStartVpnProfileRestrictedUser() throws Exception {
final Vpn vpn =
createVpnAndSetupUidChecks(
- restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
try {
vpn.startVpnProfile(TEST_VPN_PKG);
@@ -880,7 +1153,7 @@
public void testStopVpnProfileRestrictedUser() throws Exception {
final Vpn vpn =
createVpnAndSetupUidChecks(
- restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ RESTRICTED_PROFILE_A, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
try {
vpn.stopVpnProfile(TEST_VPN_PKG);
@@ -896,18 +1169,7 @@
when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
.thenReturn(mVpnProfile.encode());
vpn.startVpnProfile(TEST_VPN_PKG);
- verify(mAppOps).noteOpNoThrow(
- eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
- eq(Process.myUid()),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
- verify(mAppOps).startOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
- eq(Process.myUid()),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */,
- eq(null) /* message */);
+ verifyPlatformVpnIsActivated(TEST_VPN_PKG);
// Add a small delay to make sure that startOp is only called once.
verify(mAppOps, after(100).times(1)).startOp(
eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
@@ -923,12 +1185,7 @@
eq(null) /* attributionTag */,
eq(null) /* message */);
vpn.stopVpnProfile(TEST_VPN_PKG);
- // Add a small delay to double confirm that startOp is only called once.
- verify(mAppOps, after(100)).finishOp(
- eq(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER),
- eq(Process.myUid()),
- eq(TEST_VPN_PKG),
- eq(null) /* attributionTag */);
+ verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
}
@Test
@@ -964,6 +1221,169 @@
eq(null) /* message */);
}
+ private void verifyVpnManagerEvent(String sessionKey, String category, int errorClass,
+ int errorCode, VpnProfileState... profileState) {
+ final Context userContext =
+ mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
+ final ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+
+ final int verifyTimes = (profileState == null) ? 1 : profileState.length;
+ verify(userContext, times(verifyTimes)).startService(intentArgumentCaptor.capture());
+
+ for (int i = 0; i < verifyTimes; i++) {
+ final Intent intent = intentArgumentCaptor.getAllValues().get(i);
+ assertEquals(sessionKey, intent.getStringExtra(VpnManager.EXTRA_SESSION_KEY));
+ final Set<String> categories = intent.getCategories();
+ assertTrue(categories.contains(category));
+ assertEquals(errorClass,
+ intent.getIntExtra(VpnManager.EXTRA_ERROR_CLASS, -1 /* defaultValue */));
+ assertEquals(errorCode,
+ intent.getIntExtra(VpnManager.EXTRA_ERROR_CODE, -1 /* defaultValue */));
+ if (profileState != null) {
+ assertEquals(profileState[i], intent.getParcelableExtra(
+ VpnManager.EXTRA_VPN_PROFILE_STATE, VpnProfileState.class));
+ }
+ }
+ reset(userContext);
+ }
+
+ @Test
+ public void testVpnManagerEventForUserDeactivated() throws Exception {
+ assumeTrue(SdkLevel.isAtLeastT());
+ // For security reasons, Vpn#prepare() will check that oldPackage and newPackage are either
+ // null or the package of the caller. This test will call Vpn#prepare() to pretend the old
+ // VPN is replaced by a new one. But only Settings can change to some other packages, and
+ // this is checked with CONTROL_VPN so simulate holding CONTROL_VPN in order to pass the
+ // security checks.
+ doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
+ final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+ .thenReturn(mVpnProfile.encode());
+
+ // Test the case that the user deactivates the vpn in vpn app.
+ final String sessionKey1 = vpn.startVpnProfile(TEST_VPN_PKG);
+ verifyPlatformVpnIsActivated(TEST_VPN_PKG);
+ vpn.stopVpnProfile(TEST_VPN_PKG);
+ verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
+ verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
+ reset(mDeviceIdleInternal);
+ // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
+ // errorCode won't be set.
+ verifyVpnManagerEvent(sessionKey1, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
+ -1 /* errorClass */, -1 /* errorCode */, null /* profileState */);
+ reset(mAppOps);
+
+ // Test the case that the user chooses another vpn and the original one is replaced.
+ final String sessionKey2 = vpn.startVpnProfile(TEST_VPN_PKG);
+ verifyPlatformVpnIsActivated(TEST_VPN_PKG);
+ vpn.prepare(TEST_VPN_PKG, "com.new.vpn" /* newPackage */, TYPE_VPN_PLATFORM);
+ verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
+ verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
+ reset(mDeviceIdleInternal);
+ // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
+ // errorCode won't be set.
+ verifyVpnManagerEvent(sessionKey2, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
+ -1 /* errorClass */, -1 /* errorCode */, null /* profileState */);
+ }
+
+ @Test
+ public void testVpnManagerEventForAlwaysOnChanged() throws Exception {
+ assumeTrue(SdkLevel.isAtLeastT());
+ // Calling setAlwaysOnPackage() needs to hold CONTROL_VPN.
+ doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ // Enable VPN always-on for PKGS[1].
+ assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
+ null /* lockdownAllowlist */));
+ verifyPowerSaveTempWhitelistApp(PKGS[1]);
+ reset(mDeviceIdleInternal);
+ verifyVpnManagerEvent(null /* sessionKey */,
+ VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+ -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+ null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
+
+ // Enable VPN lockdown for PKGS[1].
+ assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true /* lockdown */,
+ null /* lockdownAllowlist */));
+ verifyPowerSaveTempWhitelistApp(PKGS[1]);
+ reset(mDeviceIdleInternal);
+ verifyVpnManagerEvent(null /* sessionKey */,
+ VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+ -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+ null /* sessionKey */, true /* alwaysOn */, true /* lockdown */));
+
+ // Disable VPN lockdown for PKGS[1].
+ assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
+ null /* lockdownAllowlist */));
+ verifyPowerSaveTempWhitelistApp(PKGS[1]);
+ reset(mDeviceIdleInternal);
+ verifyVpnManagerEvent(null /* sessionKey */,
+ VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+ -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+ null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
+
+ // Disable VPN always-on.
+ assertTrue(vpn.setAlwaysOnPackage(null, false /* lockdown */,
+ null /* lockdownAllowlist */));
+ verifyPowerSaveTempWhitelistApp(PKGS[1]);
+ reset(mDeviceIdleInternal);
+ verifyVpnManagerEvent(null /* sessionKey */,
+ VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+ -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+ null /* sessionKey */, false /* alwaysOn */, false /* lockdown */));
+
+ // Enable VPN always-on for PKGS[1] again.
+ assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false /* lockdown */,
+ null /* lockdownAllowlist */));
+ verifyPowerSaveTempWhitelistApp(PKGS[1]);
+ reset(mDeviceIdleInternal);
+ verifyVpnManagerEvent(null /* sessionKey */,
+ VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+ -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+ null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
+
+ // Enable VPN always-on for PKGS[2].
+ assertTrue(vpn.setAlwaysOnPackage(PKGS[2], false /* lockdown */,
+ null /* lockdownAllowlist */));
+ verifyPowerSaveTempWhitelistApp(PKGS[2]);
+ reset(mDeviceIdleInternal);
+ // PKGS[1] is replaced with PKGS[2].
+ // Pass 2 VpnProfileState objects to verifyVpnManagerEvent(), the first one is sent to
+ // PKGS[1] to notify PKGS[1] that the VPN always-on is disabled, the second one is sent to
+ // PKGS[2] to notify PKGS[2] that the VPN always-on is enabled.
+ verifyVpnManagerEvent(null /* sessionKey */,
+ VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+ -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+ null /* sessionKey */, false /* alwaysOn */, false /* lockdown */),
+ new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+ null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
+ }
+
+ @Test
+ public void testReconnectVpnManagerVpnWithAlwaysOnEnabled() throws Exception {
+ final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+ .thenReturn(mVpnProfile.encode());
+ vpn.startVpnProfile(TEST_VPN_PKG);
+ verifyPlatformVpnIsActivated(TEST_VPN_PKG);
+
+ // Enable VPN always-on for TEST_VPN_PKG.
+ assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, false /* lockdown */,
+ null /* lockdownAllowlist */));
+
+ // Reset to verify next startVpnProfile.
+ reset(mAppOps);
+
+ vpn.stopVpnProfile(TEST_VPN_PKG);
+
+ // Reconnect the vpn with different package will cause exception.
+ assertThrows(SecurityException.class, () -> vpn.startVpnProfile(PKGS[0]));
+
+ // Reconnect the vpn again with the vpn always on package w/o exception.
+ vpn.startVpnProfile(TEST_VPN_PKG);
+ verifyPlatformVpnIsActivated(TEST_VPN_PKG);
+ }
+
@Test
public void testSetPackageAuthorizationVpnService() throws Exception {
final Vpn vpn = createVpnAndSetupUidChecks();
@@ -981,7 +1401,7 @@
public void testSetPackageAuthorizationPlatformVpn() throws Exception {
final Vpn vpn = createVpnAndSetupUidChecks();
- assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_PLATFORM));
+ assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, TYPE_VPN_PLATFORM));
verify(mAppOps)
.setMode(
eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN),
@@ -1031,15 +1451,16 @@
config -> Arrays.asList(config.flags).contains(flag)));
}
- @Test
- public void testStartPlatformVpnAuthenticationFailed() throws Exception {
+ private void doTestPlatformVpnWithException(IkeException exception,
+ String category, int errorType, int errorCode) throws Exception {
final ArgumentCaptor<IkeSessionCallback> captor =
ArgumentCaptor.forClass(IkeSessionCallback.class);
- final IkeProtocolException exception = mock(IkeProtocolException.class);
- when(exception.getErrorType())
- .thenReturn(IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED);
- final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), (mVpnProfile));
+ final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+ .thenReturn(mVpnProfile.encode());
+
+ final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
final NetworkCallback cb = triggerOnAvailableAndGetCallback();
verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
@@ -1048,18 +1469,116 @@
// state
verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
.createIkeSession(any(), any(), any(), any(), captor.capture(), any());
+ reset(mIkev2SessionCreator);
final IkeSessionCallback ikeCb = captor.getValue();
- ikeCb.onClosedExceptionally(exception);
+ ikeCb.onClosedWithException(exception);
- verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb));
- assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state);
+ verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
+ reset(mDeviceIdleInternal);
+ verifyVpnManagerEvent(sessionKey, category, errorType, errorCode, null /* profileState */);
+ if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) {
+ verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
+ .unregisterNetworkCallback(eq(cb));
+ } else if (errorType == VpnManager.ERROR_CLASS_RECOVERABLE) {
+ int retryIndex = 0;
+ final IkeSessionCallback ikeCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
+
+ ikeCb2.onClosedWithException(exception);
+ verifyRetryAndGetNewIkeCb(retryIndex++);
+ }
+ }
+
+ private IkeSessionCallback verifyRetryAndGetNewIkeCb(int retryIndex) {
+ final ArgumentCaptor<Runnable> runnableCaptor =
+ ArgumentCaptor.forClass(Runnable.class);
+ final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
+ ArgumentCaptor.forClass(IkeSessionCallback.class);
+
+ // Verify retry is scheduled
+ final long expectedDelay = mTestDeps.getNextRetryDelaySeconds(retryIndex);
+ verify(mExecutor).schedule(runnableCaptor.capture(), eq(expectedDelay), any());
+
+ // Mock the event of firing the retry task
+ runnableCaptor.getValue().run();
+
+ verify(mIkev2SessionCreator)
+ .createIkeSession(any(), any(), any(), any(), ikeCbCaptor.capture(), any());
+
+ // Forget the mIkev2SessionCreator#createIkeSession call and mExecutor#schedule call
+ // for the next retry verification
+ resetIkev2SessionCreator(mIkeSessionWrapper);
+ resetExecutor(mScheduledFuture);
+
+ return ikeCbCaptor.getValue();
+ }
+
+ @Test
+ public void testStartPlatformVpnAuthenticationFailed() throws Exception {
+ final IkeProtocolException exception = mock(IkeProtocolException.class);
+ final int errorCode = IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED;
+ when(exception.getErrorType()).thenReturn(errorCode);
+ doTestPlatformVpnWithException(exception,
+ VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_NOT_RECOVERABLE,
+ errorCode);
+ }
+
+ @Test
+ public void testStartPlatformVpnFailedWithRecoverableError() throws Exception {
+ final IkeProtocolException exception = mock(IkeProtocolException.class);
+ final int errorCode = IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE;
+ when(exception.getErrorType()).thenReturn(errorCode);
+ doTestPlatformVpnWithException(exception,
+ VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, errorCode);
+ }
+
+ @Test
+ public void testStartPlatformVpnFailedWithUnknownHostException() throws Exception {
+ final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
+ final UnknownHostException unknownHostException = new UnknownHostException();
+ final int errorCode = VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST;
+ when(exception.getCause()).thenReturn(unknownHostException);
+ doTestPlatformVpnWithException(exception,
+ VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
+ errorCode);
+ }
+
+ @Test
+ public void testStartPlatformVpnFailedWithIkeTimeoutException() throws Exception {
+ final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
+ final IkeTimeoutException ikeTimeoutException =
+ new IkeTimeoutException("IkeTimeoutException");
+ final int errorCode = VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT;
+ when(exception.getCause()).thenReturn(ikeTimeoutException);
+ doTestPlatformVpnWithException(exception,
+ VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
+ errorCode);
+ }
+
+ @Test
+ public void testStartPlatformVpnFailedWithIkeNetworkLostException() throws Exception {
+ final IkeNetworkLostException exception = new IkeNetworkLostException(
+ new Network(100));
+ doTestPlatformVpnWithException(exception,
+ VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
+ VpnManager.ERROR_CODE_NETWORK_LOST);
+ }
+
+ @Test
+ public void testStartPlatformVpnFailedWithIOException() throws Exception {
+ final IkeNonProtocolException exception = mock(IkeNonProtocolException.class);
+ final IOException ioException = new IOException();
+ final int errorCode = VpnManager.ERROR_CODE_NETWORK_IO;
+ when(exception.getCause()).thenReturn(ioException);
+ doTestPlatformVpnWithException(exception,
+ VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
+ errorCode);
}
@Test
public void testStartPlatformVpnIllegalArgumentExceptionInSetup() throws Exception {
when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
.thenThrow(new IllegalArgumentException());
- final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), mVpnProfile);
+ final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), mVpnProfile);
final NetworkCallback cb = triggerOnAvailableAndGetCallback();
verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
@@ -1079,18 +1598,18 @@
eq(AppOpsManager.MODE_ALLOWED));
verify(mSystemServices).settingsSecurePutStringForUser(
- eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(primaryUser.id));
+ eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(PRIMARY_USER.id));
verify(mSystemServices).settingsSecurePutIntForUser(
eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0),
- eq(primaryUser.id));
+ eq(PRIMARY_USER.id));
verify(mSystemServices).settingsSecurePutStringForUser(
- eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(primaryUser.id));
+ eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(PRIMARY_USER.id));
}
@Test
public void testSetAndStartAlwaysOnVpn() throws Exception {
- final Vpn vpn = createVpn(primaryUser.id);
- setMockedUsers(primaryUser);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ setMockedUsers(PRIMARY_USER);
// UID checks must return a different UID; otherwise it'll be treated as already prepared.
final int uid = Process.myUid() + 1;
@@ -1107,7 +1626,7 @@
}
private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
- setMockedUsers(primaryUser);
+ setMockedUsers(PRIMARY_USER);
// Dummy egress interface
final LinkProperties lp = new LinkProperties();
@@ -1121,11 +1640,266 @@
return vpn;
}
+ private IkeSessionConnectionInfo createIkeConnectInfo() {
+ return new IkeSessionConnectionInfo(TEST_VPN_CLIENT_IP, TEST_VPN_SERVER_IP, TEST_NETWORK);
+ }
+
+ private IkeSessionConnectionInfo createIkeConnectInfo_2() {
+ return new IkeSessionConnectionInfo(
+ TEST_VPN_CLIENT_IP_2, TEST_VPN_SERVER_IP_2, TEST_NETWORK_2);
+ }
+
+ private IkeSessionConfiguration createIkeConfig(
+ IkeSessionConnectionInfo ikeConnectInfo, boolean isMobikeEnabled) {
+ final IkeSessionConfiguration.Builder builder =
+ new IkeSessionConfiguration.Builder(ikeConnectInfo);
+
+ if (isMobikeEnabled) {
+ builder.addIkeExtension(EXTENSION_TYPE_MOBIKE);
+ }
+
+ return builder.build();
+ }
+
+ private ChildSessionConfiguration createChildConfig() {
+ return new ChildSessionConfiguration.Builder(Arrays.asList(IN_TS), Arrays.asList(OUT_TS))
+ .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN))
+ .addInternalDnsServer(TEST_VPN_INTERNAL_DNS)
+ .build();
+ }
+
+ private IpSecTransform createIpSecTransform() {
+ return new IpSecTransform(mContext, new IpSecConfig());
+ }
+
+ private void verifyApplyTunnelModeTransforms(int expectedTimes) throws Exception {
+ verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
+ eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_IN),
+ anyInt(), anyString());
+ verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
+ eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_OUT),
+ anyInt(), anyString());
+ }
+
+ private Pair<IkeSessionCallback, ChildSessionCallback> verifyCreateIkeAndCaptureCbs()
+ throws Exception {
+ final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
+ ArgumentCaptor.forClass(IkeSessionCallback.class);
+ final ArgumentCaptor<ChildSessionCallback> childCbCaptor =
+ ArgumentCaptor.forClass(ChildSessionCallback.class);
+
+ verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)).createIkeSession(
+ any(), any(), any(), any(), ikeCbCaptor.capture(), childCbCaptor.capture());
+
+ return new Pair<>(ikeCbCaptor.getValue(), childCbCaptor.getValue());
+ }
+
+ private static class PlatformVpnSnapshot {
+ public final Vpn vpn;
+ public final NetworkCallback nwCb;
+ public final IkeSessionCallback ikeCb;
+ public final ChildSessionCallback childCb;
+
+ PlatformVpnSnapshot(Vpn vpn, NetworkCallback nwCb,
+ IkeSessionCallback ikeCb, ChildSessionCallback childCb) {
+ this.vpn = vpn;
+ this.nwCb = nwCb;
+ this.ikeCb = ikeCb;
+ this.childCb = childCb;
+ }
+ }
+
+ private PlatformVpnSnapshot verifySetupPlatformVpn(IkeSessionConfiguration ikeConfig)
+ throws Exception {
+ doReturn(mMockNetworkAgent).when(mTestDeps)
+ .newNetworkAgent(
+ any(), any(), anyString(), any(), any(), any(), any(), any());
+
+ final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+ .thenReturn(mVpnProfile.encode());
+
+ vpn.startVpnProfile(TEST_VPN_PKG);
+ final NetworkCallback nwCb = triggerOnAvailableAndGetCallback();
+
+ // Mock the setup procedure by firing callbacks
+ final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
+ verifyCreateIkeAndCaptureCbs();
+ final IkeSessionCallback ikeCb = cbPair.first;
+ final ChildSessionCallback childCb = cbPair.second;
+
+ ikeCb.onOpened(ikeConfig);
+ childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
+ childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
+ childCb.onOpened(createChildConfig());
+
+ // Verification VPN setup
+ verifyApplyTunnelModeTransforms(1);
+
+ ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
+ ArgumentCaptor<NetworkCapabilities> ncCaptor =
+ ArgumentCaptor.forClass(NetworkCapabilities.class);
+ verify(mTestDeps).newNetworkAgent(
+ any(), any(), anyString(), ncCaptor.capture(), lpCaptor.capture(),
+ any(), any(), any());
+
+ // Check LinkProperties
+ final LinkProperties lp = lpCaptor.getValue();
+ final List<RouteInfo> expectedRoutes = Arrays.asList(
+ new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null /*gateway*/,
+ TEST_IFACE_NAME, RouteInfo.RTN_UNICAST),
+ new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /*gateway*/,
+ TEST_IFACE_NAME, RTN_UNREACHABLE));
+ assertEquals(expectedRoutes, lp.getRoutes());
+
+ // Check internal addresses
+ final List<LinkAddress> expectedAddresses =
+ Arrays.asList(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN));
+ assertEquals(expectedAddresses, lp.getLinkAddresses());
+
+ // Check internal DNS
+ assertEquals(Arrays.asList(TEST_VPN_INTERNAL_DNS), lp.getDnsServers());
+
+ // Check NetworkCapabilities
+ assertEquals(Arrays.asList(TEST_NETWORK), ncCaptor.getValue().getUnderlyingNetworks());
+
+ return new PlatformVpnSnapshot(vpn, nwCb, ikeCb, childCb);
+ }
+
@Test
public void testStartPlatformVpn() throws Exception {
- startLegacyVpn(createVpn(primaryUser.id), mVpnProfile);
- // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in
- // a subsequent patch.
+ final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+ createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
+ vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+ }
+
+ @Test
+ public void testStartPlatformVpnMobility_mobikeEnabled() throws Exception {
+ final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+ createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
+
+ // Mock network loss and verify a cleanup task is scheduled
+ vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+ verify(mExecutor).schedule(any(Runnable.class), anyLong(), any());
+
+ // Mock new network comes up and the cleanup task is cancelled
+ vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
+ verify(mScheduledFuture).cancel(anyBoolean());
+
+ // Verify MOBIKE is triggered
+ verify(mIkeSessionWrapper).setNetwork(TEST_NETWORK_2);
+
+ // Mock the MOBIKE procedure
+ vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
+ vpnSnapShot.childCb.onIpSecTransformsMigrated(
+ createIpSecTransform(), createIpSecTransform());
+
+ verify(mIpSecService).setNetworkForTunnelInterface(
+ eq(TEST_TUNNEL_RESOURCE_ID), eq(TEST_NETWORK_2), anyString());
+
+ // Expect 2 times: one for initial setup and one for MOBIKE
+ verifyApplyTunnelModeTransforms(2);
+
+ // Verify mNetworkCapabilities and mNetworkAgent are updated
+ assertEquals(
+ Collections.singletonList(TEST_NETWORK_2),
+ vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
+ verify(mMockNetworkAgent)
+ .setUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
+
+ vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+ }
+
+ @Test
+ public void testStartPlatformVpnReestablishes_mobikeDisabled() throws Exception {
+ final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+ createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+ // Forget the first IKE creation to be prepared to capture callbacks of the second
+ // IKE session
+ resetIkev2SessionCreator(mock(Vpn.IkeSessionWrapper.class));
+
+ // Mock network switch
+ vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+ vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
+
+ // Verify the old IKE Session is killed
+ verify(mIkeSessionWrapper).kill();
+
+ // Capture callbacks of the new IKE Session
+ final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
+ verifyCreateIkeAndCaptureCbs();
+ final IkeSessionCallback ikeCb = cbPair.first;
+ final ChildSessionCallback childCb = cbPair.second;
+
+ // Mock the IKE Session setup
+ ikeCb.onOpened(createIkeConfig(createIkeConnectInfo_2(), false /* isMobikeEnabled */));
+
+ childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
+ childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
+ childCb.onOpened(createChildConfig());
+
+ // Expect 2 times since there have been two Session setups
+ verifyApplyTunnelModeTransforms(2);
+
+ // Verify mNetworkCapabilities and mNetworkAgent are updated
+ assertEquals(
+ Collections.singletonList(TEST_NETWORK_2),
+ vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
+ verify(mMockNetworkAgent)
+ .setUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
+
+ vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+ }
+
+ private void verifyHandlingNetworkLoss() throws Exception {
+ final ArgumentCaptor<LinkProperties> lpCaptor =
+ ArgumentCaptor.forClass(LinkProperties.class);
+ verify(mMockNetworkAgent).sendLinkProperties(lpCaptor.capture());
+ final LinkProperties lp = lpCaptor.getValue();
+
+ assertNull(lp.getInterfaceName());
+ final List<RouteInfo> expectedRoutes = Arrays.asList(
+ new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null /*gateway*/,
+ null /*iface*/, RTN_UNREACHABLE),
+ new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /*gateway*/,
+ null /*iface*/, RTN_UNREACHABLE));
+ assertEquals(expectedRoutes, lp.getRoutes());
+ }
+
+ @Test
+ public void testStartPlatformVpnHandlesNetworkLoss_mobikeEnabled() throws Exception {
+ final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+ createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+ // Forget the #sendLinkProperties during first setup.
+ reset(mMockNetworkAgent);
+
+ final ArgumentCaptor<Runnable> runnableCaptor =
+ ArgumentCaptor.forClass(Runnable.class);
+
+ // Mock network loss
+ vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+
+ // Mock the grace period expires
+ verify(mExecutor).schedule(runnableCaptor.capture(), anyLong(), any());
+ runnableCaptor.getValue().run();
+
+ verifyHandlingNetworkLoss();
+ }
+
+ @Test
+ public void testStartPlatformVpnHandlesNetworkLoss_mobikeDisabled() throws Exception {
+ final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+ createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+ // Forget the #sendLinkProperties during first setup.
+ reset(mMockNetworkAgent);
+
+ // Mock network loss
+ vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+
+ verifyHandlingNetworkLoss();
}
@Test
@@ -1138,6 +1912,16 @@
startRacoon("hostname", "5.6.7.8"); // address returned by deps.resolve
}
+ @Test
+ public void testStartPptp() throws Exception {
+ startPptp(true /* useMppe */);
+ }
+
+ @Test
+ public void testStartPptp_NoMppe() throws Exception {
+ startPptp(false /* useMppe */);
+ }
+
private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
assertNotNull(nc);
VpnTransportInfo ti = (VpnTransportInfo) nc.getTransportInfo();
@@ -1145,6 +1929,48 @@
assertEquals(type, ti.getType());
}
+ private void startPptp(boolean useMppe) throws Exception {
+ final VpnProfile profile = new VpnProfile("testProfile" /* key */);
+ profile.type = VpnProfile.TYPE_PPTP;
+ profile.name = "testProfileName";
+ profile.username = "userName";
+ profile.password = "thePassword";
+ profile.server = "192.0.2.123";
+ profile.mppe = useMppe;
+
+ doReturn(new Network[] { new Network(101) }).when(mConnectivityManager).getAllNetworks();
+ doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(any(), any(),
+ any(), any(), any(), any(), anyInt());
+
+ final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
+ final TestDeps deps = (TestDeps) vpn.mDeps;
+
+ testAndCleanup(() -> {
+ final String[] mtpdArgs = deps.mtpdArgs.get(10, TimeUnit.SECONDS);
+ final String[] argsPrefix = new String[]{
+ EGRESS_IFACE, "pptp", profile.server, "1723", "name", profile.username,
+ "password", profile.password, "linkname", "vpn", "refuse-eap", "nodefaultroute",
+ "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270"
+ };
+ assertArrayEquals(argsPrefix, Arrays.copyOf(mtpdArgs, argsPrefix.length));
+ if (useMppe) {
+ assertEquals(argsPrefix.length + 2, mtpdArgs.length);
+ assertEquals("+mppe", mtpdArgs[argsPrefix.length]);
+ assertEquals("-pap", mtpdArgs[argsPrefix.length + 1]);
+ } else {
+ assertEquals(argsPrefix.length + 1, mtpdArgs.length);
+ assertEquals("nomppe", mtpdArgs[argsPrefix.length]);
+ }
+
+ verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
+ any(), any(), any(), any(), anyInt());
+ }, () -> { // Cleanup
+ vpn.mVpnRunner.exitVpnRunner();
+ deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
+ vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
+ });
+ }
+
public void startRacoon(final String serverAddr, final String expectedAddr)
throws Exception {
final ConditionVariable legacyRunnerReady = new ConditionVariable();
@@ -1167,7 +1993,7 @@
legacyRunnerReady.open();
return new Network(102);
});
- final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), profile);
+ final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
final TestDeps deps = (TestDeps) vpn.mDeps;
try {
// udppsk and 1701 are the values for TYPE_L2TP_IPSEC_PSK
@@ -1208,7 +2034,8 @@
}
}
- private static final class TestDeps extends Vpn.Dependencies {
+ // Make it public and un-final so as to spy it
+ public class TestDeps extends Vpn.Dependencies {
public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
public final CompletableFuture<String[]> mtpdArgs = new CompletableFuture();
public final File mStateFile;
@@ -1337,6 +2164,22 @@
@Override
public void setBlocking(FileDescriptor fd, boolean blocking) {}
+
+ @Override
+ public DeviceIdleInternal getDeviceIdleInternal() {
+ return mDeviceIdleInternal;
+ }
+
+ @Override
+ public long getNextRetryDelaySeconds(int retryCount) {
+ // Simply return retryCount as the delay seconds for retrying.
+ return retryCount;
+ }
+
+ @Override
+ public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor() {
+ return mExecutor;
+ }
}
/**
@@ -1348,7 +2191,7 @@
when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt()))
.thenReturn(asUserContext);
final TestLooper testLooper = new TestLooper();
- final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, new TestDeps(), mNetService,
+ final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, mTestDeps, mNetService,
mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator);
verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat(
provider -> provider.getName().contains("VpnNetworkProvider")
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java
new file mode 100644
index 0000000..f84e2d8
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/ConnectivityMonitorWithConnectivityManagerTests.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkRequest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link ConnectivityMonitor}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class ConnectivityMonitorWithConnectivityManagerTests {
+ @Mock private Context mContext;
+ @Mock private ConnectivityMonitor.Listener mockListener;
+ @Mock private ConnectivityManager mConnectivityManager;
+
+ private ConnectivityMonitorWithConnectivityManager monitor;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ doReturn(mConnectivityManager).when(mContext)
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ monitor = new ConnectivityMonitorWithConnectivityManager(mContext, mockListener);
+ }
+
+ @Test
+ public void testInitialState_shouldNotRegisterNetworkCallback() {
+ verifyNetworkCallbackRegistered(0 /* time */);
+ verifyNetworkCallbackUnregistered(0 /* time */);
+ }
+
+ @Test
+ public void testStartDiscovery_shouldRegisterNetworkCallback() {
+ monitor.startWatchingConnectivityChanges();
+
+ verifyNetworkCallbackRegistered(1 /* time */);
+ verifyNetworkCallbackUnregistered(0 /* time */);
+ }
+
+ @Test
+ public void testStartDiscoveryTwice_shouldRegisterOneNetworkCallback() {
+ monitor.startWatchingConnectivityChanges();
+ monitor.startWatchingConnectivityChanges();
+
+ verifyNetworkCallbackRegistered(1 /* time */);
+ verifyNetworkCallbackUnregistered(0 /* time */);
+ }
+
+ @Test
+ public void testStopDiscovery_shouldUnregisterNetworkCallback() {
+ monitor.startWatchingConnectivityChanges();
+ monitor.stopWatchingConnectivityChanges();
+
+ verifyNetworkCallbackRegistered(1 /* time */);
+ verifyNetworkCallbackUnregistered(1 /* time */);
+ }
+
+ @Test
+ public void testStopDiscoveryTwice_shouldUnregisterNetworkCallback() {
+ monitor.startWatchingConnectivityChanges();
+ monitor.stopWatchingConnectivityChanges();
+
+ verifyNetworkCallbackRegistered(1 /* time */);
+ verifyNetworkCallbackUnregistered(1 /* time */);
+ }
+
+ @Test
+ public void testIntentFired_shouldNotifyListener() {
+ InOrder inOrder = inOrder(mockListener);
+ monitor.startWatchingConnectivityChanges();
+
+ final ArgumentCaptor<NetworkCallback> callbackCaptor =
+ ArgumentCaptor.forClass(NetworkCallback.class);
+ verify(mConnectivityManager, times(1)).registerNetworkCallback(
+ any(NetworkRequest.class), callbackCaptor.capture());
+
+ final NetworkCallback callback = callbackCaptor.getValue();
+ final Network testNetwork = new Network(1 /* netId */);
+
+ // Simulate network available.
+ callback.onAvailable(testNetwork);
+ inOrder.verify(mockListener).onConnectivityChanged();
+
+ // Simulate network lost.
+ callback.onLost(testNetwork);
+ inOrder.verify(mockListener).onConnectivityChanged();
+
+ // Simulate network unavailable.
+ callback.onUnavailable();
+ inOrder.verify(mockListener).onConnectivityChanged();
+ }
+
+ private void verifyNetworkCallbackRegistered(int time) {
+ verify(mConnectivityManager, times(time)).registerNetworkCallback(
+ any(NetworkRequest.class), any(NetworkCallback.class));
+ }
+
+ private void verifyNetworkCallbackUnregistered(int time) {
+ verify(mConnectivityManager, times(time))
+ .unregisterNetworkCallback(any(NetworkCallback.class));
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
new file mode 100644
index 0000000..3e3c3bf
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.text.TextUtils;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.util.Collections;
+
+/** Tests for {@link MdnsDiscoveryManager}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsDiscoveryManagerTests {
+
+ private static final String SERVICE_TYPE_1 = "_googlecast._tcp.local";
+ private static final String SERVICE_TYPE_2 = "_test._tcp.local";
+
+ @Mock private ExecutorProvider executorProvider;
+ @Mock private MdnsSocketClient socketClient;
+ @Mock private MdnsServiceTypeClient mockServiceTypeClientOne;
+ @Mock private MdnsServiceTypeClient mockServiceTypeClientTwo;
+
+ @Mock MdnsServiceBrowserListener mockListenerOne;
+ @Mock MdnsServiceBrowserListener mockListenerTwo;
+ private MdnsDiscoveryManager discoveryManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mockServiceTypeClientOne.getServiceTypeLabels())
+ .thenReturn(TextUtils.split(SERVICE_TYPE_1, "\\."));
+ when(mockServiceTypeClientTwo.getServiceTypeLabels())
+ .thenReturn(TextUtils.split(SERVICE_TYPE_2, "\\."));
+
+ discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient) {
+ @Override
+ MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType) {
+ if (serviceType.equals(SERVICE_TYPE_1)) {
+ return mockServiceTypeClientOne;
+ } else if (serviceType.equals(SERVICE_TYPE_2)) {
+ return mockServiceTypeClientTwo;
+ }
+ return null;
+ }
+ };
+ }
+
+ @Test
+ public void registerListener_unregisterListener() throws IOException {
+ discoveryManager.registerListener(
+ SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ verify(socketClient).startDiscovery();
+ verify(mockServiceTypeClientOne)
+ .startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+ when(mockServiceTypeClientOne.stopSendAndReceive(mockListenerOne)).thenReturn(true);
+ discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne);
+ verify(mockServiceTypeClientOne).stopSendAndReceive(mockListenerOne);
+ verify(socketClient).stopDiscovery();
+ }
+
+ @Test
+ public void registerMultipleListeners() throws IOException {
+ discoveryManager.registerListener(
+ SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ verify(socketClient).startDiscovery();
+ verify(mockServiceTypeClientOne)
+ .startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+ discoveryManager.registerListener(
+ SERVICE_TYPE_2, mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+ verify(mockServiceTypeClientTwo)
+ .startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+ }
+
+ @Test
+ public void onResponseReceived() {
+ discoveryManager.registerListener(
+ SERVICE_TYPE_1, mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ discoveryManager.registerListener(
+ SERVICE_TYPE_2, mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+ MdnsResponse responseForServiceTypeOne = createMockResponse(SERVICE_TYPE_1);
+ discoveryManager.onResponseReceived(responseForServiceTypeOne);
+ verify(mockServiceTypeClientOne).processResponse(responseForServiceTypeOne);
+
+ MdnsResponse responseForServiceTypeTwo = createMockResponse(SERVICE_TYPE_2);
+ discoveryManager.onResponseReceived(responseForServiceTypeTwo);
+ verify(mockServiceTypeClientTwo).processResponse(responseForServiceTypeTwo);
+
+ MdnsResponse responseForSubtype = createMockResponse("subtype._sub._googlecast._tcp.local");
+ discoveryManager.onResponseReceived(responseForSubtype);
+ verify(mockServiceTypeClientOne).processResponse(responseForSubtype);
+ }
+
+ private MdnsResponse createMockResponse(String serviceType) {
+ MdnsPointerRecord mockPointerRecord = mock(MdnsPointerRecord.class);
+ MdnsResponse mockResponse = mock(MdnsResponse.class);
+ when(mockResponse.getPointerRecords())
+ .thenReturn(Collections.singletonList(mockPointerRecord));
+ when(mockPointerRecord.getName()).thenReturn(TextUtils.split(serviceType, "\\."));
+ return mockResponse;
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
new file mode 100644
index 0000000..19d8a00
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.Locale;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsPacketReaderTests {
+
+ @Test
+ public void testLimits() throws IOException {
+ byte[] data = new byte[25];
+ DatagramPacket datagramPacket = new DatagramPacket(data, data.length);
+
+ // After creating a new reader, confirm that the remaining is equal to the packet length
+ // (or that there is no temporary limit).
+ MdnsPacketReader packetReader = new MdnsPacketReader(datagramPacket);
+ assertEquals(data.length, packetReader.getRemaining());
+
+ // Confirm that we can set the temporary limit to 0.
+ packetReader.setLimit(0);
+ assertEquals(0, packetReader.getRemaining());
+
+ // Confirm that we can clear the temporary limit, and restore to the length of the packet.
+ packetReader.clearLimit();
+ assertEquals(data.length, packetReader.getRemaining());
+
+ // Confirm that we can set the temporary limit to the actual length of the packet.
+ // While parsing packets, it is common to set the limit to the length of the packet.
+ packetReader.setLimit(data.length);
+ assertEquals(data.length, packetReader.getRemaining());
+
+ // Confirm that we ignore negative limits.
+ packetReader.setLimit(-10);
+ assertEquals(data.length, packetReader.getRemaining());
+
+ // Confirm that we can set the temporary limit to something less than the packet length.
+ packetReader.setLimit(data.length / 2);
+ assertEquals(data.length / 2, packetReader.getRemaining());
+
+ // Confirm that we throw an exception if trying to set the temporary limit beyond the
+ // packet length.
+ packetReader.clearLimit();
+ try {
+ packetReader.setLimit(data.length * 2 + 1);
+ fail("Should have thrown an IOException when trying to set the temporary limit beyond "
+ + "the packet length");
+ } catch (IOException e) {
+ // Expected
+ } catch (Exception e) {
+ fail(String.format(
+ Locale.ROOT,
+ "Should not have thrown any other exception except " + "for IOException: %s",
+ e.getMessage()));
+ }
+ assertEquals(data.length, packetReader.getRemaining());
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
new file mode 100644
index 0000000..fdb4d4a
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+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 android.util.Log;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.util.List;
+
+// The record test data does not use compressed names (label pointers), since that would require
+// additional data to populate the label dictionary accordingly.
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsRecordTests {
+ private static final String TAG = "MdnsRecordTests";
+ private static final int MAX_PACKET_SIZE = 4096;
+ private static final InetSocketAddress MULTICAST_IPV4_ADDRESS =
+ new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
+ private static final InetSocketAddress MULTICAST_IPV6_ADDRESS =
+ new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
+
+ @Test
+ public void testInet4AddressRecord() throws IOException {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "0474657374000001" + "0001000011940004" + "0A010203");
+ assertNotNull(dataIn);
+ String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+ // Decode
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ String[] name = reader.readLabels();
+ assertNotNull(name);
+ assertEquals(1, name.length);
+ assertEquals("test", name[0]);
+ String fqdn = MdnsRecord.labelsToString(name);
+ assertEquals("test", fqdn);
+
+ int type = reader.readUInt16();
+ assertEquals(MdnsRecord.TYPE_A, type);
+
+ MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader);
+ Inet4Address addr = record.getInet4Address();
+ assertEquals("/10.1.2.3", addr.toString());
+
+ // Encode
+ MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+ record.write(writer, record.getReceiptTime());
+
+ packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+ byte[] dataOut = packet.getData();
+
+ String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+ Log.d(TAG, dataOutText);
+
+ assertEquals(dataInText, dataOutText);
+ }
+
+ @Test
+ public void testTypeAAAInet6AddressRecord() throws IOException {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "047465737400001C"
+ + "0001000011940010"
+ + "AABBCCDD11223344"
+ + "A0B0C0D010203040");
+ assertNotNull(dataIn);
+ String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+ // Decode
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ packet.setSocketAddress(
+ new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ String[] name = reader.readLabels();
+ assertNotNull(name);
+ assertEquals(1, name.length);
+ String fqdn = MdnsRecord.labelsToString(name);
+ assertEquals("test", fqdn);
+
+ int type = reader.readUInt16();
+ assertEquals(MdnsRecord.TYPE_AAAA, type);
+
+ MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA,
+ reader);
+ assertNull(record.getInet4Address());
+ Inet6Address addr = record.getInet6Address();
+ assertEquals("/aabb:ccdd:1122:3344:a0b0:c0d0:1020:3040", addr.toString());
+
+ // Encode
+ MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+ record.write(writer, record.getReceiptTime());
+
+ packet = writer.getPacket(MULTICAST_IPV6_ADDRESS);
+ byte[] dataOut = packet.getData();
+
+ String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+ Log.d(TAG, dataOutText);
+
+ assertEquals(dataInText, dataOutText);
+ }
+
+ @Test
+ public void testTypeAAAInet4AddressRecord() throws IOException {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "047465737400001C"
+ + "0001000011940010"
+ + "0000000000000000"
+ + "0000FFFF10203040");
+ assertNotNull(dataIn);
+ HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+ // Decode
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ packet.setSocketAddress(
+ new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ String[] name = reader.readLabels();
+ assertNotNull(name);
+ assertEquals(1, name.length);
+ String fqdn = MdnsRecord.labelsToString(name);
+ assertEquals("test", fqdn);
+
+ int type = reader.readUInt16();
+ assertEquals(MdnsRecord.TYPE_AAAA, type);
+
+ MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA,
+ reader);
+ assertNull(record.getInet6Address());
+ Inet4Address addr = record.getInet4Address();
+ assertEquals("/16.32.48.64", addr.toString());
+
+ // Encode
+ MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+ record.write(writer, record.getReceiptTime());
+
+ packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+ byte[] dataOut = packet.getData();
+
+ String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+ Log.d(TAG, dataOutText);
+
+ final byte[] expectedDataIn =
+ HexDump.hexStringToByteArray("047465737400001C000100001194000410203040");
+ assertNotNull(expectedDataIn);
+ String expectedDataInText = HexDump.dumpHexString(expectedDataIn, 0, expectedDataIn.length);
+
+ assertEquals(expectedDataInText, dataOutText);
+ }
+
+ @Test
+ public void testPointerRecord() throws IOException {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "047465737400000C"
+ + "000100001194000E"
+ + "03666F6F03626172"
+ + "047175787800");
+ assertNotNull(dataIn);
+ String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+ // Decode
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ String[] name = reader.readLabels();
+ assertNotNull(name);
+ assertEquals(1, name.length);
+ String fqdn = MdnsRecord.labelsToString(name);
+ assertEquals("test", fqdn);
+
+ int type = reader.readUInt16();
+ assertEquals(MdnsRecord.TYPE_PTR, type);
+
+ MdnsPointerRecord record = new MdnsPointerRecord(name, reader);
+ String[] pointer = record.getPointer();
+ assertEquals("foo.bar.quxx", MdnsRecord.labelsToString(pointer));
+
+ assertFalse(record.hasSubtype());
+ assertNull(record.getSubtype());
+
+ // Encode
+ MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+ record.write(writer, record.getReceiptTime());
+
+ packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+ byte[] dataOut = packet.getData();
+
+ String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+ Log.d(TAG, dataOutText);
+
+ assertEquals(dataInText, dataOutText);
+ }
+
+ @Test
+ public void testServiceRecord() throws IOException {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "0474657374000021"
+ + "0001000011940014"
+ + "000100FF1F480366"
+ + "6F6F036261720471"
+ + "75787800");
+ assertNotNull(dataIn);
+ String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+ // Decode
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ String[] name = reader.readLabels();
+ assertNotNull(name);
+ assertEquals(1, name.length);
+ String fqdn = MdnsRecord.labelsToString(name);
+ assertEquals("test", fqdn);
+
+ int type = reader.readUInt16();
+ assertEquals(MdnsRecord.TYPE_SRV, type);
+
+ MdnsServiceRecord record = new MdnsServiceRecord(name, reader);
+
+ int servicePort = record.getServicePort();
+ assertEquals(8008, servicePort);
+
+ String serviceHost = MdnsRecord.labelsToString(record.getServiceHost());
+ assertEquals("foo.bar.quxx", serviceHost);
+
+ assertEquals(1, record.getServicePriority());
+ assertEquals(255, record.getServiceWeight());
+
+ // Encode
+ MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+ record.write(writer, record.getReceiptTime());
+
+ packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+ byte[] dataOut = packet.getData();
+
+ String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+ Log.d(TAG, dataOutText);
+
+ assertEquals(dataInText, dataOutText);
+ }
+
+ @Test
+ public void testTextRecord() throws IOException {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "0474657374000010"
+ + "0001000011940024"
+ + "0D613D68656C6C6F"
+ + "2074686572650C62"
+ + "3D31323334353637"
+ + "3839300878797A3D"
+ + "21402324");
+ assertNotNull(dataIn);
+ String dataInText = HexDump.dumpHexString(dataIn, 0, dataIn.length);
+
+ // Decode
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+
+ String[] name = reader.readLabels();
+ assertNotNull(name);
+ assertEquals(1, name.length);
+ String fqdn = MdnsRecord.labelsToString(name);
+ assertEquals("test", fqdn);
+
+ int type = reader.readUInt16();
+ assertEquals(MdnsRecord.TYPE_TXT, type);
+
+ MdnsTextRecord record = new MdnsTextRecord(name, reader);
+
+ List<String> strings = record.getStrings();
+ assertNotNull(strings);
+ assertEquals(3, strings.size());
+
+ assertEquals("a=hello there", strings.get(0));
+ assertEquals("b=1234567890", strings.get(1));
+ assertEquals("xyz=!@#$", strings.get(2));
+
+ // Encode
+ MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
+ record.write(writer, record.getReceiptTime());
+
+ packet = writer.getPacket(MULTICAST_IPV4_ADDRESS);
+ byte[] dataOut = packet.getData();
+
+ String dataOutText = HexDump.dumpHexString(dataOut, 0, packet.getLength());
+ Log.d(TAG, dataOutText);
+
+ assertEquals(dataInText, dataOutText);
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
new file mode 100644
index 0000000..ea9156c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2021 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 static com.android.server.connectivity.mdns.MdnsResponseDecoder.Clock;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+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.Mockito.mock;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.util.LinkedList;
+import java.util.List;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsResponseDecoderTests {
+ private static final byte[] data = HexDump.hexStringToByteArray(
+ "0000840000000004"
+ + "00000003134A6F68"
+ + "6E6E792773204368"
+ + "726F6D6563617374"
+ + "0B5F676F6F676C65"
+ + "63617374045F7463"
+ + "70056C6F63616C00"
+ + "0010800100001194"
+ + "006C2369643D3937"
+ + "3062663534376237"
+ + "3533666336336332"
+ + "6432613336626238"
+ + "3936616261380576"
+ + "653D30320D6D643D"
+ + "4368726F6D656361"
+ + "73741269633D2F73"
+ + "657475702F69636F"
+ + "6E2E706E6716666E"
+ + "3D4A6F686E6E7927"
+ + "73204368726F6D65"
+ + "636173740463613D"
+ + "350473743D30095F"
+ + "7365727669636573"
+ + "075F646E732D7364"
+ + "045F756470C03100"
+ + "0C00010000119400"
+ + "02C020C020000C00"
+ + "01000011940002C0"
+ + "0CC00C0021800100"
+ + "000078001C000000"
+ + "001F49134A6F686E"
+ + "6E79277320436872"
+ + "6F6D6563617374C0"
+ + "31C0F30001800100"
+ + "0000780004C0A864"
+ + "68C0F3002F800100"
+ + "0000780005C0F300"
+ + "0140C00C002F8001"
+ + "000011940009C00C"
+ + "00050000800040");
+
+ private static final byte[] data6 = HexDump.hexStringToByteArray(
+ "0000840000000001000000030B5F676F6F676C656361737404"
+ + "5F746370056C6F63616C00000C000100000078003330476F6F676C"
+ + "652D486F6D652D4D61782D61363836666331323961366638636265"
+ + "31643636353139343065336164353766C00CC02E00108001000011"
+ + "9400C02369643D6136383666633132396136663863626531643636"
+ + "3531393430653361643537662363643D4133304233303032363546"
+ + "36384341313233353532434639344141353742314613726D3D4335"
+ + "35393134383530383841313638330576653D3035126D643D476F6F"
+ + "676C6520486F6D65204D61781269633D2F73657475702F69636F6E"
+ + "2E706E6710666E3D417474696320737065616B65720863613D3130"
+ + "3234340473743D320F62733D464138464341363734453537046E66"
+ + "3D320372733DC02E0021800100000078002D000000001F49246136"
+ + "3836666331322D396136662D386362652D316436362D3531393430"
+ + "65336164353766C01DC13F001C8001000000780010200033330000"
+ + "0000DA6C63FFFE7C74830109018001000000780004C0A801026C6F"
+ + "63616C0000018001000000780004C0A8010A000001800100000078"
+ + "0004C0A8010A00000000000000");
+
+ private static final String DUMMY_CAST_SERVICE_NAME = "_googlecast";
+ private static final String[] DUMMY_CAST_SERVICE_TYPE =
+ new String[] {DUMMY_CAST_SERVICE_NAME, "_tcp", "local"};
+
+ private final List<MdnsResponse> responses = new LinkedList<>();
+
+ private final Clock mClock = mock(Clock.class);
+
+ @Before
+ public void setUp() {
+ MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, DUMMY_CAST_SERVICE_TYPE);
+ assertNotNull(data);
+ DatagramPacket packet = new DatagramPacket(data, data.length);
+ packet.setSocketAddress(
+ new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
+ responses.clear();
+ int errorCode = decoder.decode(packet, responses);
+ assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
+ assertEquals(1, responses.size());
+ }
+
+ @Test
+ public void testDecodeWithNullServiceType() {
+ MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, null);
+ assertNotNull(data);
+ DatagramPacket packet = new DatagramPacket(data, data.length);
+ packet.setSocketAddress(
+ new InetSocketAddress(MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT));
+ responses.clear();
+ int errorCode = decoder.decode(packet, responses);
+ assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
+ assertEquals(2, responses.size());
+ }
+
+ @Test
+ public void testDecodeMultipleAnswerPacket() throws IOException {
+ MdnsResponse response = responses.get(0);
+ assertTrue(response.isComplete());
+
+ MdnsInetAddressRecord inet4AddressRecord = response.getInet4AddressRecord();
+ Inet4Address inet4Addr = inet4AddressRecord.getInet4Address();
+
+ assertNotNull(inet4Addr);
+ assertEquals("/192.168.100.104", inet4Addr.toString());
+
+ MdnsServiceRecord serviceRecord = response.getServiceRecord();
+ String serviceName = serviceRecord.getServiceName();
+ assertEquals(DUMMY_CAST_SERVICE_NAME, serviceName);
+
+ String serviceInstanceName = serviceRecord.getServiceInstanceName();
+ assertEquals("Johnny's Chromecast", serviceInstanceName);
+
+ String serviceHost = MdnsRecord.labelsToString(serviceRecord.getServiceHost());
+ assertEquals("Johnny's Chromecast.local", serviceHost);
+
+ int serviceProto = serviceRecord.getServiceProtocol();
+ assertEquals(MdnsServiceRecord.PROTO_TCP, serviceProto);
+
+ int servicePort = serviceRecord.getServicePort();
+ assertEquals(8009, servicePort);
+
+ int servicePriority = serviceRecord.getServicePriority();
+ assertEquals(0, servicePriority);
+
+ int serviceWeight = serviceRecord.getServiceWeight();
+ assertEquals(0, serviceWeight);
+
+ MdnsTextRecord textRecord = response.getTextRecord();
+ List<String> textStrings = textRecord.getStrings();
+ assertEquals(7, textStrings.size());
+ assertEquals("id=970bf547b753fc63c2d2a36bb896aba8", textStrings.get(0));
+ assertEquals("ve=02", textStrings.get(1));
+ assertEquals("md=Chromecast", textStrings.get(2));
+ assertEquals("ic=/setup/icon.png", textStrings.get(3));
+ assertEquals("fn=Johnny's Chromecast", textStrings.get(4));
+ assertEquals("ca=5", textStrings.get(5));
+ assertEquals("st=0", textStrings.get(6));
+ }
+
+ @Test
+ public void testDecodeIPv6AnswerPacket() throws IOException {
+ MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, DUMMY_CAST_SERVICE_TYPE);
+ assertNotNull(data6);
+ DatagramPacket packet = new DatagramPacket(data6, data6.length);
+ packet.setSocketAddress(
+ new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
+
+ responses.clear();
+ int errorCode = decoder.decode(packet, responses);
+ assertEquals(MdnsResponseDecoder.SUCCESS, errorCode);
+
+ MdnsResponse response = responses.get(0);
+ assertTrue(response.isComplete());
+
+ MdnsInetAddressRecord inet6AddressRecord = response.getInet6AddressRecord();
+ assertNotNull(inet6AddressRecord);
+ Inet4Address inet4Addr = inet6AddressRecord.getInet4Address();
+ assertNull(inet4Addr);
+
+ Inet6Address inet6Addr = inet6AddressRecord.getInet6Address();
+ assertNotNull(inet6Addr);
+ assertEquals(inet6Addr.getHostAddress(), "2000:3333::da6c:63ff:fe7c:7483");
+ }
+
+ @Test
+ public void testIsComplete() {
+ MdnsResponse response = responses.get(0);
+ assertTrue(response.isComplete());
+
+ response.clearPointerRecords();
+ assertFalse(response.isComplete());
+
+ response = responses.get(0);
+ response.setInet4AddressRecord(null);
+ assertFalse(response.isComplete());
+
+ response = responses.get(0);
+ response.setInet6AddressRecord(null);
+ assertFalse(response.isComplete());
+
+ response = responses.get(0);
+ response.setServiceRecord(null);
+ assertFalse(response.isComplete());
+
+ response = responses.get(0);
+ response.setTextRecord(null);
+ assertFalse(response.isComplete());
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
new file mode 100644
index 0000000..ae16f2b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+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;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.Arrays;
+import java.util.List;
+
+// The record test data does not use compressed names (label pointers), since that would require
+// additional data to populate the label dictionary accordingly.
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsResponseTests {
+ private static final String TAG = "MdnsResponseTests";
+ // MDNS response packet for name "test" with an IPv4 address of 10.1.2.3
+ private static final byte[] dataIn_ipv4_1 = HexDump.hexStringToByteArray(
+ "0474657374000001" + "0001000011940004" + "0A010203");
+ // MDNS response packet for name "tess" with an IPv4 address of 10.1.2.4
+ private static final byte[] dataIn_ipv4_2 = HexDump.hexStringToByteArray(
+ "0474657373000001" + "0001000011940004" + "0A010204");
+ // MDNS response w/name "test" & IPv6 address of aabb:ccdd:1122:3344:a0b0:c0d0:1020:3040
+ private static final byte[] dataIn_ipv6_1 = HexDump.hexStringToByteArray(
+ "047465737400001C" + "0001000011940010" + "AABBCCDD11223344" + "A0B0C0D010203040");
+ // MDNS response w/name "test" & IPv6 address of aabb:ccdd:1122:3344:a0b0:c0d0:1020:3030
+ private static final byte[] dataIn_ipv6_2 = HexDump.hexStringToByteArray(
+ "047465737400001C" + "0001000011940010" + "AABBCCDD11223344" + "A0B0C0D010203030");
+ // MDNS response w/name "test" & PTR to foo.bar.quxx
+ private static final byte[] dataIn_ptr_1 = HexDump.hexStringToByteArray(
+ "047465737400000C" + "000100001194000E" + "03666F6F03626172" + "047175787800");
+ // MDNS response w/name "test" & PTR to foo.bar.quxy
+ private static final byte[] dataIn_ptr_2 = HexDump.hexStringToByteArray(
+ "047465737400000C" + "000100001194000E" + "03666F6F03626172" + "047175787900");
+ // MDNS response w/name "test" & Service for host foo.bar.quxx
+ private static final byte[] dataIn_service_1 = HexDump.hexStringToByteArray(
+ "0474657374000021"
+ + "0001000011940014"
+ + "000100FF1F480366"
+ + "6F6F036261720471"
+ + "75787800");
+ // MDNS response w/name "test" & Service for host test
+ private static final byte[] dataIn_service_2 = HexDump.hexStringToByteArray(
+ "0474657374000021" + "000100001194000B" + "000100FF1F480474" + "657374");
+ // MDNS response w/name "test" & the following text strings:
+ // "a=hello there", "b=1234567890", and "xyz=!$$$"
+ private static final byte[] dataIn_text_1 = HexDump.hexStringToByteArray(
+ "0474657374000010"
+ + "0001000011940024"
+ + "0D613D68656C6C6F"
+ + "2074686572650C62"
+ + "3D31323334353637"
+ + "3839300878797A3D"
+ + "21242424");
+ // MDNS response w/name "test" & the following text strings:
+ // "a=hello there", "b=1234567890", and "xyz=!@#$"
+ private static final byte[] dataIn_text_2 = HexDump.hexStringToByteArray(
+ "0474657374000010"
+ + "0001000011940024"
+ + "0D613D68656C6C6F"
+ + "2074686572650C62"
+ + "3D31323334353637"
+ + "3839300878797A3D"
+ + "21402324");
+
+ // The following helper classes act as wrappers so that IPv4 and IPv6 address records can
+ // be explicitly created by type using same constructor signature as all other records.
+ static class MdnsInet4AddressRecord extends MdnsInetAddressRecord {
+ public MdnsInet4AddressRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, MdnsRecord.TYPE_A, reader);
+ }
+ }
+
+ static class MdnsInet6AddressRecord extends MdnsInetAddressRecord {
+ public MdnsInet6AddressRecord(String[] name, MdnsPacketReader reader) throws IOException {
+ super(name, MdnsRecord.TYPE_AAAA, reader);
+ }
+ }
+
+ // This helper class just wraps the data bytes of a response packet with the contained record
+ // type.
+ // Its only purpose is to make the test code a bit more readable.
+ static class PacketAndRecordClass {
+ public final byte[] packetData;
+ public final Class<?> recordClass;
+
+ public PacketAndRecordClass() {
+ packetData = null;
+ recordClass = null;
+ }
+
+ public PacketAndRecordClass(byte[] data, Class<?> c) {
+ packetData = data;
+ recordClass = c;
+ }
+ }
+
+ // Construct an MdnsResponse with the specified data packets applied.
+ private MdnsResponse makeMdnsResponse(long time, List<PacketAndRecordClass> responseList)
+ throws IOException {
+ MdnsResponse response = new MdnsResponse(time);
+ for (PacketAndRecordClass responseData : responseList) {
+ DatagramPacket packet =
+ new DatagramPacket(responseData.packetData, responseData.packetData.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ reader.skip(2); // skip record type indication.
+ // Apply the right kind of record to the response.
+ if (responseData.recordClass == MdnsInet4AddressRecord.class) {
+ response.setInet4AddressRecord(new MdnsInet4AddressRecord(name, reader));
+ } else if (responseData.recordClass == MdnsInet6AddressRecord.class) {
+ response.setInet6AddressRecord(new MdnsInet6AddressRecord(name, reader));
+ } else if (responseData.recordClass == MdnsPointerRecord.class) {
+ response.addPointerRecord(new MdnsPointerRecord(name, reader));
+ } else if (responseData.recordClass == MdnsServiceRecord.class) {
+ response.setServiceRecord(new MdnsServiceRecord(name, reader));
+ } else if (responseData.recordClass == MdnsTextRecord.class) {
+ response.setTextRecord(new MdnsTextRecord(name, reader));
+ } else {
+ fail("Unsupported/unexpected MdnsRecord subtype used in test - invalid test!");
+ }
+ }
+ return response;
+ }
+
+ @Test
+ public void getInet4AddressRecord_returnsAddedRecord() throws IOException {
+ DatagramPacket packet = new DatagramPacket(dataIn_ipv4_1, dataIn_ipv4_1.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ reader.skip(2); // skip record type indication.
+ MdnsInetAddressRecord record = new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader);
+ MdnsResponse response = new MdnsResponse(0);
+ assertFalse(response.hasInet4AddressRecord());
+ assertTrue(response.setInet4AddressRecord(record));
+ assertEquals(response.getInet4AddressRecord(), record);
+ }
+
+ @Test
+ public void getInet6AddressRecord_returnsAddedRecord() throws IOException {
+ DatagramPacket packet = new DatagramPacket(dataIn_ipv6_1, dataIn_ipv6_1.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ reader.skip(2); // skip record type indication.
+ MdnsInetAddressRecord record =
+ new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader);
+ MdnsResponse response = new MdnsResponse(0);
+ assertFalse(response.hasInet6AddressRecord());
+ assertTrue(response.setInet6AddressRecord(record));
+ assertEquals(response.getInet6AddressRecord(), record);
+ }
+
+ @Test
+ public void getPointerRecords_returnsAddedRecord() throws IOException {
+ DatagramPacket packet = new DatagramPacket(dataIn_ptr_1, dataIn_ptr_1.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ reader.skip(2); // skip record type indication.
+ MdnsPointerRecord record = new MdnsPointerRecord(name, reader);
+ MdnsResponse response = new MdnsResponse(0);
+ assertFalse(response.hasPointerRecords());
+ assertTrue(response.addPointerRecord(record));
+ List<MdnsPointerRecord> recordList = response.getPointerRecords();
+ assertNotNull(recordList);
+ assertEquals(1, recordList.size());
+ assertEquals(record, recordList.get(0));
+ }
+
+ @Test
+ public void getServiceRecord_returnsAddedRecord() throws IOException {
+ DatagramPacket packet = new DatagramPacket(dataIn_service_1, dataIn_service_1.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ reader.skip(2); // skip record type indication.
+ MdnsServiceRecord record = new MdnsServiceRecord(name, reader);
+ MdnsResponse response = new MdnsResponse(0);
+ assertFalse(response.hasServiceRecord());
+ assertTrue(response.setServiceRecord(record));
+ assertEquals(response.getServiceRecord(), record);
+ }
+
+ @Test
+ public void getTextRecord_returnsAddedRecord() throws IOException {
+ DatagramPacket packet = new DatagramPacket(dataIn_text_1, dataIn_text_1.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ reader.skip(2); // skip record type indication.
+ MdnsTextRecord record = new MdnsTextRecord(name, reader);
+ MdnsResponse response = new MdnsResponse(0);
+ assertFalse(response.hasTextRecord());
+ assertTrue(response.setTextRecord(record));
+ assertEquals(response.getTextRecord(), record);
+ }
+
+ @Test
+ public void mergeRecordsFrom_indicates_change_on_ipv4_address() throws IOException {
+ MdnsResponse response = makeMdnsResponse(
+ 0,
+ Arrays.asList(
+ new PacketAndRecordClass(dataIn_ipv4_1, MdnsInet4AddressRecord.class)));
+ // Now create a new response that updates the address.
+ MdnsResponse response2 = makeMdnsResponse(
+ 100,
+ Arrays.asList(
+ new PacketAndRecordClass(dataIn_ipv4_2, MdnsInet4AddressRecord.class)));
+ assertTrue(response.mergeRecordsFrom(response2));
+ }
+
+ @Test
+ public void mergeRecordsFrom_indicates_change_on_ipv6_address() throws IOException {
+ MdnsResponse response = makeMdnsResponse(
+ 0,
+ Arrays.asList(
+ new PacketAndRecordClass(dataIn_ipv6_1, MdnsInet6AddressRecord.class)));
+ // Now create a new response that updates the address.
+ MdnsResponse response2 = makeMdnsResponse(
+ 100,
+ Arrays.asList(
+ new PacketAndRecordClass(dataIn_ipv6_2, MdnsInet6AddressRecord.class)));
+ assertTrue(response.mergeRecordsFrom(response2));
+ }
+
+ @Test
+ public void mergeRecordsFrom_indicates_change_on_text() throws IOException {
+ MdnsResponse response = makeMdnsResponse(
+ 0,
+ Arrays.asList(new PacketAndRecordClass(dataIn_text_1, MdnsTextRecord.class)));
+ // Now create a new response that updates the address.
+ MdnsResponse response2 = makeMdnsResponse(
+ 100,
+ Arrays.asList(new PacketAndRecordClass(dataIn_text_2, MdnsTextRecord.class)));
+ assertTrue(response.mergeRecordsFrom(response2));
+ }
+
+ @Test
+ public void mergeRecordsFrom_indicates_change_on_service() throws IOException {
+ MdnsResponse response = makeMdnsResponse(
+ 0,
+ Arrays.asList(new PacketAndRecordClass(dataIn_service_1, MdnsServiceRecord.class)));
+ // Now create a new response that updates the address.
+ MdnsResponse response2 = makeMdnsResponse(
+ 100,
+ Arrays.asList(new PacketAndRecordClass(dataIn_service_2, MdnsServiceRecord.class)));
+ assertTrue(response.mergeRecordsFrom(response2));
+ }
+
+ @Test
+ public void mergeRecordsFrom_indicates_change_on_pointer() throws IOException {
+ MdnsResponse response = makeMdnsResponse(
+ 0,
+ Arrays.asList(new PacketAndRecordClass(dataIn_ptr_1, MdnsPointerRecord.class)));
+ // Now create a new response that updates the address.
+ MdnsResponse response2 = makeMdnsResponse(
+ 100,
+ Arrays.asList(new PacketAndRecordClass(dataIn_ptr_2, MdnsPointerRecord.class)));
+ assertTrue(response.mergeRecordsFrom(response2));
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void mergeRecordsFrom_indicates_noChange() throws IOException {
+ //MdnsConfigsFlagsImpl.useReducedMergeRecordUpdateEvents.override(true);
+ List<PacketAndRecordClass> recordList =
+ Arrays.asList(
+ new PacketAndRecordClass(dataIn_ipv4_1, MdnsInet4AddressRecord.class),
+ new PacketAndRecordClass(dataIn_ipv6_1, MdnsInet6AddressRecord.class),
+ new PacketAndRecordClass(dataIn_ptr_1, MdnsPointerRecord.class),
+ new PacketAndRecordClass(dataIn_service_2, MdnsServiceRecord.class),
+ new PacketAndRecordClass(dataIn_text_1, MdnsTextRecord.class));
+ // Create a two identical responses.
+ MdnsResponse response = makeMdnsResponse(0, recordList);
+ MdnsResponse response2 = makeMdnsResponse(100, recordList);
+ // Merging should not indicate any change.
+ assertFalse(response.mergeRecordsFrom(response2));
+ }
+}
\ 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
new file mode 100644
index 0000000..5843fd0
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -0,0 +1,770 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+
+import com.android.server.connectivity.mdns.MdnsServiceTypeClient.QueryTaskConfig;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for {@link MdnsServiceTypeClient}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsServiceTypeClientTests {
+
+ private static final String SERVICE_TYPE = "_googlecast._tcp.local";
+
+ @Mock
+ private MdnsServiceBrowserListener mockListenerOne;
+ @Mock
+ private MdnsServiceBrowserListener mockListenerTwo;
+ @Mock
+ private MdnsPacketWriter mockPacketWriter;
+ @Mock
+ private MdnsSocketClient mockSocketClient;
+ @Captor
+ private ArgumentCaptor<MdnsServiceInfo> serviceInfoCaptor;
+
+ private final byte[] buf = new byte[10];
+
+ private DatagramPacket[] expectedPackets;
+ private ScheduledFuture<?>[] expectedSendFutures;
+ private FakeExecutor currentThreadExecutor = new FakeExecutor();
+
+ private MdnsServiceTypeClient client;
+
+ @Before
+ @SuppressWarnings("DoNotMock")
+ public void setUp() throws IOException {
+ MockitoAnnotations.initMocks(this);
+
+ expectedPackets = new DatagramPacket[16];
+ expectedSendFutures = new ScheduledFuture<?>[16];
+
+ for (int i = 0; i < expectedSendFutures.length; ++i) {
+ expectedPackets[i] = new DatagramPacket(buf, 0, 5);
+ expectedSendFutures[i] = Mockito.mock(ScheduledFuture.class);
+ }
+ when(mockPacketWriter.getPacket(any(SocketAddress.class)))
+ .thenReturn(expectedPackets[0])
+ .thenReturn(expectedPackets[1])
+ .thenReturn(expectedPackets[2])
+ .thenReturn(expectedPackets[3])
+ .thenReturn(expectedPackets[4])
+ .thenReturn(expectedPackets[5])
+ .thenReturn(expectedPackets[6])
+ .thenReturn(expectedPackets[7])
+ .thenReturn(expectedPackets[8])
+ .thenReturn(expectedPackets[9])
+ .thenReturn(expectedPackets[10])
+ .thenReturn(expectedPackets[11])
+ .thenReturn(expectedPackets[12])
+ .thenReturn(expectedPackets[13])
+ .thenReturn(expectedPackets[14])
+ .thenReturn(expectedPackets[15]);
+
+ client =
+ new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+ @Override
+ MdnsPacketWriter createMdnsPacketWriter() {
+ return mockPacketWriter;
+ }
+ };
+ }
+
+ @Test
+ public void sendQueries_activeScanMode() {
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+
+ // First burst, 3 queries.
+ verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(
+ 1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ // Second burst will be sent after initialTimeBetweenBurstsMs, 3 queries.
+ verifyAndSendQuery(
+ 3, MdnsConfigs.initialTimeBetweenBurstsMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 4, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 5, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ // Third burst will be sent after initialTimeBetweenBurstsMs * 2, 3 queries.
+ verifyAndSendQuery(
+ 6, MdnsConfigs.initialTimeBetweenBurstsMs() * 2, /* expectsUnicastResponse= */
+ false);
+ verifyAndSendQuery(
+ 7, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 8, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ // Forth burst will be sent after initialTimeBetweenBurstsMs * 4, 3 queries.
+ verifyAndSendQuery(
+ 9, MdnsConfigs.initialTimeBetweenBurstsMs() * 4, /* expectsUnicastResponse= */
+ false);
+ verifyAndSendQuery(
+ 10, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 11, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ // Fifth burst will be sent after timeBetweenBurstsMs, 3 queries.
+ verifyAndSendQuery(12, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
+ false);
+ verifyAndSendQuery(
+ 13, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 14, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+
+ // Stop sending packets.
+ client.stopSendAndReceive(mockListenerOne);
+ verify(expectedSendFutures[15]).cancel(true);
+ }
+
+ @Test
+ public void sendQueries_reentry_activeScanMode() {
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+
+ // First burst, first query is sent.
+ verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+
+ // After the first query is sent, change the subtypes, and restart.
+ searchOptions =
+ MdnsSearchOptions.newBuilder()
+ .addSubtype("12345")
+ .addSubtype("abcde")
+ .setIsPassiveMode(false)
+ .build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+ // The previous scheduled task should be canceled.
+ verify(expectedSendFutures[1]).cancel(true);
+
+ // Queries should continue to be sent.
+ verifyAndSendQuery(1, 0, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(
+ 2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 3, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+
+ // Stop sending packets.
+ client.stopSendAndReceive(mockListenerOne);
+ verify(expectedSendFutures[5]).cancel(true);
+ }
+
+ @Test
+ public void sendQueries_passiveScanMode() {
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+
+ // First burst, 3 query.
+ verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(
+ 1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ // Second burst will be sent after timeBetweenBurstsMs, 1 query.
+ verifyAndSendQuery(3, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
+ false);
+ // Third burst will be sent after timeBetweenBurstsMs, 1 query.
+ verifyAndSendQuery(4, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
+ false);
+
+ // Stop sending packets.
+ client.stopSendAndReceive(mockListenerOne);
+ verify(expectedSendFutures[5]).cancel(true);
+ }
+
+ @Test
+ public void sendQueries_reentry_passiveScanMode() {
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+
+ // First burst, first query is sent.
+ verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
+
+ // After the first query is sent, change the subtypes, and restart.
+ searchOptions =
+ MdnsSearchOptions.newBuilder()
+ .addSubtype("12345")
+ .addSubtype("abcde")
+ .setIsPassiveMode(true)
+ .build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+ // The previous scheduled task should be canceled.
+ verify(expectedSendFutures[1]).cancel(true);
+
+ // Queries should continue to be sent.
+ verifyAndSendQuery(1, 0, /* expectsUnicastResponse= */ true);
+ verifyAndSendQuery(
+ 2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+ verifyAndSendQuery(
+ 3, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
+
+ // Stop sending packets.
+ client.stopSendAndReceive(mockListenerOne);
+ verify(expectedSendFutures[5]).cancel(true);
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void testQueryTaskConfig_alwaysAskForUnicastResponse() {
+ //MdnsConfigsFlagsImpl.alwaysAskForUnicastResponseInEachBurst.override(true);
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+ QueryTaskConfig config =
+ new QueryTaskConfig(searchOptions.getSubtypes(), searchOptions.isPassiveMode(), 1);
+
+ // This is the first query. We will ask for unicast response.
+ assertTrue(config.expectUnicastResponse);
+ assertEquals(config.subtypes, searchOptions.getSubtypes());
+ assertEquals(config.transactionId, 1);
+
+ // For the rest of queries in this burst, we will NOT ask for unicast response.
+ for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
+ int oldTransactionId = config.transactionId;
+ config = config.getConfigForNextRun();
+ assertFalse(config.expectUnicastResponse);
+ assertEquals(config.subtypes, searchOptions.getSubtypes());
+ assertEquals(config.transactionId, oldTransactionId + 1);
+ }
+
+ // This is the first query of a new burst. We will ask for unicast response.
+ int oldTransactionId = config.transactionId;
+ config = config.getConfigForNextRun();
+ assertTrue(config.expectUnicastResponse);
+ assertEquals(config.subtypes, searchOptions.getSubtypes());
+ assertEquals(config.transactionId, oldTransactionId + 1);
+ }
+
+ @Test
+ public void testQueryTaskConfig_askForUnicastInFirstQuery() {
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
+ QueryTaskConfig config =
+ new QueryTaskConfig(searchOptions.getSubtypes(), searchOptions.isPassiveMode(), 1);
+
+ // This is the first query. We will ask for unicast response.
+ assertTrue(config.expectUnicastResponse);
+ assertEquals(config.subtypes, searchOptions.getSubtypes());
+ assertEquals(config.transactionId, 1);
+
+ // For the rest of queries in this burst, we will NOT ask for unicast response.
+ for (int i = 1; i < MdnsConfigs.queriesPerBurst(); i++) {
+ int oldTransactionId = config.transactionId;
+ config = config.getConfigForNextRun();
+ assertFalse(config.expectUnicastResponse);
+ assertEquals(config.subtypes, searchOptions.getSubtypes());
+ assertEquals(config.transactionId, oldTransactionId + 1);
+ }
+
+ // This is the first query of a new burst. We will NOT ask for unicast response.
+ int oldTransactionId = config.transactionId;
+ config = config.getConfigForNextRun();
+ assertFalse(config.expectUnicastResponse);
+ assertEquals(config.subtypes, searchOptions.getSubtypes());
+ assertEquals(config.transactionId, oldTransactionId + 1);
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void testIfPreviousTaskIsCanceledWhenNewSessionStarts() {
+ //MdnsConfigsFlagsImpl.useSessionIdToScheduleMdnsTask.override(true);
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+ Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+ // Change the sutypes and start a new session.
+ searchOptions =
+ MdnsSearchOptions.newBuilder()
+ .addSubtype("12345")
+ .addSubtype("abcde")
+ .setIsPassiveMode(true)
+ .build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+
+ // Clear the scheduled runnable.
+ currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+ // Simulate the case where the first mdns task is not successful canceled and it gets
+ // executed anyway.
+ firstMdnsTask.run();
+
+ // Although it gets executes, no more task gets scheduled.
+ assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void testIfPreviousTaskIsCanceledWhenSessionStops() {
+ //MdnsConfigsFlagsImpl.shouldCancelScanTaskWhenFutureIsNull.override(true);
+ MdnsSearchOptions searchOptions =
+ MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+ // Change the sutypes and start a new session.
+ client.stopSendAndReceive(mockListenerOne);
+ // Clear the scheduled runnable.
+ currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+ // Simulate the case where the first mdns task is not successful canceled and it gets
+ // executed anyway.
+ currentThreadExecutor.getAndClearSubmittedRunnable().run();
+
+ // Although it gets executes, no more task gets scheduled.
+ assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
+ }
+
+ @Test
+ public void processResponse_incompleteResponse() {
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+ MdnsResponse response = mock(MdnsResponse.class);
+ when(response.getServiceInstanceName()).thenReturn("service-instance-1");
+ when(response.isComplete()).thenReturn(false);
+
+ client.processResponse(response);
+
+ verify(mockListenerOne, never()).onServiceFound(any(MdnsServiceInfo.class));
+ verify(mockListenerOne, never()).onServiceUpdated(any(MdnsServiceInfo.class));
+ }
+
+ @Test
+ public void processIPv4Response_completeResponseForNewServiceInstance() throws Exception {
+ final String ipV4Address = "192.168.1.1";
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ "service-instance-1",
+ ipV4Address,
+ 5353,
+ Collections.singletonList("ABCDE"),
+ Collections.emptyMap());
+ client.processResponse(initialResponse);
+
+ // Process a second response with a different port and updated text attributes.
+ MdnsResponse secondResponse =
+ createResponse(
+ "service-instance-1",
+ ipV4Address,
+ 5354,
+ Collections.singletonList("ABCDE"),
+ Collections.singletonMap("key", "value"));
+ client.processResponse(secondResponse);
+
+ // Verify onServiceFound was called once for the initial response.
+ verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+ MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+ assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
+ assertEquals(initialServiceInfo.getIpv4Address(), ipV4Address);
+ assertEquals(initialServiceInfo.getPort(), 5353);
+ assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+ assertNull(initialServiceInfo.getAttributeByKey("key"));
+
+ // Verify onServiceUpdated was called once for the second response.
+ verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
+ MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(1);
+ assertEquals(updatedServiceInfo.getServiceInstanceName(), "service-instance-1");
+ assertEquals(updatedServiceInfo.getIpv4Address(), ipV4Address);
+ assertEquals(updatedServiceInfo.getPort(), 5354);
+ assertTrue(updatedServiceInfo.hasSubtypes());
+ assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+ assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value");
+ }
+
+ @Test
+ public void processIPv6Response_getCorrectServiceInfo() throws Exception {
+ final String ipV6Address = "2000:3333::da6c:63ff:fe7c:7483";
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ "service-instance-1",
+ ipV6Address,
+ 5353,
+ Collections.singletonList("ABCDE"),
+ Collections.emptyMap());
+ client.processResponse(initialResponse);
+
+ // Process a second response with a different port and updated text attributes.
+ MdnsResponse secondResponse =
+ createResponse(
+ "service-instance-1",
+ ipV6Address,
+ 5354,
+ Collections.singletonList("ABCDE"),
+ Collections.singletonMap("key", "value"));
+ client.processResponse(secondResponse);
+
+ System.out.println("secondResponses ip"
+ + secondResponse.getInet6AddressRecord().getInet6Address().getHostAddress());
+
+ // Verify onServiceFound was called once for the initial response.
+ verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+ MdnsServiceInfo initialServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+ assertEquals(initialServiceInfo.getServiceInstanceName(), "service-instance-1");
+ assertEquals(initialServiceInfo.getIpv6Address(), ipV6Address);
+ assertEquals(initialServiceInfo.getPort(), 5353);
+ assertEquals(initialServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+ assertNull(initialServiceInfo.getAttributeByKey("key"));
+
+ // Verify onServiceUpdated was called once for the second response.
+ verify(mockListenerOne).onServiceUpdated(serviceInfoCaptor.capture());
+ MdnsServiceInfo updatedServiceInfo = serviceInfoCaptor.getAllValues().get(1);
+ assertEquals(updatedServiceInfo.getServiceInstanceName(), "service-instance-1");
+ assertEquals(updatedServiceInfo.getIpv6Address(), ipV6Address);
+ assertEquals(updatedServiceInfo.getPort(), 5354);
+ assertTrue(updatedServiceInfo.hasSubtypes());
+ assertEquals(updatedServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+ assertEquals(updatedServiceInfo.getAttributeByKey("key"), "value");
+ }
+
+ @Test
+ public void processResponse_goodBye() {
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+ MdnsResponse response = mock(MdnsResponse.class);
+ when(response.getServiceInstanceName()).thenReturn("goodbye-service-instance-name");
+ when(response.isGoodbye()).thenReturn(true);
+ client.processResponse(response);
+
+ verify(mockListenerOne).onServiceRemoved("goodbye-service-instance-name");
+ verify(mockListenerTwo).onServiceRemoved("goodbye-service-instance-name");
+ }
+
+ @Test
+ public void reportExistingServiceToNewlyRegisteredListeners() throws UnknownHostException {
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ "service-instance-1",
+ "192.168.1.1",
+ 5353,
+ Collections.singletonList("ABCDE"),
+ Collections.emptyMap());
+ client.processResponse(initialResponse);
+
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+
+ // Verify onServiceFound was called once for the existing response.
+ verify(mockListenerOne).onServiceFound(serviceInfoCaptor.capture());
+ MdnsServiceInfo existingServiceInfo = serviceInfoCaptor.getAllValues().get(0);
+ assertEquals(existingServiceInfo.getServiceInstanceName(), "service-instance-1");
+ assertEquals(existingServiceInfo.getIpv4Address(), "192.168.1.1");
+ assertEquals(existingServiceInfo.getPort(), 5353);
+ assertEquals(existingServiceInfo.getSubtypes(), Collections.singletonList("ABCDE"));
+ assertNull(existingServiceInfo.getAttributeByKey("key"));
+
+ // Process a goodbye message for the existing response.
+ MdnsResponse goodByeResponse = mock(MdnsResponse.class);
+ when(goodByeResponse.getServiceInstanceName()).thenReturn("service-instance-1");
+ when(goodByeResponse.isGoodbye()).thenReturn(true);
+ client.processResponse(goodByeResponse);
+
+ client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+ // Verify onServiceFound was not called on the newly registered listener after the existing
+ // response is gone.
+ verify(mockListenerTwo, never()).onServiceFound(any(MdnsServiceInfo.class));
+ }
+
+ @Test
+ public void processResponse_notAllowRemoveSearch_shouldNotRemove() throws Exception {
+ final String serviceInstanceName = "service-instance-1";
+ client.startSendAndReceive(
+ mockListenerOne,
+ MdnsSearchOptions.newBuilder().build());
+ Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+ Map.of());
+ client.processResponse(initialResponse);
+
+ // Clear the scheduled runnable.
+ currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+ // Simulate the case where the response is after TTL.
+ when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+ firstMdnsTask.run();
+
+ // Verify onServiceRemoved was not called.
+ verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void processResponse_allowSearchOptionsToRemoveExpiredService_shouldRemove()
+ throws Exception {
+ //MdnsConfigsFlagsImpl.allowSearchOptionsToRemoveExpiredService.override(true);
+ final String serviceInstanceName = "service-instance-1";
+ client =
+ new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+ @Override
+ MdnsPacketWriter createMdnsPacketWriter() {
+ return mockPacketWriter;
+ }
+ };
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+ Map.of());
+ client.processResponse(initialResponse);
+
+ // Clear the scheduled runnable.
+ currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+ // Simulate the case where the response is under TTL.
+ when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 1000);
+ firstMdnsTask.run();
+
+ // Verify onServiceRemoved was not called.
+ verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
+
+ // Simulate the case where the response is after TTL.
+ when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+ firstMdnsTask.run();
+
+ // Verify onServiceRemoved was called.
+ verify(mockListenerOne, times(1)).onServiceRemoved(serviceInstanceName);
+ }
+
+ @Test
+ public void processResponse_searchOptionsNotEnableServiceRemoval_shouldNotRemove()
+ throws Exception {
+ final String serviceInstanceName = "service-instance-1";
+ client =
+ new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+ @Override
+ MdnsPacketWriter createMdnsPacketWriter() {
+ return mockPacketWriter;
+ }
+ };
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+ Map.of());
+ client.processResponse(initialResponse);
+
+ // Clear the scheduled runnable.
+ currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+ // Simulate the case where the response is after TTL.
+ when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+ firstMdnsTask.run();
+
+ // Verify onServiceRemoved was not called.
+ verify(mockListenerOne, never()).onServiceRemoved(serviceInstanceName);
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void processResponse_removeServiceAfterTtlExpiresEnabled_shouldRemove()
+ throws Exception {
+ //MdnsConfigsFlagsImpl.removeServiceAfterTtlExpires.override(true);
+ final String serviceInstanceName = "service-instance-1";
+ client =
+ new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor) {
+ @Override
+ MdnsPacketWriter createMdnsPacketWriter() {
+ return mockPacketWriter;
+ }
+ };
+ client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
+
+ // Process the initial response.
+ MdnsResponse initialResponse =
+ createResponse(
+ serviceInstanceName, "192.168.1.1", 5353, List.of("ABCDE"),
+ Map.of());
+ client.processResponse(initialResponse);
+
+ // Clear the scheduled runnable.
+ currentThreadExecutor.getAndClearLastScheduledRunnable();
+
+ // Simulate the case where the response is after TTL.
+ when(initialResponse.getServiceRecord().getRemainingTTL(anyLong())).thenReturn((long) 0);
+ firstMdnsTask.run();
+
+ // Verify onServiceRemoved was not called.
+ verify(mockListenerOne, times(1)).onServiceRemoved(serviceInstanceName);
+ }
+
+ // verifies that the right query was enqueued with the right delay, and send query by executing
+ // the runnable.
+ private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse) {
+ assertEquals(currentThreadExecutor.getAndClearLastScheduledDelayInMs(), timeInMs);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ if (expectsUnicastResponse) {
+ verify(mockSocketClient).sendUnicastPacket(expectedPackets[index]);
+ } else {
+ verify(mockSocketClient).sendMulticastPacket(expectedPackets[index]);
+ }
+ }
+
+ // A fake ScheduledExecutorService that keeps tracking the last scheduled Runnable and its delay
+ // time.
+ private class FakeExecutor extends ScheduledThreadPoolExecutor {
+ private long lastScheduledDelayInMs;
+ private Runnable lastScheduledRunnable;
+ private Runnable lastSubmittedRunnable;
+ private int futureIndex;
+
+ FakeExecutor() {
+ super(1);
+ lastScheduledDelayInMs = -1;
+ }
+
+ @Override
+ public Future<?> submit(Runnable command) {
+ Future<?> future = super.submit(command);
+ lastSubmittedRunnable = command;
+ return future;
+ }
+
+ // Don't call through the real implementation, just track the scheduled Runnable, and
+ // returns a ScheduledFuture.
+ @Override
+ public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+ lastScheduledDelayInMs = delay;
+ lastScheduledRunnable = command;
+ return expectedSendFutures[futureIndex++];
+ }
+
+ // Returns the delay of the last scheduled task, and clear it.
+ long getAndClearLastScheduledDelayInMs() {
+ long val = lastScheduledDelayInMs;
+ lastScheduledDelayInMs = -1;
+ return val;
+ }
+
+ // Returns the last scheduled task, and clear it.
+ Runnable getAndClearLastScheduledRunnable() {
+ Runnable val = lastScheduledRunnable;
+ lastScheduledRunnable = null;
+ return val;
+ }
+
+ Runnable getAndClearSubmittedRunnable() {
+ Runnable val = lastSubmittedRunnable;
+ lastSubmittedRunnable = null;
+ return val;
+ }
+ }
+
+ // Creates a complete mDNS response.
+ private MdnsResponse createResponse(
+ @NonNull String serviceInstanceName,
+ @NonNull String host,
+ int port,
+ @NonNull List<String> subtypes,
+ @NonNull Map<String, String> textAttributes)
+ throws UnknownHostException {
+ String[] hostName = new String[]{"hostname"};
+ MdnsServiceRecord serviceRecord = mock(MdnsServiceRecord.class);
+ when(serviceRecord.getServiceHost()).thenReturn(hostName);
+ when(serviceRecord.getServicePort()).thenReturn(port);
+
+ MdnsResponse response = spy(new MdnsResponse(0));
+
+ MdnsInetAddressRecord inetAddressRecord = mock(MdnsInetAddressRecord.class);
+ if (host.contains(":")) {
+ when(inetAddressRecord.getInet6Address())
+ .thenReturn((Inet6Address) Inet6Address.getByName(host));
+ response.setInet6AddressRecord(inetAddressRecord);
+ } else {
+ when(inetAddressRecord.getInet4Address())
+ .thenReturn((Inet4Address) Inet4Address.getByName(host));
+ response.setInet4AddressRecord(inetAddressRecord);
+ }
+
+ MdnsTextRecord textRecord = mock(MdnsTextRecord.class);
+ List<String> textStrings = new ArrayList<>();
+ for (Map.Entry<String, String> kv : textAttributes.entrySet()) {
+ textStrings.add(kv.getKey() + "=" + kv.getValue());
+ }
+ when(textRecord.getStrings()).thenReturn(textStrings);
+
+ response.setServiceRecord(serviceRecord);
+ response.setTextRecord(textRecord);
+
+ doReturn(false).when(response).isGoodbye();
+ doReturn(true).when(response).isComplete();
+ doReturn(serviceInstanceName).when(response).getServiceInstanceName();
+ doReturn(new ArrayList<>(subtypes)).when(response).getSubtypes();
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
new file mode 100644
index 0000000..21ed7eb
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest.permission;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.MulticastLock;
+import android.text.format.DateUtils;
+
+import com.android.net.module.util.HexDump;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Tests for {@link MdnsSocketClient} */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsSocketClientTests {
+ private static final long TIMEOUT = 500;
+ private final byte[] buf = new byte[10];
+ final AtomicBoolean enableMulticastResponse = new AtomicBoolean(true);
+ final AtomicBoolean enableUnicastResponse = new AtomicBoolean(true);
+
+ @Mock private Context mContext;
+ @Mock private WifiManager mockWifiManager;
+ @Mock private MdnsSocket mockMulticastSocket;
+ @Mock private MdnsSocket mockUnicastSocket;
+ @Mock private MulticastLock mockMulticastLock;
+ @Mock private MdnsSocketClient.Callback mockCallback;
+
+ private MdnsSocketClient mdnsClient;
+
+ @Before
+ public void setup() throws RuntimeException, IOException {
+ MockitoAnnotations.initMocks(this);
+
+ when(mockWifiManager.createMulticastLock(ArgumentMatchers.anyString()))
+ .thenReturn(mockMulticastLock);
+
+ mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock) {
+ @Override
+ MdnsSocket createMdnsSocket(int port) throws IOException {
+ if (port == MdnsConstants.MDNS_PORT) {
+ return mockMulticastSocket;
+ }
+ return mockUnicastSocket;
+ }
+ };
+ mdnsClient.setCallback(mockCallback);
+
+ doAnswer(
+ (InvocationOnMock invocationOnMock) -> {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "0000840000000004"
+ + "00000003134A6F68"
+ + "6E6E792773204368"
+ + "726F6D6563617374"
+ + "0B5F676F6F676C65"
+ + "63617374045F7463"
+ + "70056C6F63616C00"
+ + "0010800100001194"
+ + "006C2369643D3937"
+ + "3062663534376237"
+ + "3533666336336332"
+ + "6432613336626238"
+ + "3936616261380576"
+ + "653D30320D6D643D"
+ + "4368726F6D656361"
+ + "73741269633D2F73"
+ + "657475702F69636F"
+ + "6E2E706E6716666E"
+ + "3D4A6F686E6E7927"
+ + "73204368726F6D65"
+ + "636173740463613D"
+ + "350473743D30095F"
+ + "7365727669636573"
+ + "075F646E732D7364"
+ + "045F756470C03100"
+ + "0C00010000119400"
+ + "02C020C020000C00"
+ + "01000011940002C0"
+ + "0CC00C0021800100"
+ + "000078001C000000"
+ + "001F49134A6F686E"
+ + "6E79277320436872"
+ + "6F6D6563617374C0"
+ + "31C0F30001800100"
+ + "0000780004C0A864"
+ + "68C0F3002F800100"
+ + "0000780005C0F300"
+ + "0140C00C002F8001"
+ + "000011940009C00C"
+ + "00050000800040");
+ if (enableMulticastResponse.get()) {
+ DatagramPacket packet = invocationOnMock.getArgument(0);
+ packet.setData(dataIn);
+ }
+ return null;
+ })
+ .when(mockMulticastSocket)
+ .receive(any(DatagramPacket.class));
+ doAnswer(
+ (InvocationOnMock invocationOnMock) -> {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "0000840000000004"
+ + "00000003134A6F68"
+ + "6E6E792773204368"
+ + "726F6D6563617374"
+ + "0B5F676F6F676C65"
+ + "63617374045F7463"
+ + "70056C6F63616C00"
+ + "0010800100001194"
+ + "006C2369643D3937"
+ + "3062663534376237"
+ + "3533666336336332"
+ + "6432613336626238"
+ + "3936616261380576"
+ + "653D30320D6D643D"
+ + "4368726F6D656361"
+ + "73741269633D2F73"
+ + "657475702F69636F"
+ + "6E2E706E6716666E"
+ + "3D4A6F686E6E7927"
+ + "73204368726F6D65"
+ + "636173740463613D"
+ + "350473743D30095F"
+ + "7365727669636573"
+ + "075F646E732D7364"
+ + "045F756470C03100"
+ + "0C00010000119400"
+ + "02C020C020000C00"
+ + "01000011940002C0"
+ + "0CC00C0021800100"
+ + "000078001C000000"
+ + "001F49134A6F686E"
+ + "6E79277320436872"
+ + "6F6D6563617374C0"
+ + "31C0F30001800100"
+ + "0000780004C0A864"
+ + "68C0F3002F800100"
+ + "0000780005C0F300"
+ + "0140C00C002F8001"
+ + "000011940009C00C"
+ + "00050000800040");
+ if (enableUnicastResponse.get()) {
+ DatagramPacket packet = invocationOnMock.getArgument(0);
+ packet.setData(dataIn);
+ }
+ return null;
+ })
+ .when(mockUnicastSocket)
+ .receive(any(DatagramPacket.class));
+ }
+
+ @After
+ public void tearDown() {
+ mdnsClient.stopDiscovery();
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void testSendPackets_useSeparateSocketForUnicast()
+ throws InterruptedException, IOException {
+ //MdnsConfigsFlagsImpl.useSeparateSocketToSendUnicastQuery.override(true);
+ //MdnsConfigsFlagsImpl.checkMulticastResponse.override(true);
+ //MdnsConfigsFlagsImpl.checkMulticastResponseIntervalMs
+ // .override(DateUtils.SECOND_IN_MILLIS);
+ mdnsClient.startDiscovery();
+ Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
+ Thread unicastReceiverThread = mdnsClient.unicastReceiveThread;
+ Thread sendThread = mdnsClient.sendThread;
+
+ assertTrue(multicastReceiverThread.isAlive());
+ assertTrue(sendThread.isAlive());
+ assertTrue(unicastReceiverThread.isAlive());
+
+ // Sends a packet.
+ DatagramPacket packet = new DatagramPacket(buf, 0, 5);
+ mdnsClient.sendMulticastPacket(packet);
+ // mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
+ // it may not be called yet. So timeout is added.
+ verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
+ verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
+
+ // Verify the packet is sent by the unicast socket.
+ mdnsClient.sendUnicastPacket(packet);
+ verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
+ verify(mockUnicastSocket, timeout(TIMEOUT).times(1)).send(packet);
+
+ // Stop the MdnsClient, and ensure that it stops in a reasonable amount of time.
+ // Run part of the test logic in a background thread, in case stopDiscovery() blocks
+ // for a long time (the foreground thread can fail the test early).
+ final CountDownLatch stopDiscoveryLatch = new CountDownLatch(1);
+ Thread testThread =
+ new Thread(
+ new Runnable() {
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ @Override
+ public void run() {
+ mdnsClient.stopDiscovery();
+ stopDiscoveryLatch.countDown();
+ }
+ });
+ testThread.start();
+ assertTrue(stopDiscoveryLatch.await(DateUtils.SECOND_IN_MILLIS, TimeUnit.MILLISECONDS));
+
+ // We should be able to join in a reasonable amount of time, to prove that the
+ // the MdnsClient exited without sending the large queue of packets.
+ testThread.join(DateUtils.SECOND_IN_MILLIS);
+
+ assertFalse(multicastReceiverThread.isAlive());
+ assertFalse(sendThread.isAlive());
+ assertFalse(unicastReceiverThread.isAlive());
+ }
+
+ @Test
+ public void testSendPackets_useSameSocketForMulticastAndUnicast()
+ throws InterruptedException, IOException {
+ mdnsClient.startDiscovery();
+ Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
+ Thread unicastReceiverThread = mdnsClient.unicastReceiveThread;
+ Thread sendThread = mdnsClient.sendThread;
+
+ assertTrue(multicastReceiverThread.isAlive());
+ assertTrue(sendThread.isAlive());
+ assertNull(unicastReceiverThread);
+
+ // Sends a packet.
+ DatagramPacket packet = new DatagramPacket(buf, 0, 5);
+ mdnsClient.sendMulticastPacket(packet);
+ // mockMulticastSocket.send() will be called on another thread. If we verify it immediately,
+ // it may not be called yet. So timeout is added.
+ verify(mockMulticastSocket, timeout(TIMEOUT).times(1)).send(packet);
+ verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
+
+ // Verify the packet is sent by the multicast socket as well.
+ mdnsClient.sendUnicastPacket(packet);
+ verify(mockMulticastSocket, timeout(TIMEOUT).times(2)).send(packet);
+ verify(mockUnicastSocket, timeout(TIMEOUT).times(0)).send(packet);
+
+ // Stop the MdnsClient, and ensure that it stops in a reasonable amount of time.
+ // Run part of the test logic in a background thread, in case stopDiscovery() blocks
+ // for a long time (the foreground thread can fail the test early).
+ final CountDownLatch stopDiscoveryLatch = new CountDownLatch(1);
+ Thread testThread =
+ new Thread(
+ new Runnable() {
+ @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
+ @Override
+ public void run() {
+ mdnsClient.stopDiscovery();
+ stopDiscoveryLatch.countDown();
+ }
+ });
+ testThread.start();
+ assertTrue(stopDiscoveryLatch.await(DateUtils.SECOND_IN_MILLIS, TimeUnit.MILLISECONDS));
+
+ // We should be able to join in a reasonable amount of time, to prove that the
+ // the MdnsClient exited without sending the large queue of packets.
+ testThread.join(DateUtils.SECOND_IN_MILLIS);
+
+ assertFalse(multicastReceiverThread.isAlive());
+ assertFalse(sendThread.isAlive());
+ assertNull(unicastReceiverThread);
+ }
+
+ @Test
+ public void testStartStop() throws IOException {
+ for (int i = 0; i < 5; i++) {
+ mdnsClient.startDiscovery();
+
+ Thread multicastReceiverThread = mdnsClient.multicastReceiveThread;
+ Thread socketThread = mdnsClient.sendThread;
+
+ assertTrue(multicastReceiverThread.isAlive());
+ assertTrue(socketThread.isAlive());
+
+ mdnsClient.stopDiscovery();
+
+ assertFalse(multicastReceiverThread.isAlive());
+ assertFalse(socketThread.isAlive());
+ }
+ }
+
+ @Test
+ public void testStopDiscovery_queueIsCleared() throws IOException {
+ mdnsClient.startDiscovery();
+ mdnsClient.stopDiscovery();
+ mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+
+ synchronized (mdnsClient.multicastPacketQueue) {
+ assertTrue(mdnsClient.multicastPacketQueue.isEmpty());
+ }
+ }
+
+ @Test
+ public void testSendPacket_afterDiscoveryStops() throws IOException {
+ mdnsClient.startDiscovery();
+ mdnsClient.stopDiscovery();
+ mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+
+ synchronized (mdnsClient.multicastPacketQueue) {
+ assertTrue(mdnsClient.multicastPacketQueue.isEmpty());
+ }
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void testSendPacket_queueReachesSizeLimit() throws IOException {
+ //MdnsConfigsFlagsImpl.mdnsPacketQueueMaxSize.override(2L);
+ mdnsClient.startDiscovery();
+ for (int i = 0; i < 100; i++) {
+ mdnsClient.sendMulticastPacket(new DatagramPacket(buf, 0, 5));
+ }
+
+ synchronized (mdnsClient.multicastPacketQueue) {
+ assertTrue(mdnsClient.multicastPacketQueue.size() <= 2);
+ }
+ }
+
+ @Test
+ public void testMulticastResponseReceived_useSeparateSocketForUnicast() throws IOException {
+ mdnsClient.setCallback(mockCallback);
+
+ mdnsClient.startDiscovery();
+
+ verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+ .onResponseReceived(any(MdnsResponse.class));
+ }
+
+ @Test
+ public void testMulticastResponseReceived_useSameSocketForMulticastAndUnicast()
+ throws Exception {
+ mdnsClient.startDiscovery();
+
+ verify(mockCallback, timeout(TIMEOUT).atLeastOnce())
+ .onResponseReceived(any(MdnsResponse.class));
+
+ mdnsClient.stopDiscovery();
+ }
+
+ @Test
+ public void testFailedToParseMdnsResponse_useSeparateSocketForUnicast() throws IOException {
+ mdnsClient.setCallback(mockCallback);
+
+ // Both multicast socket and unicast socket receive malformed responses.
+ byte[] dataIn = HexDump.hexStringToByteArray("0000840000000004");
+ doAnswer(
+ (InvocationOnMock invocationOnMock) -> {
+ // Malformed data.
+ DatagramPacket packet = invocationOnMock.getArgument(0);
+ packet.setData(dataIn);
+ return null;
+ })
+ .when(mockMulticastSocket)
+ .receive(any(DatagramPacket.class));
+ doAnswer(
+ (InvocationOnMock invocationOnMock) -> {
+ // Malformed data.
+ DatagramPacket packet = invocationOnMock.getArgument(0);
+ packet.setData(dataIn);
+ return null;
+ })
+ .when(mockUnicastSocket)
+ .receive(any(DatagramPacket.class));
+
+ mdnsClient.startDiscovery();
+
+ verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+ .onFailedToParseMdnsResponse(anyInt(), eq(MdnsResponseErrorCode.ERROR_END_OF_FILE));
+
+ mdnsClient.stopDiscovery();
+ }
+
+ @Test
+ public void testFailedToParseMdnsResponse_useSameSocketForMulticastAndUnicast()
+ throws IOException {
+ doAnswer(
+ (InvocationOnMock invocationOnMock) -> {
+ final byte[] dataIn = HexDump.hexStringToByteArray("0000840000000004");
+ DatagramPacket packet = invocationOnMock.getArgument(0);
+ packet.setData(dataIn);
+ return null;
+ })
+ .when(mockMulticastSocket)
+ .receive(any(DatagramPacket.class));
+
+ mdnsClient.startDiscovery();
+
+ verify(mockCallback, timeout(TIMEOUT).atLeast(1))
+ .onFailedToParseMdnsResponse(1, MdnsResponseErrorCode.ERROR_END_OF_FILE);
+
+ mdnsClient.stopDiscovery();
+ }
+
+ @Test
+ @Ignore("MdnsConfigs is not configurable currently.")
+ public void testMulticastResponseIsNotReceived() throws IOException, InterruptedException {
+ //MdnsConfigsFlagsImpl.checkMulticastResponse.override(true);
+ //MdnsConfigsFlagsImpl.checkMulticastResponseIntervalMs
+ // .override(DateUtils.SECOND_IN_MILLIS);
+ //MdnsConfigsFlagsImpl.useSeparateSocketToSendUnicastQuery.override(true);
+ enableMulticastResponse.set(false);
+ enableUnicastResponse.set(true);
+
+ mdnsClient.startDiscovery();
+ DatagramPacket packet = new DatagramPacket(buf, 0, 5);
+ mdnsClient.sendUnicastPacket(packet);
+ mdnsClient.sendMulticastPacket(packet);
+
+ // Wait for the timer to be triggered.
+ Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
+
+ assertFalse(mdnsClient.receivedMulticastResponse);
+ assertTrue(mdnsClient.receivedUnicastResponse);
+ assertTrue(mdnsClient.cannotReceiveMulticastResponse.get());
+
+ // Allow multicast response and verify the states again.
+ enableMulticastResponse.set(true);
+ Thread.sleep(DateUtils.SECOND_IN_MILLIS);
+
+ // Verify cannotReceiveMulticastResponse is reset to false.
+ assertTrue(mdnsClient.receivedMulticastResponse);
+ assertTrue(mdnsClient.receivedUnicastResponse);
+ assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
+
+ // Stop the discovery and start a new session. Don't respond the unicsat query either in
+ // this session.
+ enableMulticastResponse.set(false);
+ enableUnicastResponse.set(false);
+ mdnsClient.stopDiscovery();
+ mdnsClient.startDiscovery();
+
+ // Verify the states are reset.
+ assertFalse(mdnsClient.receivedMulticastResponse);
+ assertFalse(mdnsClient.receivedUnicastResponse);
+ assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
+
+ mdnsClient.sendUnicastPacket(packet);
+ mdnsClient.sendMulticastPacket(packet);
+ Thread.sleep(MdnsConfigs.checkMulticastResponseIntervalMs() * 2);
+
+ // Verify cannotReceiveMulticastResponse is not set the true because we didn't receive the
+ // unicast response either. This is expected for users who don't have any cast device.
+ assertFalse(mdnsClient.receivedMulticastResponse);
+ assertFalse(mdnsClient.receivedUnicastResponse);
+ assertFalse(mdnsClient.cannotReceiveMulticastResponse.get());
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java
new file mode 100644
index 0000000..9f11a4b
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketTests.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Collections;
+
+/** Tests for {@link MdnsSocket}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsSocketTests {
+
+ @Mock private NetworkInterfaceWrapper mockNetworkInterfaceWrapper;
+ @Mock private MulticastSocket mockMulticastSocket;
+ @Mock private MulticastNetworkInterfaceProvider mockMulticastNetworkInterfaceProvider;
+ private SocketAddress socketIPv4Address;
+ private SocketAddress socketIPv6Address;
+
+ private byte[] data = new byte[25];
+ private final DatagramPacket datagramPacket = new DatagramPacket(data, data.length);
+ private NetworkInterface networkInterface;
+
+ private MdnsSocket mdnsSocket;
+
+ @Before
+ public void setUp() throws SocketException, UnknownHostException {
+ MockitoAnnotations.initMocks(this);
+
+ networkInterface = createEmptyNetworkInterface();
+ when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
+ when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
+ .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
+ socketIPv4Address = new InetSocketAddress(
+ InetAddress.getByName("224.0.0.251"), MdnsConstants.MDNS_PORT);
+ socketIPv6Address = new InetSocketAddress(
+ InetAddress.getByName("FF02::FB"), MdnsConstants.MDNS_PORT);
+ }
+
+ @Test
+ public void testMdnsSocket() throws IOException {
+ mdnsSocket =
+ new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
+ @Override
+ MulticastSocket createMulticastSocket(int port) throws IOException {
+ return mockMulticastSocket;
+ }
+ };
+ mdnsSocket.send(datagramPacket);
+ verify(mockMulticastSocket).setNetworkInterface(networkInterface);
+ verify(mockMulticastSocket).send(datagramPacket);
+
+ mdnsSocket.receive(datagramPacket);
+ verify(mockMulticastSocket).receive(datagramPacket);
+
+ mdnsSocket.joinGroup();
+ verify(mockMulticastSocket).joinGroup(socketIPv4Address, networkInterface);
+
+ mdnsSocket.leaveGroup();
+ verify(mockMulticastSocket).leaveGroup(socketIPv4Address, networkInterface);
+
+ mdnsSocket.close();
+ verify(mockMulticastSocket).close();
+ }
+
+ @Test
+ public void testIPv6OnlyNetwork_IPv6Enabled() throws IOException {
+ // Have mockMulticastNetworkInterfaceProvider send back an IPv6Only networkInterfaceWrapper
+ networkInterface = createEmptyNetworkInterface();
+ when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
+ when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
+ .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
+
+ mdnsSocket =
+ new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
+ @Override
+ MulticastSocket createMulticastSocket(int port) throws IOException {
+ return mockMulticastSocket;
+ }
+ };
+
+ when(mockMulticastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(
+ Collections.singletonList(mockNetworkInterfaceWrapper)))
+ .thenReturn(true);
+
+ mdnsSocket.joinGroup();
+ verify(mockMulticastSocket).joinGroup(socketIPv6Address, networkInterface);
+
+ mdnsSocket.leaveGroup();
+ verify(mockMulticastSocket).leaveGroup(socketIPv6Address, networkInterface);
+
+ mdnsSocket.close();
+ verify(mockMulticastSocket).close();
+ }
+
+ @Test
+ public void testIPv6OnlyNetwork_IPv6Toggle() throws IOException {
+ // Have mockMulticastNetworkInterfaceProvider send back a networkInterfaceWrapper
+ networkInterface = createEmptyNetworkInterface();
+ when(mockNetworkInterfaceWrapper.getNetworkInterface()).thenReturn(networkInterface);
+ when(mockMulticastNetworkInterfaceProvider.getMulticastNetworkInterfaces())
+ .thenReturn(Collections.singletonList(mockNetworkInterfaceWrapper));
+
+ mdnsSocket =
+ new MdnsSocket(mockMulticastNetworkInterfaceProvider, MdnsConstants.MDNS_PORT) {
+ @Override
+ MulticastSocket createMulticastSocket(int port) throws IOException {
+ return mockMulticastSocket;
+ }
+ };
+
+ when(mockMulticastNetworkInterfaceProvider.isOnIpV6OnlyNetwork(
+ Collections.singletonList(mockNetworkInterfaceWrapper)))
+ .thenReturn(true);
+
+ mdnsSocket.joinGroup();
+ verify(mockMulticastSocket).joinGroup(socketIPv6Address, networkInterface);
+
+ mdnsSocket.leaveGroup();
+ verify(mockMulticastSocket).leaveGroup(socketIPv6Address, networkInterface);
+
+ mdnsSocket.close();
+ verify(mockMulticastSocket).close();
+ }
+
+ private NetworkInterface createEmptyNetworkInterface() {
+ try {
+ Constructor<NetworkInterface> constructor =
+ NetworkInterface.class.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ return constructor.newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java
new file mode 100644
index 0000000..2268dfe
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MulticastNetworkInterfaceProviderTests.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2021 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 static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Tests for {@link MulticastNetworkInterfaceProvider}. */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MulticastNetworkInterfaceProviderTests {
+
+ @Mock private NetworkInterfaceWrapper loopbackInterface;
+ @Mock private NetworkInterfaceWrapper pointToPointInterface;
+ @Mock private NetworkInterfaceWrapper virtualInterface;
+ @Mock private NetworkInterfaceWrapper inactiveMulticastInterface;
+ @Mock private NetworkInterfaceWrapper activeIpv6MulticastInterface;
+ @Mock private NetworkInterfaceWrapper activeIpv6MulticastInterfaceTwo;
+ @Mock private NetworkInterfaceWrapper nonMulticastInterface;
+ @Mock private NetworkInterfaceWrapper multicastInterfaceOne;
+ @Mock private NetworkInterfaceWrapper multicastInterfaceTwo;
+
+ private final List<NetworkInterfaceWrapper> networkInterfaces = new ArrayList<>();
+ private MulticastNetworkInterfaceProvider provider;
+ private Context context;
+
+ @Before
+ public void setUp() throws SocketException, UnknownHostException {
+ MockitoAnnotations.initMocks(this);
+ context = InstrumentationRegistry.getContext();
+
+ setupNetworkInterface(
+ loopbackInterface,
+ true /* isUp */,
+ true /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ setupNetworkInterface(
+ pointToPointInterface,
+ true /* isUp */,
+ false /* isLoopBack */,
+ true /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ setupNetworkInterface(
+ virtualInterface,
+ true /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ true /* isVirtual */,
+ true /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ setupNetworkInterface(
+ inactiveMulticastInterface,
+ false /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ setupNetworkInterface(
+ nonMulticastInterface,
+ true /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ false /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ setupNetworkInterface(
+ activeIpv6MulticastInterface,
+ true /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ true /* isIpv6 */);
+
+ setupNetworkInterface(
+ activeIpv6MulticastInterfaceTwo,
+ true /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ true /* isIpv6 */);
+
+ setupNetworkInterface(
+ multicastInterfaceOne,
+ true /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ setupNetworkInterface(
+ multicastInterfaceTwo,
+ true /* isUp */,
+ false /* isLoopBack */,
+ false /* isPointToPoint */,
+ false /* isVirtual */,
+ true /* supportsMulticast */,
+ false /* isIpv6 */);
+
+ provider =
+ new MulticastNetworkInterfaceProvider(context) {
+ @Override
+ List<NetworkInterfaceWrapper> getNetworkInterfaces() {
+ return networkInterfaces;
+ }
+ };
+ }
+
+ @Test
+ public void testGetMulticastNetworkInterfaces() {
+ // getNetworkInterfaces returns 1 multicast interface and 5 interfaces that can not be used
+ // to send and receive multicast packets.
+ networkInterfaces.add(loopbackInterface);
+ networkInterfaces.add(pointToPointInterface);
+ networkInterfaces.add(virtualInterface);
+ networkInterfaces.add(inactiveMulticastInterface);
+ networkInterfaces.add(nonMulticastInterface);
+ networkInterfaces.add(multicastInterfaceOne);
+
+ assertEquals(Collections.singletonList(multicastInterfaceOne),
+ provider.getMulticastNetworkInterfaces());
+
+ // getNetworkInterfaces returns 2 multicast interfaces after a connectivity change.
+ networkInterfaces.clear();
+ networkInterfaces.add(multicastInterfaceOne);
+ networkInterfaces.add(multicastInterfaceTwo);
+
+ provider.connectivityMonitor.notifyConnectivityChange();
+
+ assertEquals(networkInterfaces, provider.getMulticastNetworkInterfaces());
+ }
+
+ @Test
+ public void testStartWatchingConnectivityChanges() {
+ ConnectivityMonitor mockMonitor = mock(ConnectivityMonitor.class);
+ provider.connectivityMonitor = mockMonitor;
+
+ InOrder inOrder = inOrder(mockMonitor);
+
+ provider.startWatchingConnectivityChanges();
+ inOrder.verify(mockMonitor).startWatchingConnectivityChanges();
+
+ provider.stopWatchingConnectivityChanges();
+ inOrder.verify(mockMonitor).stopWatchingConnectivityChanges();
+ }
+
+ @Test
+ public void testIpV6OnlyNetwork_EmptyNetwork() {
+ // getNetworkInterfaces returns no network interfaces.
+ assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+ }
+
+ @Test
+ public void testIpV6OnlyNetwork_IPv4Only() {
+ // getNetworkInterfaces returns two IPv4 network interface.
+ networkInterfaces.add(multicastInterfaceOne);
+ networkInterfaces.add(multicastInterfaceTwo);
+ assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+ }
+
+ @Test
+ public void testIpV6OnlyNetwork_MixedNetwork() {
+ // getNetworkInterfaces returns one IPv6 network interface.
+ networkInterfaces.add(activeIpv6MulticastInterface);
+ networkInterfaces.add(multicastInterfaceOne);
+ networkInterfaces.add(activeIpv6MulticastInterfaceTwo);
+ networkInterfaces.add(multicastInterfaceTwo);
+ assertFalse(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+ }
+
+ @Test
+ public void testIpV6OnlyNetwork_IPv6Only() {
+ // getNetworkInterfaces returns one IPv6 network interface.
+ networkInterfaces.add(activeIpv6MulticastInterface);
+ networkInterfaces.add(activeIpv6MulticastInterfaceTwo);
+ assertTrue(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+ }
+
+ @Test
+ public void testIpV6OnlyNetwork_IPv6Enabled() {
+ // getNetworkInterfaces returns one IPv6 network interface.
+ networkInterfaces.add(activeIpv6MulticastInterface);
+ assertTrue(provider.isOnIpV6OnlyNetwork(networkInterfaces));
+
+ final List<NetworkInterfaceWrapper> interfaces = provider.getMulticastNetworkInterfaces();
+ assertEquals(Collections.singletonList(activeIpv6MulticastInterface), interfaces);
+ }
+
+ private void setupNetworkInterface(
+ @NonNull NetworkInterfaceWrapper networkInterfaceWrapper,
+ boolean isUp,
+ boolean isLoopback,
+ boolean isPointToPoint,
+ boolean isVirtual,
+ boolean supportsMulticast,
+ boolean isIpv6)
+ throws SocketException, UnknownHostException {
+ when(networkInterfaceWrapper.isUp()).thenReturn(isUp);
+ when(networkInterfaceWrapper.isLoopback()).thenReturn(isLoopback);
+ when(networkInterfaceWrapper.isPointToPoint()).thenReturn(isPointToPoint);
+ when(networkInterfaceWrapper.isVirtual()).thenReturn(isVirtual);
+ when(networkInterfaceWrapper.supportsMulticast()).thenReturn(supportsMulticast);
+ if (isIpv6) {
+ InterfaceAddress interfaceAddress = mock(InterfaceAddress.class);
+ InetAddress ip6Address = Inet6Address.getByName("2001:4860:0:1001::68");
+ when(interfaceAddress.getAddress()).thenReturn(ip6Address);
+ when(networkInterfaceWrapper.getInterfaceAddresses())
+ .thenReturn(Collections.singletonList(interfaceAddress));
+ } else {
+ Inet4Address ip = (Inet4Address) Inet4Address.getByName("192.168.0.1");
+ InterfaceAddress interfaceAddress = mock(InterfaceAddress.class);
+ when(interfaceAddress.getAddress()).thenReturn(ip);
+ when(networkInterfaceWrapper.getInterfaceAddresses())
+ .thenReturn(Collections.singletonList(interfaceAddress));
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java b/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java
new file mode 100644
index 0000000..a9f80ea
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.net.InetAddresses;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkAddress;
+import android.net.ProxyInfo;
+import android.net.StaticIpConfiguration;
+import android.util.ArrayMap;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class EthernetConfigStoreTest {
+ private static final LinkAddress LINKADDR = new LinkAddress("192.168.1.100/25");
+ private static final InetAddress GATEWAY = InetAddresses.parseNumericAddress("192.168.1.1");
+ private static final InetAddress DNS1 = InetAddresses.parseNumericAddress("8.8.8.8");
+ private static final InetAddress DNS2 = InetAddresses.parseNumericAddress("8.8.4.4");
+ private static final StaticIpConfiguration STATIC_IP_CONFIG =
+ new StaticIpConfiguration.Builder()
+ .setIpAddress(LINKADDR)
+ .setGateway(GATEWAY)
+ .setDnsServers(new ArrayList<InetAddress>(
+ List.of(DNS1, DNS2)))
+ .build();
+ private static final ProxyInfo PROXY_INFO = ProxyInfo.buildDirectProxy("test", 8888);
+ private static final IpConfiguration APEX_IP_CONFIG =
+ new IpConfiguration(IpAssignment.DHCP, ProxySettings.NONE, null, null);
+ private static final IpConfiguration LEGACY_IP_CONFIG =
+ new IpConfiguration(IpAssignment.STATIC, ProxySettings.STATIC, STATIC_IP_CONFIG,
+ PROXY_INFO);
+
+ private EthernetConfigStore mEthernetConfigStore;
+ private File mApexTestDir;
+ private File mLegacyTestDir;
+ private File mApexConfigFile;
+ private File mLegacyConfigFile;
+
+ private void createTestDir() {
+ final Context context = InstrumentationRegistry.getContext();
+ final File baseDir = context.getFilesDir();
+ mApexTestDir = new File(baseDir.getPath() + "/apex");
+ mApexTestDir.mkdirs();
+
+ mLegacyTestDir = new File(baseDir.getPath() + "/legacy");
+ mLegacyTestDir.mkdirs();
+ }
+
+ @Before
+ public void setUp() {
+ createTestDir();
+ mEthernetConfigStore = new EthernetConfigStore();
+ }
+
+ @After
+ public void tearDown() {
+ mApexTestDir.delete();
+ mLegacyTestDir.delete();
+ }
+
+ private void assertConfigFileExist(final String filepath) {
+ assertTrue(new File(filepath).exists());
+ }
+
+ /** Wait for the delayed write operation completes. */
+ private void waitForMs(long ms) {
+ try {
+ Thread.sleep(ms);
+ } catch (final InterruptedException e) {
+ fail("Thread was interrupted");
+ }
+ }
+
+ @Test
+ public void testWriteIpConfigToApexFilePathAndRead() throws Exception {
+ // Write the config file to the apex file path, pretend the config file exits and
+ // check if IP config should be read from apex file path.
+ mApexConfigFile = new File(mApexTestDir.getPath(), "test.txt");
+ mEthernetConfigStore.write("eth0", APEX_IP_CONFIG, mApexConfigFile.getPath());
+ waitForMs(50);
+
+ mEthernetConfigStore.read(mApexTestDir.getPath(), mLegacyTestDir.getPath(), "/test.txt");
+ final ArrayMap<String, IpConfiguration> ipConfigurations =
+ mEthernetConfigStore.getIpConfigurations();
+ assertEquals(APEX_IP_CONFIG, ipConfigurations.get("eth0"));
+
+ mApexConfigFile.delete();
+ }
+
+ @Test
+ public void testWriteIpConfigToLegacyFilePathAndRead() throws Exception {
+ // Write the config file to the legacy file path, pretend the config file exits and
+ // check if IP config should be read from legacy file path.
+ mLegacyConfigFile = new File(mLegacyTestDir, "test.txt");
+ mEthernetConfigStore.write("0", LEGACY_IP_CONFIG, mLegacyConfigFile.getPath());
+ waitForMs(50);
+
+ mEthernetConfigStore.read(mApexTestDir.getPath(), mLegacyTestDir.getPath(), "/test.txt");
+ final ArrayMap<String, IpConfiguration> ipConfigurations =
+ mEthernetConfigStore.getIpConfigurations();
+ assertEquals(LEGACY_IP_CONFIG, ipConfigurations.get("0"));
+
+ // Check the same config file in apex file path is created.
+ assertConfigFileExist(mApexTestDir.getPath() + "/test.txt");
+
+ final File apexConfigFile = new File(mApexTestDir.getPath() + "/test.txt");
+ apexConfigFile.delete();
+ mLegacyConfigFile.delete();
+ }
+}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
index dfb4fcc..2178b33 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -16,8 +16,6 @@
package com.android.server.ethernet;
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
@@ -32,7 +30,6 @@
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -51,20 +48,22 @@
import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
import android.net.NetworkProvider;
+import android.net.NetworkProvider.NetworkOfferCallback;
import android.net.NetworkRequest;
import android.net.StaticIpConfiguration;
import android.net.ip.IpClientCallbacks;
import android.net.ip.IpClientManager;
+import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.test.TestLooper;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.net.module.util.InterfaceParams;
import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.After;
import org.junit.Before;
@@ -79,8 +78,9 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
-@RunWith(AndroidJUnit4.class)
@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class EthernetNetworkFactoryTest {
private static final int TIMEOUT_MS = 2_000;
private static final String TEST_IFACE = "test123";
@@ -92,6 +92,8 @@
private Handler mHandler;
private EthernetNetworkFactory mNetFactory = null;
private IpClientCallbacks mIpClientCallbacks;
+ private NetworkOfferCallback mNetworkOfferCallback;
+ private NetworkRequest mRequestToKeepNetworkUp;
@Mock private Context mContext;
@Mock private Resources mResources;
@Mock private EthernetNetworkFactory.Dependencies mDeps;
@@ -99,6 +101,7 @@
@Mock private EthernetNetworkAgent mNetworkAgent;
@Mock private InterfaceParams mInterfaceParams;
@Mock private Network mMockNetwork;
+ @Mock private NetworkProvider mNetworkProvider;
@Before
public void setUp() throws Exception {
@@ -112,7 +115,7 @@
private void initEthernetNetworkFactory() {
mLooper = new TestLooper();
mHandler = new Handler(mLooper.getLooper());
- mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mDeps);
+ mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mNetworkProvider, mDeps);
}
private void setupNetworkAgentMock() {
@@ -239,9 +242,18 @@
mNetFactory.addInterface(iface, HW_ADDR, ipConfig,
createInterfaceCapsBuilder(transportType).build());
assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
+
+ ArgumentCaptor<NetworkOfferCallback> captor = ArgumentCaptor.forClass(
+ NetworkOfferCallback.class);
+ verify(mNetworkProvider).registerNetworkOffer(any(), any(), any(), captor.capture());
+ mRequestToKeepNetworkUp = createDefaultRequest();
+ mNetworkOfferCallback = captor.getValue();
+ mNetworkOfferCallback.onNetworkNeeded(mRequestToKeepNetworkUp);
+
verifyStart(ipConfig);
clearInvocations(mDeps);
clearInvocations(mIpClient);
+ clearInvocations(mNetworkProvider);
}
// creates a provisioned interface
@@ -281,29 +293,15 @@
// To create an unprovisioned interface, provision and then "stop" it, i.e. stop its
// NetworkAgent and IpClient. One way this can be done is by provisioning an interface and
// then calling onNetworkUnwanted.
- createAndVerifyProvisionedInterface(iface);
-
- mNetworkAgent.getCallbacks().onNetworkUnwanted();
- mLooper.dispatchAll();
- verifyStop();
+ mNetFactory.addInterface(iface, HW_ADDR, createDefaultIpConfig(),
+ createInterfaceCapsBuilder(NetworkCapabilities.TRANSPORT_ETHERNET).build());
+ assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
clearInvocations(mIpClient);
clearInvocations(mNetworkAgent);
}
@Test
- public void testAcceptRequest() throws Exception {
- initEthernetNetworkFactory();
- createInterfaceUndergoingProvisioning(TEST_IFACE);
- assertTrue(mNetFactory.acceptRequest(createDefaultRequest()));
-
- NetworkRequest wifiRequest = createDefaultRequestBuilder()
- .removeTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build();
- assertFalse(mNetFactory.acceptRequest(wifiRequest));
- }
-
- @Test
public void testUpdateInterfaceLinkStateForActiveProvisioningInterface() throws Exception {
initEthernetNetworkFactory();
createInterfaceUndergoingProvisioning(TEST_IFACE);
@@ -378,36 +376,6 @@
}
@Test
- public void testNeedNetworkForOnProvisionedInterface() throws Exception {
- initEthernetNetworkFactory();
- createAndVerifyProvisionedInterface(TEST_IFACE);
- mNetFactory.needNetworkFor(createDefaultRequest());
- verify(mIpClient, never()).startProvisioning(any());
- }
-
- @Test
- public void testNeedNetworkForOnUnprovisionedInterface() throws Exception {
- initEthernetNetworkFactory();
- createUnprovisionedInterface(TEST_IFACE);
- mNetFactory.needNetworkFor(createDefaultRequest());
- verify(mIpClient).startProvisioning(any());
-
- triggerOnProvisioningSuccess();
- verifyNetworkAgentRegistersAndConnects();
- }
-
- @Test
- public void testNeedNetworkForOnInterfaceUndergoingProvisioning() throws Exception {
- initEthernetNetworkFactory();
- createInterfaceUndergoingProvisioning(TEST_IFACE);
- mNetFactory.needNetworkFor(createDefaultRequest());
- verify(mIpClient, never()).startProvisioning(any());
-
- triggerOnProvisioningSuccess();
- verifyNetworkAgentRegistersAndConnects();
- }
-
- @Test
public void testProvisioningLoss() throws Exception {
initEthernetNetworkFactory();
when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
@@ -441,31 +409,6 @@
}
@Test
- public void testIpClientIsNotStartedWhenLinkIsDown() throws Exception {
- initEthernetNetworkFactory();
- createUnprovisionedInterface(TEST_IFACE);
- mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER);
-
- mNetFactory.needNetworkFor(createDefaultRequest());
-
- verify(mDeps, never()).makeIpClient(any(), any(), any());
-
- // BUG(b/191854824): requesting a network with a specifier (Android Auto use case) should
- // not start an IpClient when the link is down, but fixing this may make matters worse by
- // tiggering b/197548738.
- NetworkRequest specificNetRequest = new NetworkRequest.Builder()
- .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
- .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE))
- .build();
- mNetFactory.needNetworkFor(specificNetRequest);
- mNetFactory.releaseNetworkFor(specificNetRequest);
-
- mNetFactory.updateInterfaceLinkState(TEST_IFACE, true, NULL_LISTENER);
- // TODO: change to once when b/191854824 is fixed.
- verify(mDeps, times(2)).makeIpClient(any(), eq(TEST_IFACE), any());
- }
-
- @Test
public void testLinkPropertiesChanged() throws Exception {
initEthernetNetworkFactory();
createAndVerifyProvisionedInterface(TEST_IFACE);
@@ -669,7 +612,6 @@
assertEquals(listener.expectOnResult(), TEST_IFACE);
}
- @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
@Test
public void testUpdateInterfaceAbortsOnConcurrentRemoveInterface() throws Exception {
initEthernetNetworkFactory();
@@ -678,7 +620,6 @@
() -> mNetFactory.removeInterface(TEST_IFACE));
}
- @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
@Test
public void testUpdateInterfaceAbortsOnConcurrentUpdateInterfaceLinkState() throws Exception {
initEthernetNetworkFactory();
@@ -687,7 +628,14 @@
() -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER));
}
- @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+ @Test
+ public void testUpdateInterfaceAbortsOnNetworkUneededRemovesAllRequests() throws Exception {
+ initEthernetNetworkFactory();
+ verifyNetworkManagementCallIsAbortedWhenInterrupted(
+ TEST_IFACE,
+ () -> mNetworkOfferCallback.onNetworkUnneeded(mRequestToKeepNetworkUp));
+ }
+
@Test
public void testUpdateInterfaceCallsListenerCorrectlyOnConcurrentRequests() throws Exception {
initEthernetNetworkFactory();
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
index dd1f1ed..a1d93a0 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
@@ -16,16 +16,17 @@
package com.android.server.ethernet;
+import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
-
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
@@ -35,29 +36,37 @@
import android.annotation.NonNull;
import android.content.Context;
import android.content.pm.PackageManager;
-import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.EthernetNetworkSpecifier;
import android.net.EthernetNetworkUpdateRequest;
+import android.net.INetworkInterfaceOutcomeReceiver;
import android.net.IpConfiguration;
import android.net.NetworkCapabilities;
+import android.net.StringNetworkSpecifier;
+import android.os.Build;
import android.os.Handler;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-@RunWith(AndroidJUnit4.class)
@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class EthernetServiceImplTest {
private static final String TEST_IFACE = "test123";
+ private static final NetworkCapabilities DEFAULT_CAPS = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE))
+ .build();
private static final EthernetNetworkUpdateRequest UPDATE_REQUEST =
new EthernetNetworkUpdateRequest.Builder()
.setIpConfiguration(new IpConfiguration())
- .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+ .setNetworkCapabilities(DEFAULT_CAPS)
.build();
private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_CAPABILITIES =
new EthernetNetworkUpdateRequest.Builder()
@@ -65,18 +74,21 @@
.build();
private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_IP_CONFIG =
new EthernetNetworkUpdateRequest.Builder()
- .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+ .setNetworkCapabilities(DEFAULT_CAPS)
.build();
private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
private EthernetServiceImpl mEthernetServiceImpl;
- @Mock private Context mContext;
- @Mock private Handler mHandler;
- @Mock private EthernetTracker mEthernetTracker;
- @Mock private PackageManager mPackageManager;
+ private Context mContext;
+ private Handler mHandler;
+ private EthernetTracker mEthernetTracker;
+ private PackageManager mPackageManager;
@Before
public void setup() {
- MockitoAnnotations.initMocks(this);
+ mContext = mock(Context.class);
+ mHandler = mock(Handler.class);
+ mEthernetTracker = mock(EthernetTracker.class);
+ mPackageManager = mock(PackageManager.class);
doReturn(mPackageManager).when(mContext).getPackageManager();
mEthernetServiceImpl = new EthernetServiceImpl(mContext, mHandler, mEthernetTracker);
mEthernetServiceImpl.mStarted.set(true);
@@ -111,18 +123,18 @@
}
@Test
- public void testConnectNetworkRejectsWhenEthNotStarted() {
+ public void testEnableInterfaceRejectsWhenEthNotStarted() {
mEthernetServiceImpl.mStarted.set(false);
assertThrows(IllegalStateException.class, () -> {
- mEthernetServiceImpl.connectNetwork("" /* iface */, null /* listener */);
+ mEthernetServiceImpl.enableInterface("" /* iface */, null /* listener */);
});
}
@Test
- public void testDisconnectNetworkRejectsWhenEthNotStarted() {
+ public void testDisableInterfaceRejectsWhenEthNotStarted() {
mEthernetServiceImpl.mStarted.set(false);
assertThrows(IllegalStateException.class, () -> {
- mEthernetServiceImpl.disconnectNetwork("" /* iface */, null /* listener */);
+ mEthernetServiceImpl.disableInterface("" /* iface */, null /* listener */);
});
}
@@ -134,16 +146,16 @@
}
@Test
- public void testConnectNetworkRejectsNullIface() {
+ public void testEnableInterfaceRejectsNullIface() {
assertThrows(NullPointerException.class, () -> {
- mEthernetServiceImpl.connectNetwork(null /* iface */, NULL_LISTENER);
+ mEthernetServiceImpl.enableInterface(null /* iface */, NULL_LISTENER);
});
}
@Test
- public void testDisconnectNetworkRejectsNullIface() {
+ public void testDisableInterfaceRejectsNullIface() {
assertThrows(NullPointerException.class, () -> {
- mEthernetServiceImpl.disconnectNetwork(null /* iface */, NULL_LISTENER);
+ mEthernetServiceImpl.disableInterface(null /* iface */, NULL_LISTENER);
});
}
@@ -156,6 +168,41 @@
}
@Test
+ public void testUpdateConfigurationRejectsWithInvalidSpecifierType() {
+ final StringNetworkSpecifier invalidSpecifierType = new StringNetworkSpecifier("123");
+ final EthernetNetworkUpdateRequest request =
+ new EthernetNetworkUpdateRequest.Builder()
+ .setNetworkCapabilities(
+ new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .setNetworkSpecifier(invalidSpecifierType)
+ .build()
+ ).build();
+ assertThrows(IllegalArgumentException.class, () -> {
+ mEthernetServiceImpl.updateConfiguration(
+ "" /* iface */, request, null /* listener */);
+ });
+ }
+
+ @Test
+ public void testUpdateConfigurationRejectsWithInvalidSpecifierName() {
+ final String ifaceToUpdate = "eth0";
+ final String ifaceOnSpecifier = "wlan0";
+ EthernetNetworkUpdateRequest request =
+ new EthernetNetworkUpdateRequest.Builder()
+ .setNetworkCapabilities(
+ new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET)
+ .setNetworkSpecifier(
+ new EthernetNetworkSpecifier(ifaceOnSpecifier))
+ .build()
+ ).build();
+ assertThrows(IllegalArgumentException.class, () -> {
+ mEthernetServiceImpl.updateConfiguration(ifaceToUpdate, request, null /* listener */);
+ });
+ }
+
+ @Test
public void testUpdateConfigurationWithCapabilitiesWithAutomotiveFeature() {
toggleAutomotiveFeature(false);
mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_CAPABILITIES,
@@ -165,22 +212,6 @@
eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()), isNull());
}
- @Test
- public void testConnectNetworkRejectsWithoutAutomotiveFeature() {
- toggleAutomotiveFeature(false);
- assertThrows(UnsupportedOperationException.class, () -> {
- mEthernetServiceImpl.connectNetwork("" /* iface */, NULL_LISTENER);
- });
- }
-
- @Test
- public void testDisconnectNetworkRejectsWithoutAutomotiveFeature() {
- toggleAutomotiveFeature(false);
- assertThrows(UnsupportedOperationException.class, () -> {
- mEthernetServiceImpl.disconnectNetwork("" /* iface */, NULL_LISTENER);
- });
- }
-
private void denyManageEthPermission() {
doThrow(new SecurityException("")).when(mContext)
.enforceCallingOrSelfPermission(
@@ -202,18 +233,18 @@
}
@Test
- public void testConnectNetworkRejectsWithoutManageEthPermission() {
+ public void testEnableInterfaceRejectsWithoutManageEthPermission() {
denyManageEthPermission();
assertThrows(SecurityException.class, () -> {
- mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+ mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
});
}
@Test
- public void testDisconnectNetworkRejectsWithoutManageEthPermission() {
+ public void testDisableInterfaceRejectsWithoutManageEthPermission() {
denyManageEthPermission();
assertThrows(SecurityException.class, () -> {
- mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+ mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
});
}
@@ -231,20 +262,20 @@
}
@Test
- public void testConnectNetworkRejectsTestRequestWithoutTestPermission() {
+ public void testEnableInterfaceRejectsTestRequestWithoutTestPermission() {
enableTestInterface();
denyManageTestNetworksPermission();
assertThrows(SecurityException.class, () -> {
- mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+ mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
});
}
@Test
- public void testDisconnectNetworkRejectsTestRequestWithoutTestPermission() {
+ public void testDisableInterfaceRejectsTestRequestWithoutTestPermission() {
enableTestInterface();
denyManageTestNetworksPermission();
assertThrows(SecurityException.class, () -> {
- mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+ mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
});
}
@@ -258,15 +289,33 @@
}
@Test
- public void testConnectNetwork() {
- mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
- verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+ public void testUpdateConfigurationAddsSpecifierWhenNotSet() {
+ final NetworkCapabilities nc = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_ETHERNET).build();
+ final EthernetNetworkUpdateRequest requestSansSpecifier =
+ new EthernetNetworkUpdateRequest.Builder()
+ .setNetworkCapabilities(nc)
+ .build();
+ final NetworkCapabilities ncWithSpecifier = new NetworkCapabilities(nc)
+ .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE));
+
+ mEthernetServiceImpl.updateConfiguration(TEST_IFACE, requestSansSpecifier, NULL_LISTENER);
+ verify(mEthernetTracker).updateConfiguration(
+ eq(TEST_IFACE),
+ isNull(),
+ eq(ncWithSpecifier), eq(NULL_LISTENER));
}
@Test
- public void testDisconnectNetwork() {
- mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
- verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+ public void testEnableInterface() {
+ mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
+ verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
+ }
+
+ @Test
+ public void testDisableInterface() {
+ mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
+ verify(mEthernetTracker).disableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
}
@Test
@@ -324,23 +373,23 @@
}
@Test
- public void testConnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+ public void testEnableInterfaceForTestRequestDoesNotRequireNetPermission() {
enableTestInterface();
toggleAutomotiveFeature(false);
denyManageEthPermission();
- mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
- verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+ mEthernetServiceImpl.enableInterface(TEST_IFACE, NULL_LISTENER);
+ verify(mEthernetTracker).enableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
}
@Test
- public void testDisconnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+ public void testDisableInterfaceForTestRequestDoesNotRequireAutoOrNetPermission() {
enableTestInterface();
toggleAutomotiveFeature(false);
denyManageEthPermission();
- mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
- verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+ mEthernetServiceImpl.disableInterface(TEST_IFACE, NULL_LISTENER);
+ verify(mEthernetTracker).disableInterface(eq(TEST_IFACE), eq(NULL_LISTENER));
}
private void denyPermissions(String... permissions) {
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
index b1831c4..38094ae 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -28,47 +28,53 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
-import android.content.res.Resources;
import android.net.EthernetManager;
-import android.net.InetAddresses;
-import android.net.INetworkInterfaceOutcomeReceiver;
import android.net.IEthernetServiceListener;
import android.net.INetd;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
import android.net.IpConfiguration;
import android.net.IpConfiguration.IpAssignment;
import android.net.IpConfiguration.ProxySettings;
-import android.net.InterfaceConfigurationParcel;
import android.net.LinkAddress;
import android.net.NetworkCapabilities;
import android.net.StaticIpConfiguration;
+import android.os.Build;
import android.os.HandlerThread;
import android.os.RemoteException;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-import com.android.connectivity.resources.R;
+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;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.net.InetAddress;
import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class EthernetTrackerTest {
private static final String TEST_IFACE = "test123";
private static final int TIMEOUT_MS = 1_000;
@@ -351,8 +357,8 @@
}
@Test
- public void testConnectNetworkCorrectlyCallsFactory() {
- tracker.connectNetwork(TEST_IFACE, NULL_LISTENER);
+ public void testEnableInterfaceCorrectlyCallsFactory() {
+ tracker.enableInterface(TEST_IFACE, NULL_LISTENER);
waitForIdle();
verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */),
@@ -360,8 +366,8 @@
}
@Test
- public void testDisconnectNetworkCorrectlyCallsFactory() {
- tracker.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+ public void testDisableInterfaceCorrectlyCallsFactory() {
+ tracker.disableInterface(TEST_IFACE, NULL_LISTENER);
waitForIdle();
verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */),
@@ -410,22 +416,41 @@
IpConfiguration configuration) { }
}
+ private InterfaceConfigurationParcel createMockedIfaceParcel(final String ifname,
+ final String hwAddr) {
+ final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel();
+ ifaceParcel.ifName = ifname;
+ ifaceParcel.hwAddr = hwAddr;
+ ifaceParcel.flags = new String[] {INetd.IF_STATE_UP};
+ return ifaceParcel;
+ }
+
@Test
public void testListenEthernetStateChange() throws Exception {
- final String testIface = "testtap123";
- final String testHwAddr = "11:22:33:44:55:66";
- final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel();
- ifaceParcel.ifName = testIface;
- ifaceParcel.hwAddr = testHwAddr;
- ifaceParcel.flags = new String[] {INetd.IF_STATE_UP};
-
tracker.setIncludeTestInterfaces(true);
waitForIdle();
+ final String testIface = "testtap123";
+ final String testHwAddr = "11:22:33:44:55:66";
+ final InterfaceConfigurationParcel ifaceParcel = createMockedIfaceParcel(testIface,
+ testHwAddr);
when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(ifaceParcel);
doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
- doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface));
+
+ final AtomicBoolean ifaceUp = new AtomicBoolean(true);
+ doAnswer(inv -> ifaceUp.get()).when(mFactory).hasInterface(testIface);
+ doAnswer(inv ->
+ ifaceUp.get() ? EthernetManager.STATE_LINK_UP : EthernetManager.STATE_ABSENT)
+ .when(mFactory).getInterfaceState(testIface);
+ doAnswer(inv -> {
+ ifaceUp.set(true);
+ return null;
+ }).when(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any());
+ doAnswer(inv -> {
+ ifaceUp.set(false);
+ return null;
+ }).when(mFactory).removeInterface(testIface);
final EthernetStateListener listener = spy(new EthernetStateListener());
tracker.addListener(listener, true /* canUseRestrictedNetworks */);
@@ -436,7 +461,6 @@
verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED));
reset(listener);
- doReturn(EthernetManager.STATE_ABSENT).when(mFactory).getInterfaceState(eq(testIface));
tracker.setEthernetEnabled(false);
waitForIdle();
verify(mFactory).removeInterface(eq(testIface));
@@ -445,7 +469,6 @@
anyInt(), any());
reset(listener);
- doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface));
tracker.setEthernetEnabled(true);
waitForIdle();
verify(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any());
@@ -453,4 +476,43 @@
verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
anyInt(), any());
}
+
+ @Test
+ public void testListenEthernetStateChange_unsolicitedEventListener() throws Exception {
+ when(mNetd.interfaceGetList()).thenReturn(new String[] {});
+ doReturn(new String[] {}).when(mFactory).getAvailableInterfaces(anyBoolean());
+
+ tracker.setIncludeTestInterfaces(true);
+ tracker.start();
+
+ final ArgumentCaptor<EthernetTracker.InterfaceObserver> captor =
+ ArgumentCaptor.forClass(EthernetTracker.InterfaceObserver.class);
+ verify(mNetd, timeout(TIMEOUT_MS)).registerUnsolicitedEventListener(captor.capture());
+ final EthernetTracker.InterfaceObserver observer = captor.getValue();
+
+ tracker.setEthernetEnabled(false);
+ waitForIdle();
+ reset(mFactory);
+ reset(mNetd);
+
+ final String testIface = "testtap1";
+ observer.onInterfaceAdded(testIface);
+ verify(mFactory, never()).addInterface(eq(testIface), anyString(), any(), any());
+ observer.onInterfaceRemoved(testIface);
+ verify(mFactory, never()).removeInterface(eq(testIface));
+
+ final String testHwAddr = "11:22:33:44:55:66";
+ final InterfaceConfigurationParcel testIfaceParce =
+ createMockedIfaceParcel(testIface, testHwAddr);
+ when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
+ when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(testIfaceParce);
+ doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
+ tracker.setEthernetEnabled(true);
+ waitForIdle();
+ reset(mFactory);
+
+ final String testIface2 = "testtap2";
+ observer.onInterfaceRemoved(testIface2);
+ verify(mFactory, timeout(TIMEOUT_MS)).removeInterface(eq(testIface2));
+ }
}
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
index 987b7b7..c6852d1 100644
--- a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
+++ b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
@@ -24,16 +24,18 @@
import android.content.Context;
import android.net.INetd;
import android.net.MacAddress;
+import android.os.Build;
import android.os.Handler;
import android.os.test.TestLooper;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.Struct.U32;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
import org.junit.Test;
@@ -42,8 +44,9 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-@RunWith(AndroidJUnit4.class)
@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public final class BpfInterfaceMapUpdaterTest {
private static final int TEST_INDEX = 1;
private static final int TEST_INDEX2 = 2;
diff --git a/tests/unit/java/com/android/server/net/IpConfigStoreTest.java b/tests/unit/java/com/android/server/net/IpConfigStoreTest.java
index e9a5309..4adc999 100644
--- a/tests/unit/java/com/android/server/net/IpConfigStoreTest.java
+++ b/tests/unit/java/com/android/server/net/IpConfigStoreTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
+import android.content.Context;
import android.net.InetAddresses;
import android.net.IpConfiguration;
import android.net.IpConfiguration.IpAssignment;
@@ -27,9 +28,15 @@
import android.net.LinkAddress;
import android.net.ProxyInfo;
import android.net.StaticIpConfiguration;
+import android.os.Build;
+import android.os.HandlerThread;
import android.util.ArrayMap;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.InstrumentationRegistry;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -37,17 +44,21 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
/**
* Unit tests for {@link IpConfigStore}
*/
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class IpConfigStoreTest {
+ private static final int TIMEOUT_MS = 2_000;
private static final int KEY_CONFIG = 17;
private static final String IFACE_1 = "eth0";
private static final String IFACE_2 = "eth1";
@@ -56,6 +67,22 @@
private static final String DNS_IP_ADDR_1 = "1.2.3.4";
private static final String DNS_IP_ADDR_2 = "5.6.7.8";
+ private static final ArrayList<InetAddress> DNS_SERVERS = new ArrayList<>(List.of(
+ InetAddresses.parseNumericAddress(DNS_IP_ADDR_1),
+ InetAddresses.parseNumericAddress(DNS_IP_ADDR_2)));
+ private static final StaticIpConfiguration STATIC_IP_CONFIG_1 =
+ new StaticIpConfiguration.Builder()
+ .setIpAddress(new LinkAddress(IP_ADDR_1))
+ .setDnsServers(DNS_SERVERS)
+ .build();
+ private static final StaticIpConfiguration STATIC_IP_CONFIG_2 =
+ new StaticIpConfiguration.Builder()
+ .setIpAddress(new LinkAddress(IP_ADDR_2))
+ .setDnsServers(DNS_SERVERS)
+ .build();
+ private static final ProxyInfo PROXY_INFO =
+ ProxyInfo.buildDirectProxy("10.10.10.10", 88, Arrays.asList("host1", "host2"));
+
@Test
public void backwardCompatibility2to3() throws IOException {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
@@ -79,40 +106,73 @@
@Test
public void staticIpMultiNetworks() throws Exception {
- final ArrayList<InetAddress> dnsServers = new ArrayList<>();
- dnsServers.add(InetAddresses.parseNumericAddress(DNS_IP_ADDR_1));
- dnsServers.add(InetAddresses.parseNumericAddress(DNS_IP_ADDR_2));
- final StaticIpConfiguration staticIpConfiguration1 = new StaticIpConfiguration.Builder()
- .setIpAddress(new LinkAddress(IP_ADDR_1))
- .setDnsServers(dnsServers).build();
- final StaticIpConfiguration staticIpConfiguration2 = new StaticIpConfiguration.Builder()
- .setIpAddress(new LinkAddress(IP_ADDR_2))
- .setDnsServers(dnsServers).build();
+ final IpConfiguration expectedConfig1 = newIpConfiguration(IpAssignment.STATIC,
+ ProxySettings.STATIC, STATIC_IP_CONFIG_1, PROXY_INFO);
+ final IpConfiguration expectedConfig2 = newIpConfiguration(IpAssignment.STATIC,
+ ProxySettings.STATIC, STATIC_IP_CONFIG_2, PROXY_INFO);
- ProxyInfo proxyInfo =
- ProxyInfo.buildDirectProxy("10.10.10.10", 88, Arrays.asList("host1", "host2"));
-
- IpConfiguration expectedConfig1 = newIpConfiguration(IpAssignment.STATIC,
- ProxySettings.STATIC, staticIpConfiguration1, proxyInfo);
- IpConfiguration expectedConfig2 = newIpConfiguration(IpAssignment.STATIC,
- ProxySettings.STATIC, staticIpConfiguration2, proxyInfo);
-
- ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
+ final ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
expectedNetworks.put(IFACE_1, expectedConfig1);
expectedNetworks.put(IFACE_2, expectedConfig2);
- MockedDelayedDiskWrite writer = new MockedDelayedDiskWrite();
- IpConfigStore store = new IpConfigStore(writer);
+ final MockedDelayedDiskWrite writer = new MockedDelayedDiskWrite();
+ final IpConfigStore store = new IpConfigStore(writer);
store.writeIpConfigurations("file/path/not/used/", expectedNetworks);
- InputStream in = new ByteArrayInputStream(writer.mByteStream.toByteArray());
- ArrayMap<String, IpConfiguration> actualNetworks = IpConfigStore.readIpConfigurations(in);
+ final InputStream in = new ByteArrayInputStream(writer.mByteStream.toByteArray());
+ final ArrayMap<String, IpConfiguration> actualNetworks =
+ IpConfigStore.readIpConfigurations(in);
assertNotNull(actualNetworks);
assertEquals(2, actualNetworks.size());
assertEquals(expectedNetworks.get(IFACE_1), actualNetworks.get(IFACE_1));
assertEquals(expectedNetworks.get(IFACE_2), actualNetworks.get(IFACE_2));
}
+ @Test
+ public void readIpConfigurationFromFilePath() throws Exception {
+ final HandlerThread testHandlerThread = new HandlerThread("IpConfigStoreTest");
+ final DelayedDiskWrite.Dependencies dependencies =
+ new DelayedDiskWrite.Dependencies() {
+ @Override
+ public HandlerThread makeHandlerThread() {
+ return testHandlerThread;
+ }
+ @Override
+ public void quitHandlerThread(HandlerThread handlerThread) {
+ testHandlerThread.quitSafely();
+ }
+ };
+
+ final IpConfiguration ipConfig = newIpConfiguration(IpAssignment.STATIC,
+ ProxySettings.STATIC, STATIC_IP_CONFIG_1, PROXY_INFO);
+ final ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
+ expectedNetworks.put(IFACE_1, ipConfig);
+
+ // Write IP config to specific file path and read it later.
+ final Context context = InstrumentationRegistry.getContext();
+ final File configFile = new File(context.getFilesDir().getPath(),
+ "IpConfigStoreTest-ipconfig.txt");
+ final DelayedDiskWrite writer = new DelayedDiskWrite(dependencies);
+ final IpConfigStore store = new IpConfigStore(writer);
+ store.writeIpConfigurations(configFile.getPath(), expectedNetworks);
+ HandlerUtils.waitForIdle(testHandlerThread, TIMEOUT_MS);
+
+ // Read IP config from the file path.
+ final ArrayMap<String, IpConfiguration> actualNetworks =
+ IpConfigStore.readIpConfigurations(configFile.getPath());
+ assertNotNull(actualNetworks);
+ assertEquals(1, actualNetworks.size());
+ assertEquals(expectedNetworks.get(IFACE_1), actualNetworks.get(IFACE_1));
+
+ // Return an empty array when reading IP configuration from an non-exist config file.
+ final ArrayMap<String, IpConfiguration> emptyNetworks =
+ IpConfigStore.readIpConfigurations("/dev/null");
+ assertNotNull(emptyNetworks);
+ assertEquals(0, emptyNetworks.size());
+
+ configFile.delete();
+ }
+
private IpConfiguration newIpConfiguration(IpAssignment ipAssignment,
ProxySettings proxySettings, StaticIpConfiguration staticIpConfig, ProxyInfo info) {
final IpConfiguration config = new IpConfiguration();
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
index 79744b1..f6fb45c 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -29,6 +29,7 @@
import static android.net.NetworkStats.UID_ALL;
import static com.android.server.net.NetworkStatsFactory.kernelToTag;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@@ -38,12 +39,12 @@
import android.net.NetworkStats;
import android.net.TrafficStats;
import android.net.UnderlyingNetworkInfo;
-import android.os.Build;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import com.android.frameworks.tests.net.R;
+import com.android.server.BpfNetMaps;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -67,13 +68,14 @@
/** Tests for {@link NetworkStatsFactory}. */
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class NetworkStatsFactoryTest extends NetworkStatsBaseTest {
private static final String CLAT_PREFIX = "v4-";
private File mTestProc;
private NetworkStatsFactory mFactory;
@Mock private Context mContext;
+ @Mock private BpfNetMaps mBpfNetMaps;
@Before
public void setUp() throws Exception {
@@ -84,7 +86,7 @@
// applications. So in order to have a test support native library, the native code
// related to networkStatsFactory is compiled to a minimal native library and loaded here.
System.loadLibrary("networkstatsfactorytestjni");
- mFactory = new NetworkStatsFactory(mContext, mTestProc, false);
+ mFactory = new NetworkStatsFactory(mContext, mTestProc, false, mBpfNetMaps);
mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]);
}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 5f9d1ff..5747e10 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -32,6 +32,7 @@
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
@@ -64,6 +65,7 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
import java.util.Objects;
/**
@@ -86,11 +88,19 @@
private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1);
private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2);
+ private static final int PID_SYSTEM = 1234;
+ private static final int PID_RED = 1235;
+ private static final int PID_BLUE = 1236;
+
private static final int UID_RED = UserHandle.PER_USER_RANGE + 1;
private static final int UID_BLUE = UserHandle.PER_USER_RANGE + 2;
private static final int UID_GREEN = UserHandle.PER_USER_RANGE + 3;
private static final int UID_ANOTHER_USER = 2 * UserHandle.PER_USER_RANGE + 4;
+ private static final String PACKAGE_SYSTEM = "android";
+ private static final String PACKAGE_RED = "RED";
+ private static final String PACKAGE_BLUE = "BLUE";
+
private static final long WAIT_TIMEOUT_MS = 500;
private static final long THRESHOLD_BYTES = 2 * MB_IN_BYTES;
private static final long BASE_BYTES = 7 * MB_IN_BYTES;
@@ -131,14 +141,15 @@
DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdTooLowBytes);
final DataUsageRequest requestByApp = mStatsObservers.register(mContext, inputRequest,
- mUsageCallback, UID_RED, NetworkStatsAccess.Level.DEVICE);
+ mUsageCallback, PID_RED , UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE);
assertTrue(requestByApp.requestId > 0);
assertTrue(Objects.equals(sTemplateWifi, requestByApp.template));
assertEquals(thresholdTooLowBytes, requestByApp.thresholdInBytes);
// Verify the threshold requested by system uid won't be overridden.
final DataUsageRequest requestBySystem = mStatsObservers.register(mContext, inputRequest,
- mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+ mUsageCallback, PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM,
+ NetworkStatsAccess.Level.DEVICE);
assertTrue(requestBySystem.requestId > 0);
assertTrue(Objects.equals(sTemplateWifi, requestBySystem.template));
assertEquals(1, requestBySystem.thresholdInBytes);
@@ -151,7 +162,7 @@
DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, highThresholdBytes);
DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
- Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+ PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateWifi, request.template));
assertEquals(highThresholdBytes, request.thresholdInBytes);
@@ -163,19 +174,64 @@
DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
DataUsageRequest request1 = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
- Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+ PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
assertTrue(request1.requestId > 0);
assertTrue(Objects.equals(sTemplateWifi, request1.template));
assertEquals(THRESHOLD_BYTES, request1.thresholdInBytes);
DataUsageRequest request2 = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
- Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+ PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
assertTrue(request2.requestId > request1.requestId);
assertTrue(Objects.equals(sTemplateWifi, request2.template));
assertEquals(THRESHOLD_BYTES, request2.thresholdInBytes);
}
@Test
+ public void testRegister_limit() throws Exception {
+ final DataUsageRequest inputRequest = new DataUsageRequest(
+ DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
+
+ // Register maximum requests for red.
+ final ArrayList<DataUsageRequest> redRequests = new ArrayList<>();
+ for (int i = 0; i < NetworkStatsObservers.MAX_REQUESTS_PER_UID; i++) {
+ final DataUsageRequest returnedRequest =
+ mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+ PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE);
+ redRequests.add(returnedRequest);
+ assertTrue(returnedRequest.requestId > 0);
+ }
+
+ // Verify request exceeds the limit throws.
+ assertThrows(IllegalStateException.class, () ->
+ mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+ PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE));
+
+ // Verify another uid is not affected.
+ final ArrayList<DataUsageRequest> blueRequests = new ArrayList<>();
+ for (int i = 0; i < NetworkStatsObservers.MAX_REQUESTS_PER_UID; i++) {
+ final DataUsageRequest returnedRequest =
+ mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+ PID_BLUE, UID_BLUE, PACKAGE_BLUE, NetworkStatsAccess.Level.DEVICE);
+ blueRequests.add(returnedRequest);
+ assertTrue(returnedRequest.requestId > 0);
+ }
+
+ // Again, verify request exceeds the limit throws for the 2nd uid.
+ assertThrows(IllegalStateException.class, () ->
+ mStatsObservers.register(mContext, inputRequest, mUsageCallback,
+ PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE));
+
+ // Unregister all registered requests. Note that exceptions cannot be tested since
+ // unregister is handled in the handler thread.
+ for (final DataUsageRequest request : redRequests) {
+ mStatsObservers.unregister(request, UID_RED);
+ }
+ for (final DataUsageRequest request : blueRequests) {
+ mStatsObservers.unregister(request, UID_BLUE);
+ }
+ }
+
+ @Test
public void testUnregister_unknownRequest_noop() throws Exception {
DataUsageRequest unknownRequest = new DataUsageRequest(
123456 /* id */, sTemplateWifi, THRESHOLD_BYTES);
@@ -189,7 +245,7 @@
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
- Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+ PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -209,7 +265,7 @@
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
- UID_RED, NetworkStatsAccess.Level.DEVICE);
+ PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.DEVICE);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -218,8 +274,12 @@
mStatsObservers.unregister(request, UID_BLUE);
waitForObserverToIdle();
-
Mockito.verifyZeroInteractions(mUsageCallbackBinder);
+
+ // Verify that system uid can unregister for other uids.
+ mStatsObservers.unregister(request, Process.SYSTEM_UID);
+ waitForObserverToIdle();
+ mUsageCallback.expectOnCallbackReleased(request);
}
private NetworkIdentitySet makeTestIdentSet() {
@@ -237,7 +297,7 @@
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
- Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+ PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -261,7 +321,7 @@
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
- Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+ PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -291,7 +351,7 @@
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
- Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+ PID_SYSTEM, Process.SYSTEM_UID, PACKAGE_SYSTEM, NetworkStatsAccess.Level.DEVICE);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -322,7 +382,7 @@
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
- UID_RED, NetworkStatsAccess.Level.DEFAULT);
+ PID_RED, UID_RED, PACKAGE_SYSTEM , NetworkStatsAccess.Level.DEFAULT);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -355,7 +415,7 @@
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
- UID_BLUE, NetworkStatsAccess.Level.DEFAULT);
+ PID_BLUE, UID_BLUE, PACKAGE_BLUE, NetworkStatsAccess.Level.DEFAULT);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -387,7 +447,7 @@
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
- UID_BLUE, NetworkStatsAccess.Level.USER);
+ PID_BLUE, UID_BLUE, PACKAGE_BLUE, NetworkStatsAccess.Level.USER);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
@@ -420,7 +480,7 @@
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
DataUsageRequest request = mStatsObservers.register(mContext, inputRequest, mUsageCallback,
- UID_RED, NetworkStatsAccess.Level.USER);
+ PID_RED, UID_RED, PACKAGE_RED, NetworkStatsAccess.Level.USER);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index ceeb997..f9cbb10 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
+import static android.app.usage.NetworkStatsManager.PREFIX_DEV;
import static android.content.Intent.ACTION_UID_REMOVED;
import static android.content.Intent.EXTRA_UID;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
@@ -56,6 +57,9 @@
import static android.net.TrafficStats.MB_IN_BYTES;
import static android.net.TrafficStats.UID_REMOVED;
import static android.net.TrafficStats.UID_TETHERING;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
@@ -63,6 +67,9 @@
import static com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT;
import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
+import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME;
+import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME;
+import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
@@ -77,6 +84,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
@@ -87,15 +95,19 @@
import android.app.AlarmManager;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Resources;
import android.database.ContentObserver;
+import android.net.ConnectivityResources;
import android.net.DataUsageRequest;
import android.net.INetd;
import android.net.INetworkStatsSession;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
+import android.net.NetworkIdentity;
import android.net.NetworkStateSnapshot;
import android.net.NetworkStats;
+import android.net.NetworkStatsCollection;
import android.net.NetworkStatsHistory;
import android.net.NetworkTemplate;
import android.net.TelephonyNetworkSpecifier;
@@ -104,6 +116,7 @@
import android.net.UnderlyingNetworkInfo;
import android.net.netstats.provider.INetworkStatsProviderCallback;
import android.net.wifi.WifiInfo;
+import android.os.DropBoxManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
@@ -112,11 +125,14 @@
import android.provider.Settings;
import android.system.ErrnoException;
import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
+import com.android.connectivity.resources.R;
+import com.android.internal.util.FileRotator;
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.LocationPermissionChecker;
@@ -143,9 +159,17 @@
import org.mockito.MockitoAnnotations;
import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.time.Clock;
+import java.time.ZoneId;
import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -187,6 +211,7 @@
private long mElapsedRealtime;
private File mStatsDir;
+ private File mLegacyStatsDir;
private MockContext mServiceContext;
private @Mock TelephonyManager mTelephonyManager;
private static @Mock WifiInfo sWifiInfo;
@@ -220,6 +245,15 @@
private ContentObserver mContentObserver;
private Handler mHandler;
private TetheringManager.TetheringEventCallback mTetheringEventCallback;
+ private Map<String, NetworkStatsCollection> mPlatformNetworkStatsCollection =
+ new ArrayMap<String, NetworkStatsCollection>();
+ private boolean mStoreFilesInApexData = false;
+ private int mImportLegacyTargetAttempts = 0;
+ private @Mock PersistentInt mImportLegacyAttemptsCounter;
+ private @Mock PersistentInt mImportLegacySuccessesCounter;
+ private @Mock PersistentInt mImportLegacyFallbacksCounter;
+ private @Mock Resources mResources;
+ private Boolean mIsDebuggable;
private class MockContext extends BroadcastInterceptingContext {
private final Context mBaseContext;
@@ -280,12 +314,20 @@
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+
+ // Setup mock resources.
+ final Context mockResContext = mock(Context.class);
+ doReturn(mResources).when(mockResContext).getResources();
+ ConnectivityResources.setResourcesContextForTest(mockResContext);
+
final Context context = InstrumentationRegistry.getContext();
mServiceContext = new MockContext(context);
when(mLocationPermissionChecker.checkCallersLocationPermission(
any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true);
when(sWifiInfo.getNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY);
mStatsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
+ mLegacyStatsDir = TestIoUtils.createTemporaryDirectory(
+ getClass().getSimpleName() + "-legacy");
PowerManager powerManager = (PowerManager) mServiceContext.getSystemService(
Context.POWER_SERVICE);
@@ -295,8 +337,7 @@
mHandlerThread = new HandlerThread("HandlerThread");
final NetworkStatsService.Dependencies deps = makeDependencies();
mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
- mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), mStatsDir,
- getBaseDir(mStatsDir), deps);
+ mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), deps);
mElapsedRealtime = 0L;
@@ -339,6 +380,47 @@
private NetworkStatsService.Dependencies makeDependencies() {
return new NetworkStatsService.Dependencies() {
@Override
+ public File getLegacyStatsDir() {
+ return mLegacyStatsDir;
+ }
+
+ @Override
+ public File getOrCreateStatsDir() {
+ return mStatsDir;
+ }
+
+ @Override
+ public boolean getStoreFilesInApexData() {
+ return mStoreFilesInApexData;
+ }
+
+ @Override
+ public int getImportLegacyTargetAttempts() {
+ return mImportLegacyTargetAttempts;
+ }
+
+ @Override
+ public PersistentInt createPersistentCounter(@androidx.annotation.NonNull Path dir,
+ @androidx.annotation.NonNull String name) throws IOException {
+ switch (name) {
+ case NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME:
+ return mImportLegacyAttemptsCounter;
+ case NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME:
+ return mImportLegacySuccessesCounter;
+ case NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME:
+ return mImportLegacyFallbacksCounter;
+ default:
+ throw new IllegalArgumentException("Unknown counter name: " + name);
+ }
+ }
+
+ @Override
+ public NetworkStatsCollection readPlatformCollection(
+ @NonNull String prefix, long bucketDuration) {
+ return mPlatformNetworkStatsCollection.get(prefix);
+ }
+
+ @Override
public HandlerThread makeHandlerThread() {
return mHandlerThread;
}
@@ -393,6 +475,11 @@
public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
return mAppUidStatsMap;
}
+
+ @Override
+ public boolean isDebuggable() {
+ return mIsDebuggable == Boolean.TRUE;
+ }
};
}
@@ -1047,6 +1134,40 @@
}
@Test
+ public void testGetLatestSummary() throws Exception {
+ // Pretend that network comes online.
+ expectDefaultSettings();
+ NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{buildWifiState()};
+ expectNetworkStatsSummary(buildEmptyStats());
+ expectNetworkStatsUidDetail(buildEmptyStats());
+
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+ new UnderlyingNetworkInfo[0]);
+
+ // Increase arbitrary time which does not align to the bucket edge, create some traffic.
+ incrementCurrentTime(1751000L);
+ NetworkStats.Entry entry = new NetworkStats.Entry(
+ TEST_IFACE, UID_ALL, SET_DEFAULT, TAG_NONE, 50L, 5L, 51L, 1L, 3L);
+ expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1).insertEntry(entry));
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ forcePollAndWaitForIdle();
+
+ // Verify the mocked stats is returned by querying with the range of the latest bucket.
+ final ZonedDateTime end =
+ ZonedDateTime.ofInstant(mClock.instant(), ZoneId.systemDefault());
+ final ZonedDateTime start = end.truncatedTo(ChronoUnit.HOURS);
+ NetworkStats stats = mSession.getSummaryForNetwork(buildTemplateWifi(TEST_WIFI_NETWORK_KEY),
+ start.toInstant().toEpochMilli(), end.toInstant().toEpochMilli());
+ assertEquals(1, stats.size());
+ assertValues(stats, IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL, 50L, 5L, 51L, 1L, 3L);
+
+ // For getHistoryIntervalForNetwork, only includes buckets that atomically occur in
+ // the inclusive time range, instead of including the latest bucket. This behavior is
+ // already documented publicly, refer to {@link NetworkStatsManager#queryDetails}.
+ }
+
+ @Test
public void testUidStatsForTransport() throws Exception {
// pretend that network comes online
expectDefaultSettings();
@@ -1077,9 +1198,12 @@
assertEquals(3, stats.size());
entry1.operations = 1;
+ entry1.iface = null;
assertEquals(entry1, stats.getValues(0, null));
entry2.operations = 1;
+ entry2.iface = null;
assertEquals(entry2, stats.getValues(1, null));
+ entry3.iface = null;
assertEquals(entry3, stats.getValues(2, null));
}
@@ -1704,10 +1828,216 @@
assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
}
- private static File getBaseDir(File statsDir) {
- File baseDir = new File(statsDir, "netstats");
- baseDir.mkdirs();
- return baseDir;
+ /**
+ * Verify the service will perform data migration process can be controlled by the device flag.
+ */
+ @Test
+ public void testDataMigration() throws Exception {
+ assertStatsFilesExist(false);
+ expectDefaultSettings();
+
+ NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+ new UnderlyingNetworkInfo[0]);
+
+ // modify some number on wifi, and trigger poll event
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ // expectDefaultSettings();
+ expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+ .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+ .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
+ .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
+ .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
+ .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
+ .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
+
+ mService.noteUidForeground(UID_RED, false);
+ verify(mUidCounterSetMap, never()).deleteEntry(any());
+ mService.incrementOperationCount(UID_RED, 0xFAAD, 4);
+ mService.noteUidForeground(UID_RED, true);
+ verify(mUidCounterSetMap).updateEntry(
+ eq(new U32(UID_RED)), eq(new U8((short) SET_FOREGROUND)));
+ mService.incrementOperationCount(UID_RED, 0xFAAD, 6);
+
+ forcePollAndWaitForIdle();
+ // Simulate shutdown to force persisting data
+ mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+ assertStatsFilesExist(true);
+
+ // Move the files to the legacy directory to simulate an import from old data
+ for (File f : mStatsDir.listFiles()) {
+ Files.move(f.toPath(), mLegacyStatsDir.toPath().resolve(f.getName()));
+ }
+ assertStatsFilesExist(false);
+
+ // Fetch the stats from the legacy files and set platform stats collection to be identical
+ mPlatformNetworkStatsCollection.put(PREFIX_DEV,
+ getLegacyCollection(PREFIX_DEV, false /* includeTags */));
+ mPlatformNetworkStatsCollection.put(PREFIX_XT,
+ getLegacyCollection(PREFIX_XT, false /* includeTags */));
+ mPlatformNetworkStatsCollection.put(PREFIX_UID,
+ getLegacyCollection(PREFIX_UID, false /* includeTags */));
+ mPlatformNetworkStatsCollection.put(PREFIX_UID_TAG,
+ getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
+
+ // Mock zero usage and boot through serviceReady(), verify there is no imported data.
+ expectDefaultSettings();
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectSystemReady();
+ mService.systemReady();
+ assertStatsFilesExist(false);
+
+ // Set the flag and reboot, verify the imported data is not there until next boot.
+ mStoreFilesInApexData = true;
+ mImportLegacyTargetAttempts = 3;
+ mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+ assertStatsFilesExist(false);
+
+ // Boot through systemReady() again.
+ expectDefaultSettings();
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectSystemReady();
+ mService.systemReady();
+
+ // After systemReady(), the service should have historical stats loaded again.
+ // Thus, verify
+ // 1. The stats are absorbed by the recorder.
+ // 2. The imported data are persisted.
+ // 3. The attempts count is set to target attempts count to indicate a successful
+ // migration.
+ assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0);
+ assertStatsFilesExist(true);
+ verify(mImportLegacyAttemptsCounter).set(3);
+ verify(mImportLegacySuccessesCounter).set(1);
+
+ // TODO: Verify upgrading with Exception won't damege original data and
+ // will decrease the retry counter by 1.
+ }
+
+ @Test
+ public void testDataMigration_differentFromFallback() throws Exception {
+ assertStatsFilesExist(false);
+ expectDefaultSettings();
+
+ NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{buildWifiState()};
+
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+ new UnderlyingNetworkInfo[0]);
+
+ // modify some number on wifi, and trigger poll event
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+ .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+ .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
+ forcePollAndWaitForIdle();
+ // Simulate shutdown to force persisting data
+ mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+ assertStatsFilesExist(true);
+
+ // Move the files to the legacy directory to simulate an import from old data
+ for (File f : mStatsDir.listFiles()) {
+ Files.move(f.toPath(), mLegacyStatsDir.toPath().resolve(f.getName()));
+ }
+ assertStatsFilesExist(false);
+
+ // Prepare some unexpected data.
+ final NetworkIdentity testWifiIdent = new NetworkIdentity.Builder().setType(TYPE_WIFI)
+ .setWifiNetworkKey(TEST_WIFI_NETWORK_KEY).build();
+ final NetworkStatsCollection.Key unexpectedUidAllkey = new NetworkStatsCollection.Key(
+ Set.of(testWifiIdent), UID_ALL, SET_DEFAULT, 0);
+ final NetworkStatsCollection.Key unexpectedUidBluekey = new NetworkStatsCollection.Key(
+ Set.of(testWifiIdent), UID_BLUE, SET_DEFAULT, 0);
+ final NetworkStatsHistory unexpectedHistory = new NetworkStatsHistory
+ .Builder(965L /* bucketDuration */, 1)
+ .addEntry(new NetworkStatsHistory.Entry(TEST_START, 3L, 55L, 4L, 31L, 10L, 5L))
+ .build();
+
+ // Simulate the platform stats collection somehow is different from what is read from
+ // the fallback method. The service should read them as is. This usually happens when an
+ // OEM has changed the implementation of NetworkStatsDataMigrationUtils inside the platform.
+ final NetworkStatsCollection summaryCollection =
+ getLegacyCollection(PREFIX_XT, false /* includeTags */);
+ summaryCollection.recordHistory(unexpectedUidAllkey, unexpectedHistory);
+ final NetworkStatsCollection uidCollection =
+ getLegacyCollection(PREFIX_UID, false /* includeTags */);
+ uidCollection.recordHistory(unexpectedUidBluekey, unexpectedHistory);
+ mPlatformNetworkStatsCollection.put(PREFIX_DEV, summaryCollection);
+ mPlatformNetworkStatsCollection.put(PREFIX_XT, summaryCollection);
+ mPlatformNetworkStatsCollection.put(PREFIX_UID, uidCollection);
+ mPlatformNetworkStatsCollection.put(PREFIX_UID_TAG,
+ getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
+
+ // Mock zero usage and boot through serviceReady(), verify there is no imported data.
+ expectDefaultSettings();
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectSystemReady();
+ mService.systemReady();
+ assertStatsFilesExist(false);
+
+ // Set the flag and reboot, verify the imported data is not there until next boot.
+ mStoreFilesInApexData = true;
+ mImportLegacyTargetAttempts = 3;
+ mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+ assertStatsFilesExist(false);
+
+ // Boot through systemReady() again.
+ expectDefaultSettings();
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectSystemReady();
+ mService.systemReady();
+
+ // Verify the result read from public API matches the result returned from the importer.
+ assertNetworkTotal(sTemplateWifi, 1024L + 55L, 8L + 4L, 2048L + 31L, 16L + 10L, 0 + 5);
+ assertUidTotal(sTemplateWifi, UID_BLUE,
+ 128L + 55L, 1L + 4L, 128L + 31L, 1L + 10L, 0 + 5);
+ assertStatsFilesExist(true);
+ verify(mImportLegacyAttemptsCounter).set(3);
+ verify(mImportLegacySuccessesCounter).set(1);
+ }
+
+ @Test
+ public void testShouldRunComparison() {
+ for (Boolean isDebuggable : Set.of(Boolean.TRUE, Boolean.FALSE)) {
+ mIsDebuggable = isDebuggable;
+ // Verify return false regardless of the device is debuggable.
+ doReturn(0).when(mResources)
+ .getInteger(R.integer.config_netstats_validate_import);
+ assertShouldRunComparison(false, isDebuggable);
+ // Verify return true regardless of the device is debuggable.
+ doReturn(1).when(mResources)
+ .getInteger(R.integer.config_netstats_validate_import);
+ assertShouldRunComparison(true, isDebuggable);
+ // Verify return true iff the device is debuggable.
+ for (int testValue : Set.of(-1, 2)) {
+ doReturn(testValue).when(mResources)
+ .getInteger(R.integer.config_netstats_validate_import);
+ assertShouldRunComparison(isDebuggable, isDebuggable);
+ }
+ }
+ }
+
+ private void assertShouldRunComparison(boolean expected, boolean isDebuggable) {
+ assertEquals("shouldRunComparison (debuggable=" + isDebuggable + "): ",
+ expected, mService.shouldRunComparison());
+ }
+
+ private NetworkStatsRecorder makeTestRecorder(File directory, String prefix, Config config,
+ boolean includeTags, boolean wipeOnError) {
+ final NetworkStats.NonMonotonicObserver observer =
+ mock(NetworkStats.NonMonotonicObserver.class);
+ final DropBoxManager dropBox = mock(DropBoxManager.class);
+ return new NetworkStatsRecorder(new FileRotator(
+ directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
+ observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError);
+ }
+
+ private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
+ final NetworkStatsRecorder recorder = makeTestRecorder(mLegacyStatsDir, prefix,
+ mSettings.getDevConfig(), includeTags, false);
+ return recorder.getOrLoadCompleteLocked();
}
private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets,
@@ -1816,11 +2146,10 @@
}
private void assertStatsFilesExist(boolean exist) {
- final File basePath = new File(mStatsDir, "netstats");
if (exist) {
- assertTrue(basePath.list().length > 0);
+ assertTrue(mStatsDir.list().length > 0);
} else {
- assertTrue(basePath.list().length == 0);
+ assertTrue(mStatsDir.list().length == 0);
}
}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
index 0d34609..622f2be 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
@@ -19,6 +19,8 @@
import static android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE;
import static android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
@@ -37,7 +39,6 @@
import android.annotation.Nullable;
import android.app.usage.NetworkStatsManager;
import android.content.Context;
-import android.os.Build;
import android.os.Looper;
import android.os.Parcel;
import android.telephony.SubscriptionManager;
@@ -63,7 +64,7 @@
import java.util.concurrent.Executors;
@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public final class NetworkStatsSubscriptionsMonitorTest {
private static final int TEST_SUBID1 = 3;
private static final int TEST_SUBID2 = 5;
diff --git a/tests/unit/java/com/android/server/net/PersistentIntTest.kt b/tests/unit/java/com/android/server/net/PersistentIntTest.kt
new file mode 100644
index 0000000..9268352
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/PersistentIntTest.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.util.SystemConfigFileCommitEventLogger
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.SC_V2
+import com.android.testutils.assertThrows
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.attribute.PosixFilePermission
+import java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE
+import java.nio.file.attribute.PosixFilePermission.OWNER_READ
+import java.nio.file.attribute.PosixFilePermission.OWNER_WRITE
+import java.util.Random
+import kotlin.test.assertEquals
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(SC_V2)
+class PersistentIntTest {
+ val tempFilesCreated = mutableSetOf<Path>()
+ lateinit var tempDir: Path
+
+ @Before
+ fun setUp() {
+ tempDir = Files.createTempDirectory("tmp.PersistentIntTest.")
+ }
+
+ @After
+ fun tearDown() {
+ var permissions = setOf(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE)
+ Files.setPosixFilePermissions(tempDir, permissions)
+
+ for (file in tempFilesCreated) {
+ Files.deleteIfExists(file)
+ }
+ Files.delete(tempDir)
+ }
+
+ @Test
+ fun testNormalReadWrite() {
+ // New, initialized to 0.
+ val pi = createPersistentInt()
+ assertEquals(0, pi.get())
+ pi.set(12345)
+ assertEquals(12345, pi.get())
+
+ // Existing.
+ val pi2 = createPersistentInt(pathOf(pi))
+ assertEquals(12345, pi2.get())
+ }
+
+ @Test
+ fun testReadOrWriteFailsInCreate() {
+ setWritable(tempDir, false)
+ assertThrows(IOException::class.java) {
+ createPersistentInt()
+ }
+ }
+
+ @Test
+ fun testReadOrWriteFailsAfterCreate() {
+ val pi = createPersistentInt()
+ pi.set(42)
+ assertEquals(42, pi.get())
+
+ val path = pathOf(pi)
+ setReadable(path, false)
+ assertThrows(IOException::class.java) { pi.get() }
+ pi.set(77)
+
+ setReadable(path, true)
+ setWritable(path, false)
+ setWritable(tempDir, false) // Writing creates a new file+renames, make this fail.
+ assertThrows(IOException::class.java) { pi.set(99) }
+ assertEquals(77, pi.get())
+ }
+
+ fun addOrRemovePermission(p: Path, permission: PosixFilePermission, add: Boolean) {
+ val permissions = Files.getPosixFilePermissions(p)
+ if (add) {
+ permissions.add(permission)
+ } else {
+ permissions.remove(permission)
+ }
+ Files.setPosixFilePermissions(p, permissions)
+ }
+
+ fun setReadable(p: Path, readable: Boolean) {
+ addOrRemovePermission(p, OWNER_READ, readable)
+ }
+
+ fun setWritable(p: Path, writable: Boolean) {
+ addOrRemovePermission(p, OWNER_WRITE, writable)
+ }
+
+ fun pathOf(pi: PersistentInt): Path {
+ return File(pi.path).toPath()
+ }
+
+ fun createPersistentInt(path: Path = randomTempPath()): PersistentInt {
+ tempFilesCreated.add(path)
+ return PersistentInt(path.toString(),
+ SystemConfigFileCommitEventLogger("PersistentIntTest"))
+ }
+
+ fun randomTempPath(): Path {
+ return tempDir.resolve(Integer.toHexString(Random().nextInt())).also {
+ tempFilesCreated.add(it)
+ }
+ }
+}
diff --git a/tools/Android.bp b/tools/Android.bp
new file mode 100644
index 0000000..1fa93bb
--- /dev/null
+++ b/tools/Android.bp
@@ -0,0 +1,91 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Build tool used to generate jarjar rules for all classes in a jar, except those that are
+// API, UnsupportedAppUsage or otherwise excluded.
+python_binary_host {
+ name: "jarjar-rules-generator",
+ srcs: [
+ "gen_jarjar.py",
+ ],
+ main: "gen_jarjar.py",
+ version: {
+ py2: {
+ enabled: false,
+ },
+ py3: {
+ enabled: true,
+ },
+ },
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+genrule_defaults {
+ name: "jarjar-rules-combine-defaults",
+ // Concat files with a line break in the middle
+ cmd: "for src in $(in); do cat $${src}; echo; done > $(out)",
+ defaults_visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+java_library {
+ name: "jarjar-rules-generator-testjavalib",
+ srcs: ["testdata/java/**/*.java"],
+ visibility: ["//visibility:private"],
+}
+
+// TODO(b/233723405) - Remove this workaround.
+// Temporary work around of b/233723405. Using the module_lib stub directly
+// in the test causes it to sometimes get the dex jar and sometimes get the
+// classes jar due to b/233111644. Statically including it here instead
+// ensures that it will always get the classes jar.
+java_library {
+ name: "framework-connectivity.stubs.module_lib-for-test",
+ visibility: ["//visibility:private"],
+ static_libs: [
+ "framework-connectivity.stubs.module_lib",
+ ],
+ // Not strictly necessary but specified as this MUST not have generate
+ // a dex jar as that will break the tests.
+ compile_dex: false,
+}
+
+python_test_host {
+ name: "jarjar-rules-generator-test",
+ srcs: [
+ "gen_jarjar.py",
+ "gen_jarjar_test.py",
+ ],
+ data: [
+ "testdata/test-jarjar-excludes.txt",
+ "testdata/test-unsupportedappusage.txt",
+ ":framework-connectivity.stubs.module_lib-for-test",
+ ":jarjar-rules-generator-testjavalib",
+ ],
+ main: "gen_jarjar_test.py",
+ version: {
+ py2: {
+ enabled: false,
+ },
+ py3: {
+ enabled: true,
+ },
+ },
+}
diff --git a/tools/gen_jarjar.py b/tools/gen_jarjar.py
new file mode 100755
index 0000000..4c2cf54
--- /dev/null
+++ b/tools/gen_jarjar.py
@@ -0,0 +1,133 @@
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+""" This script generates jarjar rule files to add a jarjar prefix to all classes, except those
+that are API, unsupported API or otherwise excluded."""
+
+import argparse
+import io
+import re
+import subprocess
+from xml import sax
+from xml.sax.handler import ContentHandler
+from zipfile import ZipFile
+
+
+def parse_arguments(argv):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '--jars', nargs='+',
+ help='Path to pre-jarjar JAR. Can be followed by multiple space-separated paths.')
+ parser.add_argument(
+ '--prefix', required=True,
+ help='Package prefix to use for jarjared classes, '
+ 'for example "com.android.connectivity" (does not end with a dot).')
+ parser.add_argument(
+ '--output', required=True, help='Path to output jarjar rules file.')
+ parser.add_argument(
+ '--apistubs', nargs='*', default=[],
+ help='Path to API stubs jar. Classes that are API will not be jarjared. Can be followed by '
+ 'multiple space-separated paths.')
+ parser.add_argument(
+ '--unsupportedapi', nargs='*', default=[],
+ help='Path to UnsupportedAppUsage hidden API .txt lists. '
+ 'Classes that have UnsupportedAppUsage API will not be jarjared. Can be followed by '
+ 'multiple space-separated paths.')
+ parser.add_argument(
+ '--excludes', nargs='*', default=[],
+ help='Path to files listing classes that should not be jarjared. Can be followed by '
+ 'multiple space-separated paths. '
+ 'Each file should contain one full-match regex per line. Empty lines or lines '
+ 'starting with "#" are ignored.')
+ return parser.parse_args(argv)
+
+
+def _list_toplevel_jar_classes(jar):
+ """List all classes in a .class .jar file that are not inner classes."""
+ return {_get_toplevel_class(c) for c in _list_jar_classes(jar)}
+
+def _list_jar_classes(jar):
+ with ZipFile(jar, 'r') as zip:
+ files = zip.namelist()
+ assert 'classes.dex' not in files, f'Jar file {jar} is dexed, ' \
+ 'expected an intermediate zip of .class files'
+ class_len = len('.class')
+ return [f.replace('/', '.')[:-class_len] for f in files
+ if f.endswith('.class') and not f.endswith('/package-info.class')]
+
+
+def _list_hiddenapi_classes(txt_file):
+ out = set()
+ with open(txt_file, 'r') as f:
+ for line in f:
+ if not line.strip():
+ continue
+ assert line.startswith('L') and ';' in line, f'Class name not recognized: {line}'
+ clazz = line.replace('/', '.').split(';')[0][1:]
+ out.add(_get_toplevel_class(clazz))
+ return out
+
+
+def _get_toplevel_class(clazz):
+ """Return the name of the toplevel (not an inner class) enclosing class of the given class."""
+ if '$' not in clazz:
+ return clazz
+ return clazz.split('$')[0]
+
+
+def _get_excludes(path):
+ out = []
+ with open(path, 'r') as f:
+ for line in f:
+ stripped = line.strip()
+ if not stripped or stripped.startswith('#'):
+ continue
+ out.append(re.compile(stripped))
+ return out
+
+
+def make_jarjar_rules(args):
+ excluded_classes = set()
+ for apistubs_file in args.apistubs:
+ excluded_classes.update(_list_toplevel_jar_classes(apistubs_file))
+
+ for unsupportedapi_file in args.unsupportedapi:
+ excluded_classes.update(_list_hiddenapi_classes(unsupportedapi_file))
+
+ exclude_regexes = []
+ for exclude_file in args.excludes:
+ exclude_regexes.extend(_get_excludes(exclude_file))
+
+ with open(args.output, 'w') as outfile:
+ for jar in args.jars:
+ jar_classes = _list_jar_classes(jar)
+ jar_classes.sort()
+ for clazz in jar_classes:
+ if (_get_toplevel_class(clazz) not in excluded_classes and
+ not any(r.fullmatch(clazz) for r in exclude_regexes)):
+ outfile.write(f'rule {clazz} {args.prefix}.@0\n')
+ # Also include jarjar rules for unit tests of the class, so the package matches
+ outfile.write(f'rule {clazz}Test {args.prefix}.@0\n')
+ outfile.write(f'rule {clazz}Test$* {args.prefix}.@0\n')
+
+
+def _main():
+ # Pass in None to use argv
+ args = parse_arguments(None)
+ make_jarjar_rules(args)
+
+
+if __name__ == '__main__':
+ _main()
diff --git a/tools/gen_jarjar_test.py b/tools/gen_jarjar_test.py
new file mode 100644
index 0000000..8d8e82b
--- /dev/null
+++ b/tools/gen_jarjar_test.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# 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.
+
+import gen_jarjar
+import unittest
+
+
+class TestGenJarjar(unittest.TestCase):
+ def test_gen_rules(self):
+ args = gen_jarjar.parse_arguments([
+ "--jars", "jarjar-rules-generator-testjavalib.jar",
+ "--prefix", "jarjar.prefix",
+ "--output", "test-output-rules.txt",
+ "--apistubs", "framework-connectivity.stubs.module_lib.jar",
+ "--unsupportedapi", "testdata/test-unsupportedappusage.txt",
+ "--excludes", "testdata/test-jarjar-excludes.txt",
+ ])
+ gen_jarjar.make_jarjar_rules(args)
+
+ with open(args.output) as out:
+ lines = out.readlines()
+
+ self.assertListEqual([
+ 'rule test.utils.TestUtilClass jarjar.prefix.@0\n',
+ 'rule test.utils.TestUtilClassTest jarjar.prefix.@0\n',
+ 'rule test.utils.TestUtilClassTest$* jarjar.prefix.@0\n',
+ 'rule test.utils.TestUtilClass$TestInnerClass jarjar.prefix.@0\n',
+ 'rule test.utils.TestUtilClass$TestInnerClassTest jarjar.prefix.@0\n',
+ 'rule test.utils.TestUtilClass$TestInnerClassTest$* jarjar.prefix.@0\n'], lines)
+
+
+if __name__ == '__main__':
+ # Need verbosity=2 for the test results parser to find results
+ unittest.main(verbosity=2)
diff --git a/tools/testdata/java/android/net/LinkProperties.java b/tools/testdata/java/android/net/LinkProperties.java
new file mode 100644
index 0000000..bdca377
--- /dev/null
+++ b/tools/testdata/java/android/net/LinkProperties.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/**
+ * Test class with a name matching a public API.
+ */
+public class LinkProperties {
+}
diff --git a/tools/testdata/java/test/jarjarexcluded/JarjarExcludedClass.java b/tools/testdata/java/test/jarjarexcluded/JarjarExcludedClass.java
new file mode 100644
index 0000000..7e3bee1
--- /dev/null
+++ b/tools/testdata/java/test/jarjarexcluded/JarjarExcludedClass.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package test.jarjarexcluded;
+
+/**
+ * Test class that is excluded from jarjar.
+ */
+public class JarjarExcludedClass {
+}
diff --git a/tools/testdata/java/test/unsupportedappusage/TestUnsupportedAppUsageClass.java b/tools/testdata/java/test/unsupportedappusage/TestUnsupportedAppUsageClass.java
new file mode 100644
index 0000000..9d32296
--- /dev/null
+++ b/tools/testdata/java/test/unsupportedappusage/TestUnsupportedAppUsageClass.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package test.unsupportedappusage;
+
+public class TestUnsupportedAppUsageClass {
+ public void testMethod() {}
+}
diff --git a/tools/testdata/java/test/utils/TestUtilClass.java b/tools/testdata/java/test/utils/TestUtilClass.java
new file mode 100644
index 0000000..2162e45
--- /dev/null
+++ b/tools/testdata/java/test/utils/TestUtilClass.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package test.utils;
+
+/**
+ * Sample class to test jarjar rules.
+ */
+public class TestUtilClass {
+ public static class TestInnerClass {}
+}
diff --git a/tools/testdata/test-jarjar-excludes.txt b/tools/testdata/test-jarjar-excludes.txt
new file mode 100644
index 0000000..35d97a2
--- /dev/null
+++ b/tools/testdata/test-jarjar-excludes.txt
@@ -0,0 +1,3 @@
+# Test file for excluded classes
+test\.jarj.rexcluded\.JarjarExcludedCla.s
+test\.jarjarexcluded\.JarjarExcludedClass\$TestInnerCl.ss
diff --git a/tools/testdata/test-unsupportedappusage.txt b/tools/testdata/test-unsupportedappusage.txt
new file mode 100644
index 0000000..331eff9
--- /dev/null
+++ b/tools/testdata/test-unsupportedappusage.txt
@@ -0,0 +1 @@
+Ltest/unsupportedappusage/TestUnsupportedAppUsageClass;->testMethod()V
\ No newline at end of file