Merge "Rename BpfNetMapsReader to NetworkStackBpfNetMaps" into main
diff --git a/Cronet/tests/common/AndroidTest.xml b/Cronet/tests/common/AndroidTest.xml
index bded8fb..ae6b65b 100644
--- a/Cronet/tests/common/AndroidTest.xml
+++ b/Cronet/tests/common/AndroidTest.xml
@@ -35,26 +35,17 @@
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
<!-- b/298380508 -->
<option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
- <!-- b/316571753 -->
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
- <!-- b/316567693 -->
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
<!-- b/316559294 -->
<option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
<!-- b/316559294 -->
<option name="exclude-filter" value="org.chromium.net.NQETest#testPrefsWriteRead" />
<!-- b/316554711-->
- <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
+ <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
<!-- b/316550794 -->
<option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
<option name="hidden-api-checks" value="false"/>
<option name="isolated-storage" value="false"/>
+ <option name="orchestrator" value="true"/>
<option
name="device-listeners"
value="com.android.modules.utils.testing.NativeCoverageHackInstrumentationListener" />
diff --git a/Cronet/tests/mts/AndroidTest.xml b/Cronet/tests/mts/AndroidTest.xml
index bccbe29..5aed655 100644
--- a/Cronet/tests/mts/AndroidTest.xml
+++ b/Cronet/tests/mts/AndroidTest.xml
@@ -35,16 +35,6 @@
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
<!-- b/298380508 -->
<option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
- <!-- b/316571753 -->
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
- <!-- b/316567693 -->
- <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
<!-- b/316559294 -->
<option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
<!-- b/316559294 -->
@@ -55,6 +45,7 @@
<option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
<option name="hidden-api-checks" value="false"/>
<option name="isolated-storage" value="false"/>
+ <option name="orchestrator" value="true"/>
</test>
<!-- Only run NetHttpTests in MTS if the Tethering Mainline module is installed. -->
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 2933a44..1022b06 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -83,8 +83,10 @@
import java.net.NetworkInterface;
import java.net.SocketException;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -146,6 +148,8 @@
private static final TetheringManager sTm = sContext.getSystemService(TetheringManager.class);
private static final PackageManager sPackageManager = sContext.getPackageManager();
private static final CtsNetUtils sCtsNetUtils = new CtsNetUtils(sContext);
+ private static final List<String> sCallbackErrors =
+ Collections.synchronizedList(new ArrayList<>());
// Late initialization in setUp()
private boolean mRunTests;
@@ -201,6 +205,7 @@
assumeTrue(mRunTests);
mTetheredInterfaceRequester = new TetheredInterfaceRequester();
+ sCallbackErrors.clear();
}
private boolean isEthernetTetheringSupported() throws Exception {
@@ -280,6 +285,10 @@
mHandlerThread.quitSafely();
mHandlerThread.join();
}
+
+ if (sCallbackErrors.size() > 0) {
+ fail("Some callbacks had errors: " + sCallbackErrors);
+ }
}
protected static boolean isInterfaceForTetheringAvailable() throws Exception {
@@ -391,7 +400,7 @@
}
@Override
public void onTetheredInterfacesChanged(List<String> interfaces) {
- fail("Should only call callback that takes a Set<TetheringInterface>");
+ addCallbackError("Should only call callback that takes a Set<TetheringInterface>");
}
@Override
@@ -412,7 +421,7 @@
@Override
public void onLocalOnlyInterfacesChanged(List<String> interfaces) {
- fail("Should only call callback that takes a Set<TetheringInterface>");
+ addCallbackError("Should only call callback that takes a Set<TetheringInterface>");
}
@Override
@@ -481,7 +490,7 @@
// Ignore stale callbacks registered by previous test cases.
if (mUnregistered) return;
- fail("TetheringEventCallback got error:" + error + " on iface " + ifName);
+ addCallbackError("TetheringEventCallback got error:" + error + " on iface " + ifName);
}
@Override
@@ -536,6 +545,11 @@
}
}
+ private static void addCallbackError(String error) {
+ Log.e(TAG, error);
+ sCallbackErrors.add(error);
+ }
+
protected static MyTetheringEventCallback enableEthernetTethering(String iface,
TetheringRequest request, Network expectedUpstream) throws Exception {
// Enable ethernet tethering with null expectedUpstream means the test accept any upstream
@@ -562,7 +576,7 @@
@Override
public void onTetheringFailed(int resultCode) {
- fail("Unexpectedly got onTetheringFailed");
+ addCallbackError("Unexpectedly got onTetheringFailed");
}
};
Log.d(TAG, "Starting Ethernet tethering");
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 6f57ca8..3f5d923 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -6284,7 +6284,8 @@
@RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
if (!SdkLevel.isAtLeastU()) {
- Log.wtf(TAG, "isUidNetworkingBlocked is not supported on pre-U devices");
+ throw new IllegalStateException(
+ "isUidNetworkingBlocked is not supported on pre-U devices");
}
final NetworkStackBpfNetMaps reader = NetworkStackBpfNetMaps.getInstance();
// Note that before V, the data saver status in bpf is written by ConnectivityService
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 0688e88..c6fe9ca 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -97,7 +97,7 @@
},
};
-int loadAllElfObjects(const android::bpf::Location& location) {
+int loadAllElfObjects(const unsigned int bpfloader_ver, const android::bpf::Location& location) {
int retVal = 0;
DIR* dir;
struct dirent* ent;
@@ -111,7 +111,7 @@
progPath += s;
bool critical;
- int ret = android::bpf::loadProg(progPath.c_str(), &critical, location);
+ int ret = android::bpf::loadProg(progPath.c_str(), &critical, bpfloader_ver, location);
if (ret) {
if (critical) retVal = ret;
ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
@@ -355,9 +355,12 @@
// Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
if (createSysFsBpfSubDir("loader")) return 1;
+ // This is Network BpfLoader v0.42
+ const unsigned int bpfloader_ver = 42u;
+
// Load all ELF objects, create programs and maps, and pin them
for (const auto& location : locations) {
- if (loadAllElfObjects(location) != 0) {
+ if (loadAllElfObjects(bpfloader_ver, location) != 0) {
ALOGE("=== CRITICAL FAILURE LOADING BPF PROGRAMS FROM %s ===", location.dir);
ALOGE("If this triggers reliably, you're probably missing kernel options or patches.");
ALOGE("If this triggers randomly, you might be hitting some memory allocation "
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
index 3026655..a125ffd 100644
--- a/netbpfload/loader.cpp
+++ b/netbpfload/loader.cpp
@@ -31,24 +31,11 @@
#include <sys/wait.h>
#include <unistd.h>
-// This is Network BpfLoader v0.42
-// WARNING: If you ever hit cherrypick conflicts here you're doing it wrong:
-// You are NOT allowed to cherrypick bpfloader related patches out of order.
-// (indeed: cherrypicking is probably a bad idea and you should merge instead)
-// Mainline supports ONLY the published versions of the bpfloader for each Android release.
-#define BPFLOADER_VERSION_MAJOR 0u
-#define BPFLOADER_VERSION_MINOR 42u
-#define BPFLOADER_VERSION ((BPFLOADER_VERSION_MAJOR << 16) | BPFLOADER_VERSION_MINOR)
-
#include "BpfSyscallWrappers.h"
#include "bpf/BpfUtils.h"
#include "bpf/bpf_map_def.h"
#include "loader.h"
-#if BPFLOADER_VERSION < COMPILE_FOR_BPFLOADER_VERSION
-#error "BPFLOADER_VERSION is less than COMPILE_FOR_BPFLOADER_VERSION"
-#endif
-
#include <cstdlib>
#include <fstream>
#include <iostream>
@@ -626,7 +613,8 @@
}
static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
- const char* prefix, const size_t sizeOfBpfMapDef) {
+ const char* prefix, const size_t sizeOfBpfMapDef,
+ const unsigned int bpfloader_ver) {
int ret;
vector<char> mdData;
vector<struct bpf_map_def> md;
@@ -668,14 +656,14 @@
for (int i = 0; i < (int)mapNames.size(); i++) {
if (md[i].zero != 0) abort();
- if (BPFLOADER_VERSION < md[i].bpfloader_min_ver) {
+ if (bpfloader_ver < md[i].bpfloader_min_ver) {
ALOGI("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
md[i].bpfloader_min_ver);
mapFds.push_back(unique_fd());
continue;
}
- if (BPFLOADER_VERSION >= md[i].bpfloader_max_ver) {
+ if (bpfloader_ver >= md[i].bpfloader_max_ver) {
ALOGI("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
md[i].bpfloader_max_ver);
mapFds.push_back(unique_fd());
@@ -781,7 +769,8 @@
.max_entries = max_entries,
.map_flags = md[i].map_flags,
};
- strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
+ if (isAtLeastKernelVersion(4, 14, 0))
+ strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
fd.reset(bpf(BPF_MAP_CREATE, req));
saved_errno = errno;
ALOGD("bpf_create_map name %s, ret: %d", mapNames[i].c_str(), fd.get());
@@ -925,7 +914,7 @@
}
static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
- const char* prefix) {
+ const char* prefix, const unsigned int bpfloader_ver) {
unsigned kvers = kernelVersion();
if (!kvers) {
@@ -961,8 +950,8 @@
ALOGD("cs[%d].name:%s requires bpfloader version [0x%05x,0x%05x)", i, name.c_str(),
bpfMinVer, bpfMaxVer);
- if (BPFLOADER_VERSION < bpfMinVer) continue;
- if (BPFLOADER_VERSION >= bpfMaxVer) continue;
+ if (bpfloader_ver < bpfMinVer) continue;
+ if (bpfloader_ver >= bpfMaxVer) continue;
if ((cs[i].prog_def->ignore_on_eng && isEng()) ||
(cs[i].prog_def->ignore_on_user && isUser()) ||
@@ -1023,7 +1012,8 @@
.log_size = static_cast<__u32>(log_buf.size()),
.expected_attach_type = cs[i].expected_attach_type,
};
- strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
+ if (isAtLeastKernelVersion(4, 14, 0))
+ strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
fd.reset(bpf(BPF_PROG_LOAD, req));
ALOGD("BPF_PROG_LOAD call for %s (%s) returned fd: %d (%s)", elfPath,
@@ -1097,7 +1087,8 @@
return 0;
}
-int loadProg(const char* elfPath, bool* isCritical, const Location& location) {
+int loadProg(const char* elfPath, bool* isCritical, const unsigned int bpfloader_ver,
+ const Location& location) {
vector<char> license;
vector<char> critical;
vector<codeSection> cs;
@@ -1136,27 +1127,27 @@
readSectionUint("size_of_bpf_prog_def", elfFile, DEFAULT_SIZEOF_BPF_PROG_DEF);
// inclusive lower bound check
- if (BPFLOADER_VERSION < bpfLoaderMinVer) {
+ if (bpfloader_ver < bpfLoaderMinVer) {
ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
- BPFLOADER_VERSION, elfPath, bpfLoaderMinVer);
+ bpfloader_ver, elfPath, bpfLoaderMinVer);
return 0;
}
// exclusive upper bound check
- if (BPFLOADER_VERSION >= bpfLoaderMaxVer) {
+ if (bpfloader_ver >= bpfLoaderMaxVer) {
ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
- BPFLOADER_VERSION, elfPath, bpfLoaderMaxVer);
+ bpfloader_ver, elfPath, bpfLoaderMaxVer);
return 0;
}
- if (BPFLOADER_VERSION < bpfLoaderMinRequiredVer) {
+ if (bpfloader_ver < bpfLoaderMinRequiredVer) {
ALOGI("BpfLoader version 0x%05x failing due to ELF object %s with required min ver 0x%05x",
- BPFLOADER_VERSION, elfPath, bpfLoaderMinRequiredVer);
+ bpfloader_ver, elfPath, bpfLoaderMinRequiredVer);
return -1;
}
ALOGI("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
- BPFLOADER_VERSION, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
+ bpfloader_ver, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
if (sizeOfBpfMapDef < DEFAULT_SIZEOF_BPF_MAP_DEF) {
ALOGE("sizeof(bpf_map_def) of %zu is too small (< %d)", sizeOfBpfMapDef,
@@ -1179,7 +1170,7 @@
/* Just for future debugging */
if (0) dumpAllCs(cs);
- ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef);
+ ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef, bpfloader_ver);
if (ret) {
ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
return ret;
@@ -1190,7 +1181,7 @@
applyMapRelo(elfFile, mapFds, cs);
- ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix);
+ ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix, bpfloader_ver);
if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
return ret;
diff --git a/netbpfload/loader.h b/netbpfload/loader.h
index b884637..4da6830 100644
--- a/netbpfload/loader.h
+++ b/netbpfload/loader.h
@@ -70,7 +70,8 @@
};
// BPF loader implementation. Loads an eBPF ELF object
-int loadProg(const char* elfPath, bool* isCritical, const Location &location = {});
+int loadProg(const char* elfPath, bool* isCritical, const unsigned int bpfloader_ver,
+ const Location &location = {});
// Exposed for testing
unsigned int readSectionUint(const char* name, std::ifstream& elfFile, unsigned int defVal);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 30440f5..f27e645 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -117,6 +117,9 @@
import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
+import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
+
+import static java.util.Map.Entry;
import android.Manifest;
import android.annotation.CheckResult;
@@ -1002,6 +1005,9 @@
// Uids that ConnectivityService is pending to close sockets of.
private final Set<Integer> mPendingFrozenUids = new ArraySet<>();
+ // Flag to drop packets to VPN addresses ingressing via non-VPN interfaces.
+ private final boolean mIngressToVpnAddressFiltering;
+
/**
* Implements support for the legacy "one network per network type" model.
*
@@ -1979,6 +1985,8 @@
activityManager.registerUidFrozenStateChangedCallback(
(Runnable r) -> r.run(), frozenStateChangedCallback);
}
+ mIngressToVpnAddressFiltering = mDeps.isAtLeastT()
+ && mDeps.isFeatureNotChickenedOut(mContext, INGRESS_TO_VPN_ADDRESS_FILTERING);
}
/**
@@ -5344,6 +5352,7 @@
// was is being disconnected the callbacks have already been sent, and if it is being
// destroyed pending replacement they will be sent when it is disconnected.
maybeDisableForwardRulesForDisconnectingNai(nai, false /* sendCallbacks */);
+ updateIngressToVpnAddressFiltering(null, nai.linkProperties, nai);
try {
mNetd.networkDestroy(nai.network.getNetId());
} catch (RemoteException | ServiceSpecificException e) {
@@ -8694,6 +8703,8 @@
// new interface (the interface name -> index map becomes initialized)
updateVpnFiltering(newLp, oldLp, networkAgent);
+ updateIngressToVpnAddressFiltering(newLp, oldLp, networkAgent);
+
updateMtu(newLp, oldLp);
// TODO - figure out what to do for clat
// for (LinkProperties lp : newLp.getStackedLinks()) {
@@ -8985,6 +8996,87 @@
}
}
+ /**
+ * Returns ingress discard rules to drop packets to VPN addresses ingressing via non-VPN
+ * interfaces.
+ * Ingress discard rule is added to the address iff
+ * 1. The address is not a link local address
+ * 2. The address is used by a single VPN interface and not used by any other
+ * interfaces even non-VPN ones
+ * This method can be called during network disconnects, when nai has already been removed from
+ * mNetworkAgentInfos.
+ *
+ * @param nai This method generates rules assuming lp of this nai is the lp at the second
+ * argument.
+ * @param lp This method generates rules assuming lp of nai at the first argument is this lp.
+ * Caller passes old lp to generate old rules and new lp to generate new rules.
+ * @return ingress discard rules. Set of pairs of addresses and interface names
+ */
+ private Set<Pair<InetAddress, String>> generateIngressDiscardRules(
+ @NonNull final NetworkAgentInfo nai, @Nullable final LinkProperties lp) {
+ Set<NetworkAgentInfo> nais = new ArraySet<>(mNetworkAgentInfos);
+ nais.add(nai);
+ // Determine how many networks each IP address is currently configured on.
+ // Ingress rules are added only for IP addresses that are configured on single interface.
+ final Map<InetAddress, Integer> addressOwnerCounts = new ArrayMap<>();
+ for (final NetworkAgentInfo agent : nais) {
+ if (agent.isDestroyed()) {
+ continue;
+ }
+ final LinkProperties agentLp = (nai == agent) ? lp : agent.linkProperties;
+ if (agentLp == null) {
+ continue;
+ }
+ for (final InetAddress addr: agentLp.getAllAddresses()) {
+ addressOwnerCounts.put(addr, addressOwnerCounts.getOrDefault(addr, 0) + 1);
+ }
+ }
+
+ // Iterates all networks instead of only generating rule for nai that was passed in since
+ // lp of the nai change could cause/resolve address collision and result in affecting rule
+ // for different network.
+ final Set<Pair<InetAddress, String>> ingressDiscardRules = new ArraySet<>();
+ for (final NetworkAgentInfo agent : nais) {
+ if (!agent.isVPN() || agent.isDestroyed()) {
+ continue;
+ }
+ final LinkProperties agentLp = (nai == agent) ? lp : agent.linkProperties;
+ if (agentLp == null || agentLp.getInterfaceName() == null) {
+ continue;
+ }
+
+ for (final InetAddress addr: agentLp.getAllAddresses()) {
+ if (addressOwnerCounts.get(addr) == 1 && !addr.isLinkLocalAddress()) {
+ ingressDiscardRules.add(new Pair<>(addr, agentLp.getInterfaceName()));
+ }
+ }
+ }
+ return ingressDiscardRules;
+ }
+
+ private void updateIngressToVpnAddressFiltering(@Nullable LinkProperties newLp,
+ @Nullable LinkProperties oldLp, @NonNull NetworkAgentInfo nai) {
+ // Having isAtleastT to avoid NewApi linter error (b/303382209)
+ if (!mIngressToVpnAddressFiltering || !mDeps.isAtLeastT()) {
+ return;
+ }
+ final CompareOrUpdateResult<InetAddress, Pair<InetAddress, String>> ruleDiff =
+ new CompareOrUpdateResult<>(
+ generateIngressDiscardRules(nai, oldLp),
+ generateIngressDiscardRules(nai, newLp),
+ (rule) -> rule.first);
+ for (Pair<InetAddress, String> rule: ruleDiff.removed) {
+ mBpfNetMaps.removeIngressDiscardRule(rule.first);
+ }
+ for (Pair<InetAddress, String> rule: ruleDiff.added) {
+ mBpfNetMaps.setIngressDiscardRule(rule.first, rule.second);
+ }
+ // setIngressDiscardRule overrides the existing rule
+ for (Pair<InetAddress, String> rule: ruleDiff.updated) {
+ mBpfNetMaps.setIngressDiscardRule(rule.first, rule.second);
+ }
+ }
+
private void updateWakeOnLan(@NonNull LinkProperties lp) {
if (mWolSupportedInterfaces == null) {
mWolSupportedInterfaces = new ArraySet<>(mResources.get().getStringArray(
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index bf09160..a55c683 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -38,6 +38,10 @@
public static final String REQUEST_RESTRICTED_WIFI =
"request_restricted_wifi";
+
+ public static final String INGRESS_TO_VPN_ADDRESS_FILTERING =
+ "ingress_to_vpn_address_filtering";
+
private boolean mNoRematchAllRequestsOnRegister;
/**
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index ac922cd..92ddf44 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -39,7 +39,13 @@
// Android U / 14 (api level 34) - various new program types added
#define BPFLOADER_U_VERSION 37u
-// Android V / 15 (api level 35) - this bpfloader should eventually go back to T
+// Android V / 15 (api level 35) - platform only
+// (note: the platform bpfloader in V isn't really versioned at all,
+// as there is no need as it can only load objects compiled at the
+// same time as itself and the rest of the platform)
+#define BPFLOADER_V_VERSION 41u
+
+// Android Mainline - this bpfloader should eventually go back to T
#define BPFLOADER_MAINLINE_VERSION 42u
/* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
@@ -51,7 +57,7 @@
* In which case it's just best to use the default.
*/
#ifndef BPFLOADER_MIN_VER
-#define BPFLOADER_MIN_VER COMPILE_FOR_BPFLOADER_VERSION
+#define BPFLOADER_MIN_VER BPFLOADER_V_VERSION
#endif
#ifndef BPFLOADER_MAX_VER
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
index ef03c4d..00ef91a 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -48,9 +48,6 @@
#define DEFAULT_SIZEOF_BPF_MAP_DEF 32 // v0.0 struct: enum (uint sized) + 7 uint
#define DEFAULT_SIZEOF_BPF_PROG_DEF 20 // v0.0 struct: 4 uint + bool + 3 byte alignment pad
-// By default, unless otherwise specified, allow the use of features only supported by v0.37.
-#define COMPILE_FOR_BPFLOADER_VERSION 37u
-
/*
* The bpf_{map,prog}_def structures are compiled for different architectures.
* Once by the BPF compiler for the BPF architecture, and once by a C++
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt b/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
index af4f96d..c6e5f25 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/ConcurrentUtils.kt
@@ -19,10 +19,77 @@
package com.android.testutils
import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutorService
import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
import kotlin.system.measureTimeMillis
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
// For Java usage
fun durationOf(fn: Runnable) = measureTimeMillis { fn.run() }
fun CountDownLatch.await(timeoutMs: Long): Boolean = await(timeoutMs, TimeUnit.MILLISECONDS)
+
+/**
+ * Quit resources provided as a list by a supplier.
+ *
+ * The supplier may return more resources as the process progresses, for example while interrupting
+ * threads and waiting for them to finish they may spawn more threads, so this implements a
+ * [maxRetryCount] which, in this case, would be the maximum length of the thread chain that can be
+ * terminated.
+ */
+fun <T> quitResources(
+ maxRetryCount: Int,
+ supplier: () -> List<T>,
+ terminator: Consumer<T>
+) {
+ // Run it multiple times since new threads might be generated in a thread
+ // that is about to be terminated
+ for (retryCount in 0 until maxRetryCount) {
+ val resourcesToBeCleared = supplier()
+ if (resourcesToBeCleared.isEmpty()) return
+ for (resource in resourcesToBeCleared) {
+ terminator.accept(resource)
+ }
+ }
+ assertEmpty(supplier())
+}
+
+/**
+ * Implementation of [quitResources] to interrupt and wait for [ExecutorService]s to finish.
+ */
+@JvmOverloads
+fun quitExecutorServices(
+ maxRetryCount: Int,
+ interrupt: Boolean = true,
+ timeoutMs: Long = 10_000L,
+ supplier: () -> List<ExecutorService>
+) {
+ quitResources(maxRetryCount, supplier) { ecs ->
+ if (interrupt) {
+ ecs.shutdownNow()
+ }
+ assertTrue(ecs.awaitTermination(timeoutMs, TimeUnit.MILLISECONDS),
+ "ExecutorServices did not terminate within timeout")
+ }
+}
+
+/**
+ * Implementation of [quitResources] to interrupt and wait for [Thread]s to finish.
+ */
+@JvmOverloads
+fun quitThreads(
+ maxRetryCount: Int,
+ interrupt: Boolean = true,
+ timeoutMs: Long = 10_000L,
+ supplier: () -> List<Thread>
+) {
+ quitResources(maxRetryCount, supplier) { th ->
+ if (interrupt) {
+ th.interrupt()
+ }
+ th.join(timeoutMs)
+ assertFalse(th.isAlive, "Threads did not terminate within timeout.")
+ }
+}
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 df2e7a6..f81a03d 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
@@ -24,6 +24,7 @@
import static android.content.pm.PackageManager.FEATURE_WIFI;
import static android.net.ConnectivityManager.TYPE_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.os.Process.INVALID_UID;
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.AF_INET6;
@@ -72,6 +73,7 @@
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
+import android.net.InetAddresses;
import android.net.IpSecManager;
import android.net.LinkAddress;
import android.net.LinkProperties;
@@ -91,6 +93,7 @@
import android.net.cts.util.CtsNetUtils;
import android.net.util.KeepaliveUtils;
import android.net.wifi.WifiManager;
+import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
@@ -249,6 +252,7 @@
@Before
public void setUp() throws Exception {
+ assumeTrue(supportedHardware());
mNetwork = null;
mTestContext = getInstrumentation().getContext();
mTargetContext = getInstrumentation().getTargetContext();
@@ -879,7 +883,6 @@
@Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
public void testChangeUnderlyingNetworks() throws Exception {
- assumeTrue(supportedHardware());
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_TELEPHONY));
final TestableNetworkCallback callback = new TestableNetworkCallback();
@@ -938,7 +941,6 @@
@Test
public void testDefault() throws Exception {
- assumeTrue(supportedHardware());
if (!SdkLevel.isAtLeastS() && (
SystemProperties.getInt("persist.adb.tcp.port", -1) > -1
|| SystemProperties.getInt("service.adb.tcp.port", -1) > -1)) {
@@ -1033,8 +1035,6 @@
@Test
public void testAppAllowed() throws Exception {
- assumeTrue(supportedHardware());
-
FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
// Shell app must not be put in here or it would kill the ADB-over-network use case
@@ -1142,8 +1142,6 @@
}
private void doTestAutomaticOnOffKeepaliveMode(final boolean closeSocket) throws Exception {
- assumeTrue(supportedHardware());
-
// Get default network first before starting VPN
final Network defaultNetwork = mCM.getActiveNetwork();
final TestableNetworkCallback cb = new TestableNetworkCallback();
@@ -1231,8 +1229,6 @@
@Test
public void testAppDisallowed() throws Exception {
- assumeTrue(supportedHardware());
-
FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS);
FileDescriptor remoteFd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
@@ -1265,8 +1261,6 @@
@Test
public void testSocketClosed() throws Exception {
- assumeTrue(supportedHardware());
-
final FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS);
final List<FileDescriptor> remoteFds = new ArrayList<>();
@@ -1290,7 +1284,6 @@
@Test
public void testExcludedRoutes() throws Exception {
- assumeTrue(supportedHardware());
assumeTrue(SdkLevel.isAtLeastT());
// Shell app must not be put in here or it would kill the ADB-over-network use case
@@ -1311,8 +1304,6 @@
@Test
public void testIncludedRoutes() throws Exception {
- assumeTrue(supportedHardware());
-
// Shell app must not be put in here or it would kill the ADB-over-network use case
String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName;
startVpn(new String[]{"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
@@ -1330,7 +1321,6 @@
@Test
public void testInterleavedRoutes() throws Exception {
- assumeTrue(supportedHardware());
assumeTrue(SdkLevel.isAtLeastT());
// Shell app must not be put in here or it would kill the ADB-over-network use case
@@ -1358,8 +1348,6 @@
@Test
public void testGetConnectionOwnerUidSecurity() throws Exception {
- assumeTrue(supportedHardware());
-
DatagramSocket s;
InetAddress address = InetAddress.getByName("localhost");
s = new DatagramSocket();
@@ -1380,7 +1368,6 @@
@Test
public void testSetProxy() throws Exception {
- assumeTrue(supportedHardware());
ProxyInfo initialProxy = mCM.getDefaultProxy();
// Receiver for the proxy change broadcast.
BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
@@ -1420,7 +1407,6 @@
@Test
public void testSetProxyDisallowedApps() throws Exception {
- assumeTrue(supportedHardware());
ProxyInfo initialProxy = mCM.getDefaultProxy();
String disallowedApps = mPackageName;
@@ -1446,7 +1432,6 @@
@Test
public void testNoProxy() throws Exception {
- assumeTrue(supportedHardware());
ProxyInfo initialProxy = mCM.getDefaultProxy();
BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver();
proxyBroadcastReceiver.register();
@@ -1481,7 +1466,6 @@
@Test
public void testBindToNetworkWithProxy() throws Exception {
- assumeTrue(supportedHardware());
String allowedApps = mPackageName;
Network initialNetwork = mCM.getActiveNetwork();
ProxyInfo initialProxy = mCM.getDefaultProxy();
@@ -1506,9 +1490,6 @@
@Test
public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception {
- if (!supportedHardware()) {
- return;
- }
// VPN is not routing any traffic i.e. its underlying networks is an empty array.
ArrayList<Network> underlyingNetworks = new ArrayList<>();
String allowedApps = mPackageName;
@@ -1538,9 +1519,6 @@
@Test
public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
- if (!supportedHardware()) {
- return;
- }
Network underlyingNetwork = mCM.getActiveNetwork();
if (underlyingNetwork == null) {
Log.i(TAG, "testVpnMeterednessWithNullUnderlyingNetwork cannot execute"
@@ -1567,9 +1545,6 @@
@Test
public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
- if (!supportedHardware()) {
- return;
- }
Network underlyingNetwork = mCM.getActiveNetwork();
if (underlyingNetwork == null) {
Log.i(TAG, "testVpnMeterednessWithNonNullUnderlyingNetwork cannot execute"
@@ -1609,9 +1584,6 @@
@Test
public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
- if (!supportedHardware()) {
- return;
- }
Network underlyingNetwork = mCM.getActiveNetwork();
if (underlyingNetwork == null) {
Log.i(TAG, "testAlwaysMeteredVpnWithNullUnderlyingNetwork cannot execute"
@@ -1636,9 +1608,6 @@
@Test
public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
- if (!supportedHardware()) {
- return;
- }
Network underlyingNetwork = mCM.getActiveNetwork();
if (underlyingNetwork == null) {
Log.i(TAG, "testAlwaysMeteredVpnWithNonNullUnderlyingNetwork cannot execute"
@@ -1676,9 +1645,6 @@
@Test
public void testB141603906() throws Exception {
- if (!supportedHardware()) {
- return;
- }
final InetSocketAddress src = new InetSocketAddress(0);
final InetSocketAddress dst = new InetSocketAddress(0);
final int NUM_THREADS = 8;
@@ -1786,8 +1752,6 @@
*/
@Test
public void testDownloadWithDownloadManagerDisallowed() throws Exception {
- assumeTrue(supportedHardware());
-
// Start a VPN with DownloadManager package in disallowed list.
startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
new String[] {"192.0.2.0/24", "2001:db8::/32"},
@@ -1843,7 +1807,6 @@
@Test @IgnoreUpTo(Build.VERSION_CODES.R)
public void testBlockIncomingPackets() throws Exception {
- assumeTrue(supportedHardware());
final Network network = mCM.getActiveNetwork();
assertNotNull("Requires a working Internet connection", network);
@@ -1912,7 +1875,6 @@
@Test
public void testSetVpnDefaultForUids() throws Exception {
- assumeTrue(supportedHardware());
assumeTrue(SdkLevel.isAtLeastU());
final Network defaultNetwork = mCM.getActiveNetwork();
@@ -1958,6 +1920,81 @@
});
}
+ /**
+ * Check if packets to a VPN interface's IP arriving on a non-VPN interface are dropped or not.
+ * If the test interface has a different address from the VPN interface, packets must be dropped
+ * If the test interface has the same address as the VPN interface, packets must not be
+ * dropped
+ *
+ * @param duplicatedAddress true to bring up the test interface with the same address as the VPN
+ * interface
+ */
+ private void doTestDropPacketToVpnAddress(final boolean duplicatedAddress)
+ throws Exception {
+ final NetworkRequest request = new NetworkRequest.Builder()
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+ .addTransportType(TRANSPORT_TEST)
+ .build();
+ final CtsNetUtils.TestNetworkCallback callback = new CtsNetUtils.TestNetworkCallback();
+ mCM.requestNetwork(request, callback);
+ final FileDescriptor srcTunFd = runWithShellPermissionIdentity(() -> {
+ final TestNetworkManager tnm = mTestContext.getSystemService(TestNetworkManager.class);
+ List<LinkAddress> linkAddresses = duplicatedAddress
+ ? List.of(new LinkAddress("192.0.2.2/24"),
+ new LinkAddress("2001:db8:1:2::ffe/64")) :
+ List.of(new LinkAddress("198.51.100.2/24"),
+ new LinkAddress("2001:db8:3:4::ffe/64"));
+ final TestNetworkInterface iface = tnm.createTunInterface(linkAddresses);
+ tnm.setupTestNetwork(iface.getInterfaceName(), new Binder());
+ return iface.getFileDescriptor().getFileDescriptor();
+ }, MANAGE_TEST_NETWORKS);
+ final Network testNetwork = callback.waitForAvailable();
+ assertNotNull(testNetwork);
+ final DatagramSocket dstSock = new DatagramSocket();
+
+ testAndCleanup(() -> {
+ startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"} /* addresses */,
+ new String[]{"0.0.0.0/0", "::/0"} /* routes */,
+ "" /* allowedApplications */, "" /* disallowedApplications */,
+ null /* proxyInfo */, null /* underlyingNetworks */,
+ false /* isAlwaysMetered */);
+
+ final FileDescriptor dstUdpFd = dstSock.getFileDescriptor$();
+ checkBlockUdp(srcTunFd, dstUdpFd,
+ InetAddresses.parseNumericAddress("192.0.2.2") /* dstAddress */,
+ InetAddresses.parseNumericAddress("192.0.2.1") /* srcAddress */,
+ duplicatedAddress ? EXPECT_PASS : EXPECT_BLOCK);
+ checkBlockUdp(srcTunFd, dstUdpFd,
+ InetAddresses.parseNumericAddress("2001:db8:1:2::ffe") /* dstAddress */,
+ InetAddresses.parseNumericAddress("2001:db8:1:2::ffa") /* srcAddress */,
+ duplicatedAddress ? EXPECT_PASS : EXPECT_BLOCK);
+
+ // Traffic on VPN should not be affected
+ checkTrafficOnVpn();
+ }, /* cleanup */ () -> {
+ Os.close(srcTunFd);
+ dstSock.close();
+ }, /* cleanup */ () -> {
+ runWithShellPermissionIdentity(() -> {
+ mTestContext.getSystemService(TestNetworkManager.class)
+ .teardownTestNetwork(testNetwork);
+ }, MANAGE_TEST_NETWORKS);
+ }, /* cleanup */ () -> {
+ mCM.unregisterNetworkCallback(callback);
+ });
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testDropPacketToVpnAddress_WithoutDuplicatedAddress() throws Exception {
+ doTestDropPacketToVpnAddress(false /* duplicatedAddress */);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testDropPacketToVpnAddress_WithDuplicatedAddress() throws Exception {
+ doTestDropPacketToVpnAddress(true /* duplicatedAddress */);
+ }
+
private ByteBuffer buildIpv4UdpPacket(final Inet4Address dstAddr, final Inet4Address srcAddr,
final short dstPort, final short srcPort, final byte[] payload) throws IOException {
@@ -2001,7 +2038,8 @@
private void checkBlockUdp(
final FileDescriptor srcTunFd,
final FileDescriptor dstUdpFd,
- final boolean ipv6,
+ final InetAddress dstAddress,
+ final InetAddress srcAddress,
final boolean expectBlock) throws Exception {
final Random random = new Random();
final byte[] sendData = new byte[100];
@@ -2009,15 +2047,15 @@
final short dstPort = (short) ((InetSocketAddress) Os.getsockname(dstUdpFd)).getPort();
ByteBuffer buf;
- if (ipv6) {
+ if (dstAddress instanceof Inet6Address) {
buf = buildIpv6UdpPacket(
- (Inet6Address) TEST_IP6_DST_ADDR.getAddress(),
- (Inet6Address) TEST_IP6_SRC_ADDR.getAddress(),
+ (Inet6Address) dstAddress,
+ (Inet6Address) srcAddress,
dstPort, TEST_SRC_PORT, sendData);
} else {
buf = buildIpv4UdpPacket(
- (Inet4Address) TEST_IP4_DST_ADDR.getAddress(),
- (Inet4Address) TEST_IP4_SRC_ADDR.getAddress(),
+ (Inet4Address) dstAddress,
+ (Inet4Address) srcAddress,
dstPort, TEST_SRC_PORT, sendData);
}
@@ -2043,8 +2081,10 @@
final FileDescriptor srcTunFd,
final FileDescriptor dstUdpFd,
final boolean expectBlock) throws Exception {
- checkBlockUdp(srcTunFd, dstUdpFd, false /* ipv6 */, expectBlock);
- checkBlockUdp(srcTunFd, dstUdpFd, true /* ipv6 */, expectBlock);
+ checkBlockUdp(srcTunFd, dstUdpFd, TEST_IP4_DST_ADDR.getAddress(),
+ TEST_IP4_SRC_ADDR.getAddress(), expectBlock);
+ checkBlockUdp(srcTunFd, dstUdpFd, TEST_IP6_DST_ADDR.getAddress(),
+ TEST_IP6_SRC_ADDR.getAddress(), expectBlock);
}
private class DetailedBlockedStatusCallback extends TestableNetworkCallback {
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
index 4f21af7..f0a87af 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideVpnTests.java
@@ -171,4 +171,16 @@
public void testSetVpnDefaultForUids() throws Exception {
runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest", "testSetVpnDefaultForUids");
}
+
+ @Test
+ public void testDropPacketToVpnAddress_WithoutDuplicatedAddress() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
+ "testDropPacketToVpnAddress_WithoutDuplicatedAddress");
+ }
+
+ @Test
+ public void testDropPacketToVpnAddress_WithDuplicatedAddress() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".VpnTest",
+ "testDropPacketToVpnAddress_WithDuplicatedAddress");
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index 6a019b7..2315940 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -805,7 +805,7 @@
// harness, which is untagged, won't cause a failure.
long firstTotal = resultsWithTraffic.get(0).total;
for (QueryResult queryResult : resultsWithTraffic) {
- assertWithinPercentage(queryResult + "", firstTotal, queryResult.total, 12);
+ assertWithinPercentage(queryResult + "", firstTotal, queryResult.total, 16);
}
// Expect to see no traffic when querying for any tag in tagsWithNoTraffic or any
diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
index 104d063..3d948ba 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt
@@ -18,10 +18,14 @@
import android.app.Service
import android.content.Intent
+import androidx.annotation.GuardedBy
+import com.android.testutils.quitExecutorServices
+import com.android.testutils.quitThreads
import java.net.URL
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.ExecutorService
import kotlin.collections.ArrayList
import kotlin.test.fail
@@ -37,7 +41,12 @@
.run {
withDefault { key -> getOrPut(key) { ConcurrentLinkedQueue() } }
}
- private val httpRequestUrls = Collections.synchronizedList(ArrayList<String>())
+ private val httpRequestUrls = Collections.synchronizedList(mutableListOf<String>())
+
+ @GuardedBy("networkMonitorThreads")
+ private val networkMonitorThreads = mutableListOf<Thread>()
+ @GuardedBy("networkMonitorExecutorServices")
+ private val networkMonitorExecutorServices = mutableListOf<ExecutorService>()
/**
* Called when an HTTP request is being processed by NetworkMonitor. Returns the response
@@ -52,10 +61,47 @@
}
/**
+ * Called when NetworkMonitor creates a new Thread.
+ */
+ fun onNetworkMonitorThreadCreated(thread: Thread) {
+ synchronized(networkMonitorThreads) {
+ networkMonitorThreads.add(thread)
+ }
+ }
+
+ /**
+ * Called when NetworkMonitor creates a new ExecutorService.
+ */
+ fun onNetworkMonitorExecutorServiceCreated(executorService: ExecutorService) {
+ synchronized(networkMonitorExecutorServices) {
+ networkMonitorExecutorServices.add(executorService)
+ }
+ }
+
+ /**
* Clear all state of this connector. This is intended for use between two tests, so all
* state should be reset as if the connector was just created.
*/
override fun clearAllState() {
+ quitThreads(
+ maxRetryCount = 3,
+ interrupt = true) {
+ synchronized(networkMonitorThreads) {
+ networkMonitorThreads.toList().also { networkMonitorThreads.clear() }
+ }
+ }
+ quitExecutorServices(
+ maxRetryCount = 3,
+ // NetworkMonitor is expected to have interrupted its executors when probing
+ // finishes, otherwise it's a thread pool leak that should be caught, so they should
+ // not need to be interrupted (the test only needs to wait for them to finish).
+ interrupt = false) {
+ synchronized(networkMonitorExecutorServices) {
+ networkMonitorExecutorServices.toList().also {
+ networkMonitorExecutorServices.clear()
+ }
+ }
+ }
httpResponses.clear()
httpRequestUrls.clear()
}
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 7e227c4..e43ce29 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt
@@ -30,13 +30,14 @@
import com.android.server.NetworkStackService.NetworkStackConnector
import com.android.server.connectivity.NetworkMonitor
import com.android.server.net.integrationtests.NetworkStackInstrumentationService.InstrumentationConnector
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
import java.io.ByteArrayInputStream
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
+import java.util.concurrent.ExecutorService
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
private const val TEST_NETID = 42
@@ -60,6 +61,10 @@
private class NetworkMonitorDeps(private val privateDnsBypassNetwork: Network) :
NetworkMonitor.Dependencies() {
override fun getPrivateDnsBypassNetwork(network: Network?) = privateDnsBypassNetwork
+ override fun onThreadCreated(thread: Thread) =
+ InstrumentationConnector.onNetworkMonitorThreadCreated(thread)
+ override fun onExecutorServiceCreated(ecs: ExecutorService) =
+ InstrumentationConnector.onNetworkMonitorExecutorServiceCreated(ecs)
}
/**
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index a5d2f4a..2f88c41 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -72,17 +72,6 @@
],
}
-// Subset of services-core used to by ConnectivityService tests to test VPN realistically.
-// This is stripped by jarjar (see rules below) from other unrelated classes, so tests do not
-// include most classes from services-core, which are unrelated and cause wrong code coverage
-// calculations.
-java_library {
- name: "services.core-vpn",
- static_libs: ["services.core"],
- jarjar_rules: "vpn-jarjar-rules.txt",
- visibility: ["//visibility:private"],
-}
-
java_defaults {
name: "FrameworksNetTestsDefaults",
min_sdk_version: "30",
@@ -109,7 +98,6 @@
"platform-test-annotations",
"service-connectivity-pre-jarjar",
"service-connectivity-tiramisu-pre-jarjar",
- "services.core-vpn",
"testables",
"cts-net-utils",
],
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index e09a2cb..8c30776 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -172,6 +172,7 @@
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.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.ConcurrentUtils.await;
import static com.android.testutils.ConcurrentUtils.durationOf;
@@ -367,7 +368,6 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
-import android.security.Credentials;
import android.system.Os;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
@@ -388,7 +388,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IBatteryStats;
import com.android.internal.net.VpnConfig;
-import com.android.internal.net.VpnProfile;
import com.android.internal.util.WakeupMessage;
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.internal.util.test.FakeSettingsProvider;
@@ -423,7 +422,6 @@
import com.android.server.connectivity.SatelliteAccessController;
import com.android.server.connectivity.TcpKeepaliveController;
import com.android.server.connectivity.UidRangeUtils;
-import com.android.server.connectivity.VpnProfileStore;
import com.android.server.net.NetworkPinner;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -463,7 +461,6 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -630,7 +627,6 @@
@Mock TelephonyManager mTelephonyManager;
@Mock EthernetManager mEthernetManager;
@Mock NetworkPolicyManager mNetworkPolicyManager;
- @Mock VpnProfileStore mVpnProfileStore;
@Mock SystemConfigManager mSystemConfigManager;
@Mock DevicePolicyManager mDevicePolicyManager;
@Mock Resources mResources;
@@ -1666,23 +1662,11 @@
waitForIdle();
}
- public void startLegacyVpnPrivileged(VpnProfile profile) {
- switch (profile.type) {
- case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
- case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
- case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
- case VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS:
- startPlatformVpn();
- break;
- case VpnProfile.TYPE_L2TP_IPSEC_PSK:
- case VpnProfile.TYPE_L2TP_IPSEC_RSA:
- case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
- case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
- case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
- startLegacyVpn();
- break;
- default:
- fail("Unknown VPN profile type");
+ public void startLegacyVpnPrivileged(boolean isIkev2Vpn) {
+ if (isIkev2Vpn) {
+ startPlatformVpn();
+ } else {
+ startLegacyVpn();
}
}
@@ -2179,6 +2163,8 @@
return true;
case ALLOW_SATALLITE_NETWORK_FALLBACK:
return true;
+ case INGRESS_TO_VPN_ADDRESS_FILTERING:
+ return true;
default:
return super.isFeatureNotChickenedOut(context, name);
}
@@ -10210,24 +10196,6 @@
doAsUid(Process.SYSTEM_UID, () -> mCm.unregisterNetworkCallback(perUidCb));
}
- private VpnProfile setupLockdownVpn(int profileType) {
- final String profileName = "testVpnProfile";
- final byte[] profileTag = profileName.getBytes(StandardCharsets.UTF_8);
- doReturn(profileTag).when(mVpnProfileStore).get(Credentials.LOCKDOWN_VPN);
-
- final VpnProfile profile = new VpnProfile(profileName);
- profile.name = "My VPN";
- profile.server = "192.0.2.1";
- profile.dnsServers = "8.8.8.8";
- profile.ipsecIdentifier = "My ipsecIdentifier";
- profile.ipsecSecret = "My PSK";
- profile.type = profileType;
- final byte[] encodedProfile = profile.encode();
- doReturn(encodedProfile).when(mVpnProfileStore).get(Credentials.VPN + profileName);
-
- return profile;
- }
-
private void establishLegacyLockdownVpn(Network underlying) throws Exception {
// The legacy lockdown VPN only supports userId 0, and must have an underlying network.
assertNotNull(underlying);
@@ -10239,7 +10207,7 @@
mMockVpn.connect(true);
}
- private void doTestLockdownVpn(VpnProfile profile, boolean expectSetVpnDefaultForUids)
+ private void doTestLockdownVpn(boolean isIkev2Vpn)
throws Exception {
mServiceContext.setPermission(
Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
@@ -10277,8 +10245,8 @@
b.expectBroadcast();
// Simulate LockdownVpnTracker attempting to start the VPN since it received the
// systemDefault callback.
- mMockVpn.startLegacyVpnPrivileged(profile);
- if (expectSetVpnDefaultForUids) {
+ mMockVpn.startLegacyVpnPrivileged(isIkev2Vpn);
+ if (isIkev2Vpn) {
// setVpnDefaultForUids() releases the original network request and creates a VPN
// request so LOST callback is received.
defaultCallback.expect(LOST, mCellAgent);
@@ -10302,7 +10270,7 @@
final NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
b2.expectBroadcast();
b3.expectBroadcast();
- if (expectSetVpnDefaultForUids) {
+ if (isIkev2Vpn) {
// Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
// network satisfier which has TYPE_VPN.
assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10348,14 +10316,15 @@
// callback with different network.
final ExpectedBroadcast b6 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
mMockVpn.stopVpnRunnerPrivileged();
- mMockVpn.startLegacyVpnPrivileged(profile);
+
+ mMockVpn.startLegacyVpnPrivileged(isIkev2Vpn);
// VPN network is disconnected (to restart)
callback.expect(LOST, mMockVpn);
defaultCallback.expect(LOST, mMockVpn);
// The network preference is cleared when VPN is disconnected so it receives callbacks for
// the system-wide default.
defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiAgent);
- if (expectSetVpnDefaultForUids) {
+ if (isIkev2Vpn) {
// setVpnDefaultForUids() releases the original network request and creates a VPN
// request so LOST callback is received.
defaultCallback.expect(LOST, mWiFiAgent);
@@ -10364,7 +10333,7 @@
b6.expectBroadcast();
// While the VPN is reconnecting on the new network, everything is blocked.
- if (expectSetVpnDefaultForUids) {
+ if (isIkev2Vpn) {
// Due to the VPN default request, getActiveNetworkInfo() gets the mNoServiceNetwork
// as the network satisfier.
assertNull(mCm.getActiveNetworkInfo());
@@ -10385,7 +10354,7 @@
systemDefaultCallback.assertNoCallback();
b7.expectBroadcast();
b8.expectBroadcast();
- if (expectSetVpnDefaultForUids) {
+ if (isIkev2Vpn) {
// Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
// network satisfier which has TYPE_VPN.
assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10411,7 +10380,7 @@
defaultCallback.assertNoCallback();
systemDefaultCallback.assertNoCallback();
- if (expectSetVpnDefaultForUids) {
+ if (isIkev2Vpn) {
// Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
// network satisfier which has TYPE_VPN.
assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10452,14 +10421,12 @@
@Test
public void testLockdownVpn_LegacyVpnRunner() throws Exception {
- final VpnProfile profile = setupLockdownVpn(VpnProfile.TYPE_IPSEC_XAUTH_PSK);
- doTestLockdownVpn(profile, false /* expectSetVpnDefaultForUids */);
+ doTestLockdownVpn(false /* isIkev2Vpn */);
}
@Test
public void testLockdownVpn_Ikev2VpnRunner() throws Exception {
- final VpnProfile profile = setupLockdownVpn(VpnProfile.TYPE_IKEV2_IPSEC_PSK);
- doTestLockdownVpn(profile, true /* expectSetVpnDefaultForUids */);
+ doTestLockdownVpn(true /* isIkev2Vpn */);
}
@Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
new file mode 100644
index 0000000..e8664c1
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSIngressDiscardRuleTests.kt
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.InetAddresses
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.VpnManager.TYPE_VPN_SERVICE
+import android.net.VpnTransportInfo
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+private const val VPN_IFNAME = "tun10041"
+private const val VPN_IFNAME2 = "tun10042"
+private const val WIFI_IFNAME = "wlan0"
+private const val TIMEOUT_MS = 1_000L
+private const val LONG_TIMEOUT_MS = 5_000
+
+private fun vpnNc() = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_VPN)
+ .removeCapability(NET_CAPABILITY_NOT_VPN)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .setTransportInfo(
+ VpnTransportInfo(
+ TYPE_VPN_SERVICE,
+ "MySession12345",
+ false /* bypassable */,
+ false /* longLivedTcpConnectionsExpensive */))
+ .build()
+
+private fun wifiNc() = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+
+private fun nr(transport: Int) = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(transport).apply {
+ if (transport != TRANSPORT_VPN) {
+ addCapability(NET_CAPABILITY_NOT_VPN)
+ }
+ }.build()
+
+private fun lp(iface: String, vararg linkAddresses: LinkAddress) = LinkProperties().apply {
+ interfaceName = iface
+ for (linkAddress in linkAddresses) {
+ addLinkAddress(linkAddress)
+ }
+}
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class CSIngressDiscardRuleTests : CSTest() {
+ private val IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8:1::1")
+ private val IPV6_LINK_ADDRESS = LinkAddress(IPV6_ADDRESS, 64)
+ private val IPV6_ADDRESS2 = InetAddresses.parseNumericAddress("2001:db8:1::2")
+ private val IPV6_LINK_ADDRESS2 = LinkAddress(IPV6_ADDRESS2, 64)
+ private val IPV6_ADDRESS3 = InetAddresses.parseNumericAddress("2001:db8:1::3")
+ private val IPV6_LINK_ADDRESS3 = LinkAddress(IPV6_ADDRESS3, 64)
+ private val LOCAL_IPV6_ADDRRESS = InetAddresses.parseNumericAddress("fe80::1234")
+ private val LOCAL_IPV6_LINK_ADDRRESS = LinkAddress(LOCAL_IPV6_ADDRRESS, 64)
+
+ @Test
+ fun testVpnIngressDiscardRule_UpdateVpnAddress() {
+ // non-VPN network whose address will be not duplicated with VPN address
+ val wifiNc = wifiNc()
+ val wifiLp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS3)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+
+ val nr = nr(TRANSPORT_VPN)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(nr, cb)
+ val nc = vpnNc()
+ val lp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val agent = Agent(nc = nc, lp = lp)
+ agent.connect()
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+
+ // IngressDiscardRule is added to the VPN address
+ verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+ verify(bpfNetMaps, never()).setIngressDiscardRule(LOCAL_IPV6_ADDRRESS, VPN_IFNAME)
+
+ // The VPN address is changed
+ val newLp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS2, LOCAL_IPV6_LINK_ADDRRESS)
+ agent.sendLinkProperties(newLp)
+ cb.expect<LinkPropertiesChanged>(agent.network)
+
+ // IngressDiscardRule is removed from the old VPN address and added to the new VPN address
+ verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS)
+ verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS2, VPN_IFNAME)
+ verify(bpfNetMaps, never()).setIngressDiscardRule(LOCAL_IPV6_ADDRRESS, VPN_IFNAME)
+
+ agent.disconnect()
+ verify(bpfNetMaps, timeout(TIMEOUT_MS)).removeIngressDiscardRule(IPV6_ADDRESS2)
+
+ cm.unregisterNetworkCallback(cb)
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_UpdateInterfaceName() {
+ val inorder = inOrder(bpfNetMaps)
+
+ val nr = nr(TRANSPORT_VPN)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(nr, cb)
+ val nc = vpnNc()
+ val lp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val agent = Agent(nc = nc, lp = lp)
+ agent.connect()
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+
+ // IngressDiscardRule is added to the VPN address
+ inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+ inorder.verifyNoMoreInteractions()
+
+ // The VPN interface name is changed
+ val newlp = lp(VPN_IFNAME2, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ agent.sendLinkProperties(newlp)
+ cb.expect<LinkPropertiesChanged>(agent.network)
+
+ // IngressDiscardRule is updated with the new interface name
+ inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME2)
+ inorder.verifyNoMoreInteractions()
+
+ agent.disconnect()
+ inorder.verify(bpfNetMaps, timeout(TIMEOUT_MS)).removeIngressDiscardRule(IPV6_ADDRESS)
+
+ cm.unregisterNetworkCallback(cb)
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_DuplicatedIpAddress_UpdateVpnAddress() {
+ val inorder = inOrder(bpfNetMaps)
+
+ val wifiNc = wifiNc()
+ val wifiLp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+
+ // IngressDiscardRule is not added to non-VPN interfaces
+ inorder.verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+
+ val nr = nr(TRANSPORT_VPN)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+ val vpnNc = vpnNc()
+ val vpnLp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val vpnAgent = Agent(nc = vpnNc, lp = vpnLp)
+ vpnAgent.connect()
+ cb.expectAvailableCallbacks(vpnAgent.network, validated = false)
+
+ // IngressDiscardRule is not added since the VPN address is duplicated with the Wi-Fi
+ // address
+ inorder.verify(bpfNetMaps, never()).setIngressDiscardRule(any(), any())
+
+ // The VPN address is changed to a different address from the Wi-Fi interface
+ val newVpnlp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS2, LOCAL_IPV6_LINK_ADDRRESS)
+ vpnAgent.sendLinkProperties(newVpnlp)
+
+ // IngressDiscardRule is added to the VPN address since the VPN address is not duplicated
+ // with the Wi-Fi address
+ cb.expect<LinkPropertiesChanged>(vpnAgent.network)
+ inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS2, VPN_IFNAME)
+
+ // The VPN address is changed back to the same address as the Wi-Fi interface
+ vpnAgent.sendLinkProperties(vpnLp)
+ cb.expect<LinkPropertiesChanged>(vpnAgent.network)
+
+ // IngressDiscardRule for IPV6_ADDRESS2 is removed but IngressDiscardRule for
+ // IPV6_LINK_ADDRESS is not added since Wi-Fi also uses IPV6_LINK_ADDRESS
+ inorder.verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS2)
+ inorder.verifyNoMoreInteractions()
+
+ vpnAgent.disconnect()
+ inorder.verifyNoMoreInteractions()
+
+ cm.unregisterNetworkCallback(cb)
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_DuplicatedIpAddress_UpdateNonVpnAddress() {
+ val inorder = inOrder(bpfNetMaps)
+
+ val vpnNc = vpnNc()
+ val vpnLp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val vpnAgent = Agent(nc = vpnNc, lp = vpnLp)
+ vpnAgent.connect()
+
+ // IngressDiscardRule is added to the VPN address
+ inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+ inorder.verifyNoMoreInteractions()
+
+ val nr = nr(TRANSPORT_WIFI)
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(nr, cb)
+ val wifiNc = wifiNc()
+ val wifiLp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // IngressDiscardRule is removed since the VPN address is duplicated with the Wi-Fi address
+ inorder.verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS)
+
+ // The Wi-Fi address is changed to a different address from the VPN interface
+ val newWifilp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS2, LOCAL_IPV6_LINK_ADDRRESS)
+ wifiAgent.sendLinkProperties(newWifilp)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // IngressDiscardRule is added to the VPN address since the VPN address is not duplicated
+ // with the Wi-Fi address
+ inorder.verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+ inorder.verifyNoMoreInteractions()
+
+ // The Wi-Fi address is changed back to the same address as the VPN interface
+ wifiAgent.sendLinkProperties(wifiLp)
+ cb.expect<LinkPropertiesChanged>(wifiAgent.network)
+
+ // IngressDiscardRule is removed since the VPN address is duplicated with the Wi-Fi address
+ inorder.verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS)
+
+ // IngressDiscardRule is added to the VPN address since Wi-Fi is disconnected
+ wifiAgent.disconnect()
+ inorder.verify(bpfNetMaps, timeout(TIMEOUT_MS))
+ .setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+
+ vpnAgent.disconnect()
+ inorder.verify(bpfNetMaps, timeout(TIMEOUT_MS)).removeIngressDiscardRule(IPV6_ADDRESS)
+
+ cm.unregisterNetworkCallback(cb)
+ }
+
+ @Test
+ fun testVpnIngressDiscardRule_UnregisterAfterReplacement() {
+ val wifiNc = wifiNc()
+ val wifiLp = lp(WIFI_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+ waitForIdle()
+
+ val vpnNc = vpnNc()
+ val vpnLp = lp(VPN_IFNAME, IPV6_LINK_ADDRESS, LOCAL_IPV6_LINK_ADDRRESS)
+ val vpnAgent = Agent(nc = vpnNc, lp = vpnLp)
+ vpnAgent.connect()
+
+ // IngressDiscardRule is added since the Wi-Fi network is destroyed
+ verify(bpfNetMaps).setIngressDiscardRule(IPV6_ADDRESS, VPN_IFNAME)
+
+ // IngressDiscardRule is removed since the VPN network is destroyed
+ vpnAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+ waitForIdle()
+ verify(bpfNetMaps).removeIngressDiscardRule(IPV6_ADDRESS)
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index 7007b16..13c5cbc 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -185,6 +185,7 @@
fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
fun sendNetworkCapabilities(nc: NetworkCapabilities) = agent.sendNetworkCapabilities(nc)
+ fun sendLinkProperties(lp: LinkProperties) = agent.sendLinkProperties(lp)
fun connectWithCaptivePortal(redirectUrl: String) {
setCaptivePortal(redirectUrl)
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index b0fa7ff..6c9871c 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -153,6 +153,7 @@
it[ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION] = true
it[ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS] = true
it[ConnectivityService.ALLOW_SATALLITE_NETWORK_FALLBACK] = true
+ it[ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING] = true
}
fun enableFeature(f: String) = enabledFeatures.set(f, true)
fun disableFeature(f: String) = enabledFeatures.set(f, false)
diff --git a/tests/unit/vpn-jarjar-rules.txt b/tests/unit/vpn-jarjar-rules.txt
deleted file mode 100644
index f74eab8..0000000
--- a/tests/unit/vpn-jarjar-rules.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-# Only keep classes imported by ConnectivityServiceTest
-keep com.android.server.connectivity.VpnProfileStore
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
index b74a15a..4a7d3a7 100644
--- a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
@@ -74,42 +74,61 @@
public final class ActiveOperationalDataset implements Parcelable {
/** The maximum length of the Active Operational Dataset TLV array in bytes. */
public static final int LENGTH_MAX_DATASET_TLVS = 254;
+
/** The length of Extended PAN ID in bytes. */
public static final int LENGTH_EXTENDED_PAN_ID = 8;
+
/** The minimum length of Network Name as UTF-8 bytes. */
public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1;
+
/** The maximum length of Network Name as UTF-8 bytes. */
public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16;
+
/** The length of Network Key in bytes. */
public static final int LENGTH_NETWORK_KEY = 16;
+
/** The length of Mesh-Local Prefix in bits. */
public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64;
+
/** The length of PSKc in bytes. */
public static final int LENGTH_PSKC = 16;
+
/** The 2.4 GHz channel page. */
public static final int CHANNEL_PAGE_24_GHZ = 0;
+
/** The minimum 2.4GHz channel. */
public static final int CHANNEL_MIN_24_GHZ = 11;
+
/** The maximum 2.4GHz channel. */
public static final int CHANNEL_MAX_24_GHZ = 26;
+
/** @hide */
@VisibleForTesting public static final int TYPE_CHANNEL = 0;
+
/** @hide */
@VisibleForTesting public static final int TYPE_PAN_ID = 1;
+
/** @hide */
@VisibleForTesting public static final int TYPE_EXTENDED_PAN_ID = 2;
+
/** @hide */
@VisibleForTesting public static final int TYPE_NETWORK_NAME = 3;
+
/** @hide */
@VisibleForTesting public static final int TYPE_PSKC = 4;
+
/** @hide */
@VisibleForTesting public static final int TYPE_NETWORK_KEY = 5;
+
/** @hide */
@VisibleForTesting public static final int TYPE_MESH_LOCAL_PREFIX = 7;
+
/** @hide */
@VisibleForTesting public static final int TYPE_SECURITY_POLICY = 12;
+
/** @hide */
@VisibleForTesting public static final int TYPE_ACTIVE_TIMESTAMP = 14;
+
/** @hide */
@VisibleForTesting public static final int TYPE_CHANNEL_MASK = 53;
@@ -975,8 +994,10 @@
public static final class SecurityPolicy {
/** The default Rotation Time in hours. */
public static final int DEFAULT_ROTATION_TIME_HOURS = 672;
+
/** The minimum length of Security Policy flags in bytes. */
public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1;
+
/** The length of Rotation Time TLV value in bytes. */
private static final int LENGTH_SECURITY_POLICY_ROTATION_TIME = 2;
diff --git a/thread/service/java/com/android/server/thread/NsdPublisher.java b/thread/service/java/com/android/server/thread/NsdPublisher.java
index 440c2c3..3c7a72b 100644
--- a/thread/service/java/com/android/server/thread/NsdPublisher.java
+++ b/thread/service/java/com/android/server/thread/NsdPublisher.java
@@ -204,8 +204,12 @@
}
}
- /** On ot-daemon died, unregister all registrations. */
- public void onOtDaemonDied() {
+ @Override
+ public void reset() {
+ mHandler.post(this::resetInternal);
+ }
+
+ private void resetInternal() {
checkOnHandlerThread();
for (int i = 0; i < mRegistrationListeners.size(); ++i) {
try {
@@ -222,6 +226,12 @@
}
}
mRegistrationListeners.clear();
+ mRegistrationJobs.clear();
+ }
+
+ /** On ot-daemon died, reset. */
+ public void onOtDaemonDied() {
+ reset();
}
// TODO: b/323300118 - Remove this mechanism when the race condition in NsdManager is fixed.
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index aba4193..5cb53fe 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -46,10 +46,13 @@
*/
public class ThreadPersistentSettings {
private static final String TAG = "ThreadPersistentSettings";
+
/** File name used for storing settings. */
private static final String FILE_NAME = "ThreadPersistentSettings.xml";
+
/** Current config store data version. This will be incremented for any additions. */
private static final int CURRENT_SETTINGS_STORE_DATA_VERSION = 1;
+
/**
* Stores the version of the data. This can be used to handle migration of data if some
* non-backward compatible change introduced.
@@ -210,7 +213,7 @@
mSettings.putAll(bundleRead);
}
} catch (FileNotFoundException e) {
- Log.e(TAG, "No store file to read", e);
+ Log.w(TAG, "No store file to read", e);
} catch (IOException e) {
Log.e(TAG, "Read from store file failed", e);
}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 5890d26..8cdf38d 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -19,7 +19,6 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-// TODO: add this test to the CTS test suite
android_test {
name: "CtsThreadNetworkTestCases",
min_sdk_version: "33",
@@ -30,6 +29,7 @@
"src/**/*.java",
],
test_suites: [
+ "cts",
"general-tests",
"mcts-tethering",
"mts-tethering",
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 6c02caa..0591c87 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -40,7 +40,6 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -60,7 +59,8 @@
import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkManager;
import android.net.thread.utils.TapTestNetworkTracker;
-import android.os.Build;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
import android.os.HandlerThread;
import android.os.OutcomeReceiver;
@@ -69,16 +69,12 @@
import androidx.test.filters.LargeTest;
import com.android.net.module.util.ArrayTrackRecord;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.FunctionalUtils.ThrowingRunnable;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.runner.RunWith;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
@@ -99,8 +95,7 @@
/** CTS tests for {@link ThreadNetworkController}. */
@LargeTest
-@RunWith(DevSdkIgnoreRunner.class)
-@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+
+@RequiresThreadFeature
public class ThreadNetworkControllerTest {
private static final int JOIN_TIMEOUT_MILLIS = 30 * 1000;
private static final int LEAVE_TIMEOUT_MILLIS = 2_000;
@@ -114,7 +109,7 @@
private static final String THREAD_NETWORK_PRIVILEGED =
"android.permission.THREAD_NETWORK_PRIVILEGED";
- @Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
private final Context mContext = ApplicationProvider.getApplicationContext();
private ExecutorService mExecutor;
@@ -127,14 +122,10 @@
@Before
public void setUp() throws Exception {
- ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
- if (manager != null) {
- mController = manager.getAllThreadNetworkControllers().get(0);
- }
-
- // TODO: we will also need it in tearDown(), it's better to have a Rule to skip
- // tests if a feature is not available.
- assumeNotNull(mController);
+ mController =
+ mContext.getSystemService(ThreadNetworkManager.class)
+ .getAllThreadNetworkControllers()
+ .get(0);
mGrantedPermissions = new HashSet<String>();
mExecutor = Executors.newSingleThreadExecutor();
@@ -147,9 +138,6 @@
@After
public void tearDown() throws Exception {
- if (mController == null) {
- return;
- }
dropAllPermissions();
leaveAndWait(mController);
tearDownTestNetwork();
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index 88ee47e..353db10 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -17,15 +17,10 @@
package android.net.thread;
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
-import static android.Manifest.permission.NETWORK_SETTINGS;
-import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
-import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
import static android.net.thread.utils.IntegrationTestUtils.isFromIpv6Source;
import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
-import static android.net.thread.utils.IntegrationTestUtils.isMulticastRoutingSupported;
-import static android.net.thread.utils.IntegrationTestUtils.isSimulatedThreadRadioSupported;
import static android.net.thread.utils.IntegrationTestUtils.isToIpv6Destination;
import static android.net.thread.utils.IntegrationTestUtils.newPacketReader;
import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
@@ -38,16 +33,13 @@
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
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.Assume.assumeNotNull;
-import static org.junit.Assume.assumeTrue;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.Objects.requireNonNull;
import android.content.Context;
import android.net.InetAddresses;
@@ -56,6 +48,11 @@
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.InfraNetworkDevice;
import android.net.thread.utils.OtDaemonController;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresIpv6MulticastRouting;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.net.thread.utils.ThreadNetworkControllerWrapper;
import android.os.Handler;
import android.os.HandlerThread;
@@ -68,6 +65,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -75,26 +73,15 @@
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
/** Integration test cases for Thread Border Routing feature. */
@RunWith(AndroidJUnit4.class)
+@RequiresThreadFeature
+@RequiresSimulationThreadDevice
@LargeTest
public class BorderRoutingTest {
private static final String TAG = BorderRoutingTest.class.getSimpleName();
- private final Context mContext = ApplicationProvider.getApplicationContext();
- private ThreadNetworkController mController;
- private OtDaemonController mOtCtl;
- private HandlerThread mHandlerThread;
- private Handler mHandler;
- private TestNetworkTracker mInfraNetworkTracker;
- private List<FullThreadDevice> mFtds;
- private TapPacketReader mInfraNetworkReader;
- private InfraNetworkDevice mInfraDevice;
-
private static final int NUM_FTD = 2;
private static final Inet6Address GROUP_ADDR_SCOPE_5 =
(Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
@@ -114,17 +101,21 @@
private static final ActiveOperationalDataset DEFAULT_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final ThreadNetworkControllerWrapper mController =
+ ThreadNetworkControllerWrapper.newInstance(mContext);
+ private OtDaemonController mOtCtl;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private TestNetworkTracker mInfraNetworkTracker;
+ private List<FullThreadDevice> mFtds;
+ private TapPacketReader mInfraNetworkReader;
+ private InfraNetworkDevice mInfraDevice;
+
@Before
public void setUp() throws Exception {
- assumeTrue(isSimulatedThreadRadioSupported());
- final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
- if (manager != null) {
- mController = manager.getAllThreadNetworkControllers().get(0);
- }
-
- // Run the tests on only devices where the Thread feature is available
- assumeNotNull(mController);
-
// TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
mOtCtl = new OtDaemonController();
mOtCtl.factoryReset();
@@ -135,9 +126,7 @@
mFtds = new ArrayList<>();
setUpInfraNetwork();
-
- // BR forms a network.
- startBrLeader();
+ mController.joinAndWait(DEFAULT_DATASET);
// Creates a infra network device.
mInfraNetworkReader = newPacketReader(mInfraNetworkTracker.getTestIface(), mHandler);
@@ -151,20 +140,8 @@
@After
public void tearDown() throws Exception {
- if (mController == null) {
- return;
- }
-
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- NETWORK_SETTINGS,
- () -> {
- CountDownLatch latch = new CountDownLatch(2);
- mController.setTestNetworkAsUpstream(
- null, directExecutor(), v -> latch.countDown());
- mController.leave(directExecutor(), v -> latch.countDown());
- latch.await(10, TimeUnit.SECONDS);
- });
+ mController.setTestNetworkAsUpstreamAndWait(null);
+ mController.leaveAndWait();
tearDownInfraNetwork();
mHandlerThread.quitSafely();
@@ -210,9 +187,6 @@
* </pre>
*/
- // Form the network.
- mOtCtl.factoryReset();
- startBrLeader();
startInfraDevice();
FullThreadDevice ftd = mFtds.get(0);
startFtdChild(ftd);
@@ -225,8 +199,6 @@
@Test
public void unicastRouting_borderRouterSendsUdpToThreadDevice_datagramReceived()
throws Exception {
- assumeTrue(isSimulatedThreadRadioSupported());
-
/*
* <pre>
* Topology:
@@ -236,19 +208,10 @@
* </pre>
*/
- // BR forms a network.
- CompletableFuture<Void> joinFuture = new CompletableFuture<>();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
- joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS);
-
- // Creates a Full Thread Device (FTD) and lets it join the network.
FullThreadDevice ftd = mFtds.get(0);
startFtdChild(ftd);
- Inet6Address ftdOmr = ftd.getOmrAddress();
- Inet6Address ftdMlEid = ftd.getMlEid();
- assertNotNull(ftdMlEid);
+ Inet6Address ftdOmr = requireNonNull(ftd.getOmrAddress());
+ Inet6Address ftdMlEid = requireNonNull(ftd.getMlEid());
ftd.udpBind(ftdOmr, 12345);
sendUdpMessage(ftdOmr, 12345, "aaaaaaaa");
@@ -260,9 +223,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_ftdSubscribedMulticastAddress_infraLinkJoinsMulticastGroup()
throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -281,10 +244,10 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void
multicastRouting_ftdSubscribedScope3MulticastAddress_infraLinkNotJoinMulticastGroup()
throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -303,9 +266,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_ftdSubscribedMulticastAddress_canPingfromInfraLink()
throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -325,9 +288,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_inboundForwarding_afterBrRejoinFtdRepliesSubscribedAddress()
throws Exception {
- assumeTrue(isMulticastRoutingSupported());
// TODO (b/327311034): Testing bbr state switch from primary mode to secondary mode and back
// to primary mode requires an additional BR in the Thread network. This is not currently
@@ -335,9 +298,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_ftdSubscribedScope3MulticastAddress_cannotPingfromInfraLink()
throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -357,9 +320,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_ftdNotSubscribedMulticastAddress_cannotPingFromInfraDevice()
throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -378,9 +341,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_multipleFtdsSubscribedDifferentAddresses_canPingFromInfraDevice()
throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -414,9 +377,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_multipleFtdsSubscribedSameAddress_canPingFromInfraDevice()
throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -449,8 +412,8 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_outboundForwarding_scopeLargerThan3IsForwarded() throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -474,9 +437,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_outboundForwarding_scopeSmallerThan4IsNotForwarded()
throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -497,8 +460,8 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_outboundForwarding_llaToScope4IsNotForwarded() throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -513,15 +476,15 @@
Inet6Address ftdLla = ftd.getLinkLocalAddress();
assertNotNull(ftdLla);
- ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla, 100 /* size */, 1 /* count */);
+ ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla);
assertNull(
pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_outboundForwarding_mlaToScope4IsNotForwarded() throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -537,7 +500,7 @@
assertFalse(ftdMlas.isEmpty());
for (Inet6Address ftdMla : ftdMlas) {
- ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla, 100 /* size */, 1 /* count */);
+ ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla);
assertNull(
pollForPacketOnInfraNetwork(
@@ -546,9 +509,9 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_infraNetworkSwitch_ftdRepliesToSubscribedAddress()
throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -575,8 +538,8 @@
}
@Test
+ @RequiresIpv6MulticastRouting
public void multicastRouting_infraNetworkSwitch_outboundPacketIsForwarded() throws Exception {
- assumeTrue(isMulticastRoutingSupported());
/*
* <pre>
* Topology:
@@ -602,38 +565,21 @@
pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
}
- private void setUpInfraNetwork() {
+ private void setUpInfraNetwork() throws Exception {
mInfraNetworkTracker =
runAsShell(
MANAGE_TEST_NETWORKS,
() ->
initTestNetwork(
mContext, new LinkProperties(), 5000 /* timeoutMs */));
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- NETWORK_SETTINGS,
- () -> {
- CompletableFuture<Void> future = new CompletableFuture<>();
- mController.setTestNetworkAsUpstream(
- mInfraNetworkTracker.getTestIface().getInterfaceName(),
- directExecutor(),
- future::complete);
- future.get(5, TimeUnit.SECONDS);
- });
+ mController.setTestNetworkAsUpstreamAndWait(
+ mInfraNetworkTracker.getTestIface().getInterfaceName());
}
private void tearDownInfraNetwork() {
runAsShell(MANAGE_TEST_NETWORKS, () -> mInfraNetworkTracker.teardown());
}
- private void startBrLeader() throws Exception {
- CompletableFuture<Void> joinFuture = new CompletableFuture<>();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
- joinFuture.get(RESTART_JOIN_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
- }
-
private void startFtdChild(FullThreadDevice ftd) throws Exception {
ftd.factoryReset();
ftd.joinNetwork(DEFAULT_DATASET);
diff --git a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
index 3493d9f..56b658d 100644
--- a/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
+++ b/thread/tests/integration/src/android/net/thread/ServiceDiscoveryTest.java
@@ -16,37 +16,31 @@
package android.net.thread;
-import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.net.InetAddresses.parseNumericAddress;
-import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static android.net.thread.utils.IntegrationTestUtils.JOIN_TIMEOUT;
-import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.discoverForServiceLost;
import static android.net.thread.utils.IntegrationTestUtils.discoverService;
-import static android.net.thread.utils.IntegrationTestUtils.isSimulatedThreadRadioSupported;
import static android.net.thread.utils.IntegrationTestUtils.resolveService;
import static android.net.thread.utils.IntegrationTestUtils.resolveServiceUntil;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
-
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.junit.Assert.assertThrows;
-import static org.junit.Assume.assumeNotNull;
-import static org.junit.Assume.assumeTrue;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.TapTestNetworkTracker;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.net.thread.utils.ThreadNetworkControllerWrapper;
import android.os.HandlerThread;
import androidx.test.core.app.ApplicationProvider;
@@ -58,6 +52,7 @@
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -73,18 +68,13 @@
/** Integration test cases for Service Discovery feature. */
@RunWith(AndroidJUnit4.class)
+@RequiresThreadFeature
+@RequiresSimulationThreadDevice
@LargeTest
@Ignore("TODO: b/328527773 - enable the test when it's stable")
public class ServiceDiscoveryTest {
private static final String TAG = ServiceDiscoveryTest.class.getSimpleName();
private static final int NUM_FTD = 3;
- private final Context mContext = ApplicationProvider.getApplicationContext();
-
- private HandlerThread mHandlerThread;
- private ThreadNetworkController mController;
- private NsdManager mNsdManager;
- private TapTestNetworkTracker mTestNetworkTracker;
- private List<FullThreadDevice> mFtds;
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
@@ -100,26 +90,21 @@
private static final Correspondence<byte[], byte[]> BYTE_ARRAY_EQUALITY =
Correspondence.from(Arrays::equals, "is equivalent to");
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final ThreadNetworkControllerWrapper mController =
+ ThreadNetworkControllerWrapper.newInstance(mContext);
+
+ private HandlerThread mHandlerThread;
+ private NsdManager mNsdManager;
+ private TapTestNetworkTracker mTestNetworkTracker;
+ private List<FullThreadDevice> mFtds;
+
@Before
public void setUp() throws Exception {
- final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
- if (manager != null) {
- mController = manager.getAllThreadNetworkControllers().get(0);
- }
- // Run the tests on only devices where the Thread feature is available.
- assumeNotNull(mController);
-
- // Run the tests only when the device uses simulated Thread radio.
- assumeTrue(isSimulatedThreadRadioSupported());
-
- // BR forms a network.
- CompletableFuture<Void> joinFuture = new CompletableFuture<>();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> mController.join(DEFAULT_DATASET, directExecutor(), joinFuture::complete));
- joinFuture.get(RESTART_JOIN_TIMEOUT.toMillis(), MILLISECONDS);
-
+ mController.joinAndWait(DEFAULT_DATASET);
mNsdManager = mContext.getSystemService(NsdManager.class);
mHandlerThread = new HandlerThread(TAG);
@@ -127,17 +112,8 @@
mTestNetworkTracker = new TapTestNetworkTracker(mContext, mHandlerThread.getLooper());
assertThat(mTestNetworkTracker).isNotNull();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- NETWORK_SETTINGS,
- () -> {
- CompletableFuture<Void> future = new CompletableFuture<>();
- mController.setTestNetworkAsUpstream(
- mTestNetworkTracker.getInterfaceName(),
- directExecutor(),
- v -> future.complete(null));
- future.get(5, SECONDS);
- });
+ mController.setTestNetworkAsUpstreamAndWait(mTestNetworkTracker.getInterfaceName());
+
// Create the FTDs in setUp() so that the FTDs can be safely released in tearDown().
// Don't create new FTDs in test cases.
mFtds = new ArrayList<>();
@@ -150,12 +126,6 @@
@After
public void tearDown() throws Exception {
- if (mController == null) {
- return;
- }
- if (!isSimulatedThreadRadioSupported()) {
- return;
- }
for (FullThreadDevice ftd : mFtds) {
// Clear registered SRP hosts and services
if (ftd.isSrpHostRegistered()) {
@@ -170,18 +140,8 @@
mHandlerThread.quitSafely();
mHandlerThread.join();
}
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- NETWORK_SETTINGS,
- () -> {
- CompletableFuture<Void> setUpstreamFuture = new CompletableFuture<>();
- CompletableFuture<Void> leaveFuture = new CompletableFuture<>();
- mController.setTestNetworkAsUpstream(
- null, directExecutor(), v -> setUpstreamFuture.complete(null));
- mController.leave(directExecutor(), v -> leaveFuture.complete(null));
- setUpstreamFuture.get(5, SECONDS);
- leaveFuture.get(5, SECONDS);
- });
+ mController.setTestNetworkAsUpstreamAndWait(null);
+ mController.leaveAndWait();
}
@Test
diff --git a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
index 580a83a..4a006cf 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadIntegrationTest.java
@@ -16,31 +16,22 @@
package android.net.thread;
-import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_LEADER;
import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
-import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
-import static android.net.thread.utils.IntegrationTestUtils.LEAVE_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.RESTART_JOIN_TIMEOUT;
-import static android.net.thread.utils.IntegrationTestUtils.waitForStateAnyOf;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
-import static com.android.testutils.TestPermissionUtil.runAsShell;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-import static org.junit.Assume.assumeNotNull;
-
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-
-import android.annotation.Nullable;
import android.content.Context;
-import android.net.thread.ThreadNetworkController.StateCallback;
import android.net.thread.utils.OtDaemonController;
+import android.net.thread.utils.ThreadFeatureCheckerRule;
+import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
+import android.net.thread.utils.ThreadNetworkControllerWrapper;
import android.os.SystemClock;
import androidx.test.core.app.ApplicationProvider;
@@ -49,21 +40,18 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.net.Inet6Address;
import java.util.List;
-import java.util.concurrent.CompletableFuture;
/** Tests for E2E Android Thread integration with ot-daemon, ConnectivityService, etc.. */
@LargeTest
+@RequiresThreadFeature
@RunWith(AndroidJUnit4.class)
public class ThreadIntegrationTest {
- private final Context mContext = ApplicationProvider.getApplicationContext();
- private ThreadNetworkController mController;
- private OtDaemonController mOtCtl;
-
// A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset init new".
private static final byte[] DEFAULT_DATASET_TLVS =
base16().decode(
@@ -75,18 +63,17 @@
private static final ActiveOperationalDataset DEFAULT_DATASET =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+ @Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final ThreadNetworkControllerWrapper mController =
+ ThreadNetworkControllerWrapper.newInstance(mContext);
+ private OtDaemonController mOtCtl;
+
@Before
public void setUp() throws Exception {
- final ThreadNetworkManager manager = mContext.getSystemService(ThreadNetworkManager.class);
- if (manager != null) {
- mController = manager.getAllThreadNetworkControllers().get(0);
- }
-
- // Run the tests on only devices where the Thread feature is available
- assumeNotNull(mController);
-
mOtCtl = new OtDaemonController();
- leaveAndWait(mController);
+ mController.leaveAndWait();
// TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
mOtCtl.factoryReset();
@@ -94,47 +81,43 @@
@After
public void tearDown() throws Exception {
- if (mController == null) {
- return;
- }
-
- setTestUpStreamNetworkAndWait(mController, null);
- leaveAndWait(mController);
+ mController.setTestNetworkAsUpstreamAndWait(null);
+ mController.leaveAndWait();
}
@Test
public void otDaemonRestart_notJoinedAndStopped_deviceRoleIsStopped() throws Exception {
- leaveAndWait(mController);
+ mController.leaveAndWait();
runShellCommand("stop ot-daemon");
// TODO(b/323331973): the sleep is needed to workaround the race conditions
SystemClock.sleep(200);
- waitForStateAnyOf(mController, List.of(DEVICE_ROLE_STOPPED), CALLBACK_TIMEOUT);
+ mController.waitForRole(DEVICE_ROLE_STOPPED, CALLBACK_TIMEOUT);
}
@Test
public void otDaemonRestart_JoinedNetworkAndStopped_autoRejoined() throws Exception {
- joinAndWait(mController, DEFAULT_DATASET);
+ mController.joinAndWait(DEFAULT_DATASET);
runShellCommand("stop ot-daemon");
- waitForStateAnyOf(mController, List.of(DEVICE_ROLE_DETACHED), CALLBACK_TIMEOUT);
- waitForStateAnyOf(mController, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT);
+ mController.waitForRole(DEVICE_ROLE_DETACHED, CALLBACK_TIMEOUT);
+ mController.waitForRole(DEVICE_ROLE_LEADER, RESTART_JOIN_TIMEOUT);
}
@Test
public void otDaemonFactoryReset_deviceRoleIsStopped() throws Exception {
- joinAndWait(mController, DEFAULT_DATASET);
+ mController.joinAndWait(DEFAULT_DATASET);
mOtCtl.factoryReset();
- assertThat(getDeviceRole(mController)).isEqualTo(DEVICE_ROLE_STOPPED);
+ assertThat(mController.getDeviceRole()).isEqualTo(DEVICE_ROLE_STOPPED);
}
@Test
public void otDaemonFactoryReset_addressesRemoved() throws Exception {
- joinAndWait(mController, DEFAULT_DATASET);
+ mController.joinAndWait(DEFAULT_DATASET);
mOtCtl.factoryReset();
String ifconfig = runShellCommand("ifconfig thread-wpan");
@@ -144,7 +127,7 @@
@Test
public void tunInterface_joinedNetwork_otAddressesAddedToTunInterface() throws Exception {
- joinAndWait(mController, DEFAULT_DATASET);
+ mController.joinAndWait(DEFAULT_DATASET);
String ifconfig = runShellCommand("ifconfig thread-wpan");
List<Inet6Address> otAddresses = mOtCtl.getAddresses();
@@ -156,46 +139,4 @@
// TODO (b/323300829): add more tests for integration with linux platform and
// ConnectivityService
-
- private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
- CompletableFuture<Integer> future = new CompletableFuture<>();
- StateCallback callback = future::complete;
- controller.registerStateCallback(directExecutor(), callback);
- try {
- return future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS);
- } finally {
- controller.unregisterStateCallback(callback);
- }
- }
-
- private static void joinAndWait(
- ThreadNetworkController controller, ActiveOperationalDataset activeDataset)
- throws Exception {
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> controller.join(activeDataset, directExecutor(), result -> {}));
- waitForStateAnyOf(controller, List.of(DEVICE_ROLE_LEADER), RESTART_JOIN_TIMEOUT);
- }
-
- private static void leaveAndWait(ThreadNetworkController controller) throws Exception {
- CompletableFuture<Void> future = new CompletableFuture<>();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- () -> controller.leave(directExecutor(), future::complete));
- future.get(LEAVE_TIMEOUT.toMillis(), MILLISECONDS);
- }
-
- private static void setTestUpStreamNetworkAndWait(
- ThreadNetworkController controller, @Nullable String networkInterfaceName)
- throws Exception {
- CompletableFuture<Void> future = new CompletableFuture<>();
- runAsShell(
- PERMISSION_THREAD_NETWORK_PRIVILEGED,
- NETWORK_SETTINGS,
- () -> {
- controller.setTestNetworkAsUpstream(
- networkInterfaceName, directExecutor(), future::complete);
- });
- future.get(CALLBACK_TIMEOUT.toMillis(), MILLISECONDS);
- }
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
index 6306a65..600b662 100644
--- a/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
+++ b/thread/tests/integration/src/android/net/thread/utils/FullThreadDevice.java
@@ -52,6 +52,13 @@
* available commands.
*/
public final class FullThreadDevice {
+ private static final int HOP_LIMIT = 64;
+ private static final int PING_INTERVAL = 1;
+ private static final int PING_SIZE = 100;
+ // There may not be a response for the ping command, using a short timeout to keep the tests
+ // short.
+ private static final float PING_TIMEOUT_SECONDS = 0.1f;
+
private final Process mProcess;
private final BufferedReader mReader;
private final BufferedWriter mWriter;
@@ -339,7 +346,36 @@
executeCommand("ipmaddr add " + address.getHostAddress());
}
- public void ping(Inet6Address address, Inet6Address source, int size, int count) {
+ public void ping(Inet6Address address, Inet6Address source) {
+ ping(
+ address,
+ source,
+ PING_SIZE,
+ 1 /* count */,
+ PING_INTERVAL,
+ HOP_LIMIT,
+ PING_TIMEOUT_SECONDS);
+ }
+
+ public void ping(Inet6Address address) {
+ ping(
+ address,
+ null,
+ PING_SIZE,
+ 1 /* count */,
+ PING_INTERVAL,
+ HOP_LIMIT,
+ PING_TIMEOUT_SECONDS);
+ }
+
+ private void ping(
+ Inet6Address address,
+ Inet6Address source,
+ int size,
+ int count,
+ int interval,
+ int hopLimit,
+ float timeout) {
String cmd =
"ping"
+ ((source == null) ? "" : (" -I " + source.getHostAddress()))
@@ -348,14 +384,16 @@
+ " "
+ size
+ " "
- + count;
+ + count
+ + " "
+ + interval
+ + " "
+ + hopLimit
+ + " "
+ + timeout;
executeCommand(cmd);
}
- public void ping(Inet6Address address) {
- ping(address, null, 100 /* size */, 1 /* count */);
- }
-
@FormatMethod
private List<String> executeCommand(String commandFormat, Object... args) {
return executeCommand(String.format(commandFormat, args));
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
index bb2d973..2237e65 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.java
@@ -20,7 +20,6 @@
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
-import static com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@@ -32,8 +31,6 @@
import android.net.thread.ThreadNetworkController;
import android.os.Handler;
import android.os.SystemClock;
-import android.os.SystemProperties;
-import android.os.VintfRuntimeInfo;
import androidx.annotation.NonNull;
@@ -65,8 +62,6 @@
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
import java.util.function.Supplier;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
/** Static utility methods relating to Thread integration tests. */
public final class IntegrationTestUtils {
@@ -79,35 +74,8 @@
public static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
public static final Duration SERVICE_DISCOVERY_TIMEOUT = Duration.ofSeconds(20);
- private static final String KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED = "5.15.0";
- private static final int KERNEL_ANDROID_VERSION_MULTICAST_ROUTING_SUPPORTED = 14;
-
private IntegrationTestUtils() {}
- /** Returns whether the device supports simulated Thread radio. */
- public static boolean isSimulatedThreadRadioSupported() {
- // The integration test uses SIMULATION Thread radio so that it only supports CuttleFish.
- return SystemProperties.get("ro.product.model").startsWith("Cuttlefish");
- }
-
- public static boolean isMulticastRoutingSupported() {
- return isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED)
- && isKernelAndroidVersionAtLeast(
- KERNEL_ANDROID_VERSION_MULTICAST_ROUTING_SUPPORTED);
- }
-
- private static boolean isKernelAndroidVersionAtLeast(int n) {
- final String osRelease = VintfRuntimeInfo.getOsRelease();
- final Pattern pattern = Pattern.compile("android(\\d+)");
- Matcher matcher = pattern.matcher(osRelease);
-
- if (matcher.find()) {
- int version = Integer.parseInt(matcher.group(1));
- return (version >= n);
- }
- return false;
- }
-
/**
* Waits for the given {@link Supplier} to be true until given timeout.
*
diff --git a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
new file mode 100644
index 0000000..e7b4cd9
--- /dev/null
+++ b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread.utils;
+
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkController.StateCallback;
+import android.net.thread.ThreadNetworkException;
+import android.net.thread.ThreadNetworkManager;
+import android.os.OutcomeReceiver;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/** A helper class which provides synchronous API wrappers for {@link ThreadNetworkController}. */
+public final class ThreadNetworkControllerWrapper {
+ public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(10);
+ public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
+ private static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
+
+ private final ThreadNetworkController mController;
+
+ /**
+ * Returns a new {@link ThreadNetworkControllerWrapper} instance or {@code null} if Thread
+ * feature is not supported on this device.
+ */
+ @Nullable
+ public static ThreadNetworkControllerWrapper newInstance(Context context) {
+ final ThreadNetworkManager manager = context.getSystemService(ThreadNetworkManager.class);
+ if (manager == null) {
+ return null;
+ }
+ return new ThreadNetworkControllerWrapper(manager.getAllThreadNetworkControllers().get(0));
+ }
+
+ private ThreadNetworkControllerWrapper(ThreadNetworkController controller) {
+ mController = controller;
+ }
+
+ /**
+ * Returns the Thread enabled state.
+ *
+ * <p>The value can be one of {@code ThreadNetworkController#STATE_*}.
+ */
+ public final int getEnabledState()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Integer> future = new CompletableFuture<>();
+ StateCallback callback =
+ new StateCallback() {
+ @Override
+ public void onThreadEnableStateChanged(int enabledState) {
+ future.complete(enabledState);
+ }
+
+ @Override
+ public void onDeviceRoleChanged(int deviceRole) {}
+ };
+
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> mController.registerStateCallback(directExecutor(), callback));
+ try {
+ return future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+ }
+ }
+
+ /**
+ * Returns the Thread device role.
+ *
+ * <p>The value can be one of {@code ThreadNetworkController#DEVICE_ROLE_*}.
+ */
+ public final int getDeviceRole()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Integer> future = new CompletableFuture<>();
+ StateCallback callback = future::complete;
+
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> mController.registerStateCallback(directExecutor(), callback));
+ try {
+ return future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+ } finally {
+ runAsShell(ACCESS_NETWORK_STATE, () -> mController.unregisterStateCallback(callback));
+ }
+ }
+
+ /** Joins the given network and wait for this device to become attached. */
+ public void joinAndWait(ActiveOperationalDataset activeDataset)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.join(
+ activeDataset, directExecutor(), newOutcomeReceiver(future)));
+ future.get(JOIN_TIMEOUT.toSeconds(), SECONDS);
+ }
+
+ /** An synchronous variant of {@link ThreadNetworkController#leave}. */
+ public void leaveAndWait() throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mController.leave(directExecutor(), future::complete));
+ future.get(LEAVE_TIMEOUT.toSeconds(), SECONDS);
+ }
+
+ /** Waits for the device role to become {@code deviceRole}. */
+ public int waitForRole(int deviceRole, Duration timeout)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return waitForRoleAnyOf(List.of(deviceRole), timeout);
+ }
+
+ /** Waits for the device role to become one of the values specified in {@code deviceRoles}. */
+ public int waitForRoleAnyOf(List<Integer> deviceRoles, Duration timeout)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Integer> future = new CompletableFuture<>();
+ ThreadNetworkController.StateCallback callback =
+ newRole -> {
+ if (deviceRoles.contains(newRole)) {
+ future.complete(newRole);
+ }
+ };
+
+ runAsShell(
+ ACCESS_NETWORK_STATE,
+ () -> mController.registerStateCallback(directExecutor(), callback));
+
+ try {
+ return future.get(timeout.toSeconds(), SECONDS);
+ } finally {
+ mController.unregisterStateCallback(callback);
+ }
+ }
+
+ /** An synchronous variant of {@link ThreadNetworkController#setTestNetworkAsUpstream}. */
+ public void setTestNetworkAsUpstreamAndWait(@Nullable String networkInterfaceName)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ NETWORK_SETTINGS,
+ () -> {
+ mController.setTestNetworkAsUpstream(
+ networkInterfaceName, directExecutor(), future::complete);
+ });
+ future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+ }
+
+ private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
+ CompletableFuture<V> future) {
+ return new OutcomeReceiver<V, ThreadNetworkException>() {
+ @Override
+ public void onResult(V result) {
+ future.complete(result);
+ }
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ future.completeExceptionally(e);
+ }
+ };
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
index 54e89b1..d860166 100644
--- a/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/NsdPublisherTest.java
@@ -25,6 +25,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -468,7 +469,7 @@
}
@Test
- public void onOtDaemonDied_unregisterAll() {
+ public void reset_unregisterAll() {
prepareTest();
DnsTxtAttribute txt1 = makeTxtAttribute("key1", List.of(0x01, 0x02));
@@ -540,7 +541,7 @@
actualRegistrationListenerCaptor.getAllValues().get(1);
actualListener3.onServiceRegistered(actualServiceInfoCaptor.getValue());
- mNsdPublisher.onOtDaemonDied();
+ mNsdPublisher.reset();
mTestLooper.dispatchAll();
verify(mMockNsdManager, times(1)).unregisterService(actualListener1);
@@ -548,6 +549,17 @@
verify(mMockNsdManager, times(1)).unregisterService(actualListener3);
}
+ @Test
+ public void onOtDaemonDied_resetIsCalled() {
+ prepareTest();
+ NsdPublisher spyNsdPublisher = spy(mNsdPublisher);
+
+ spyNsdPublisher.onOtDaemonDied();
+ mTestLooper.dispatchAll();
+
+ verify(spyNsdPublisher, times(1)).reset();
+ }
+
private static DnsTxtAttribute makeTxtAttribute(String name, List<Integer> value) {
DnsTxtAttribute txtAttribute = new DnsTxtAttribute();
diff --git a/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java b/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java
new file mode 100644
index 0000000..bee9ceb
--- /dev/null
+++ b/thread/tests/utils/src/android/net/thread/utils/ThreadFeatureCheckerRule.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.utils;
+
+import static com.android.testutils.DeviceInfoUtils.isKernelVersionAtLeast;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.net.thread.ThreadNetworkManager;
+import android.os.SystemProperties;
+import android.os.VintfRuntimeInfo;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A rule used to skip Thread tests when the device doesn't support a specific feature indicated by
+ * {@code ThreadFeatureCheckerRule.Requires*}.
+ */
+public final class ThreadFeatureCheckerRule implements TestRule {
+ private static final String KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED = "5.15.0";
+ private static final int KERNEL_ANDROID_VERSION_MULTICAST_ROUTING_SUPPORTED = 14;
+
+ /**
+ * Annotates a test class or method requires the Thread feature to run.
+ *
+ * <p>In Absence of the Thread feature, the test class or method will be ignored.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD, ElementType.TYPE})
+ public @interface RequiresThreadFeature {}
+
+ /**
+ * Annotates a test class or method requires the kernel IPv6 multicast routing feature to run.
+ *
+ * <p>In Absence of the multicast routing feature, the test class or method will be ignored.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD, ElementType.TYPE})
+ public @interface RequiresIpv6MulticastRouting {}
+
+ /**
+ * Annotates a test class or method requires the simulation Thread device (i.e. ot-cli-ftd) to
+ * run.
+ *
+ * <p>In Absence of the simulation device, the test class or method will be ignored.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ElementType.METHOD, ElementType.TYPE})
+ public @interface RequiresSimulationThreadDevice {}
+
+ @Override
+ public Statement apply(final Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ if (hasAnnotation(RequiresThreadFeature.class, description)) {
+ assumeTrue(
+ "Skipping test because the Thread feature is unavailable",
+ hasThreadFeature());
+ }
+
+ if (hasAnnotation(RequiresIpv6MulticastRouting.class, description)) {
+ assumeTrue(
+ "Skipping test because kernel IPv6 multicast routing is unavailable",
+ hasIpv6MulticastRouting());
+ }
+
+ if (hasAnnotation(RequiresSimulationThreadDevice.class, description)) {
+ assumeTrue(
+ "Skipping test because simulation Thread device is unavailable",
+ hasSimulationThreadDevice());
+ }
+
+ base.evaluate();
+ }
+ };
+ }
+
+ /** Returns {@code true} if a test method or the test class is annotated with annotation. */
+ private <T extends Annotation> boolean hasAnnotation(
+ Class<T> annotationClass, Description description) {
+ // Method annotation
+ boolean hasAnnotation = description.getAnnotation(annotationClass) != null;
+
+ // Class annotation
+ Class<?> clazz = description.getTestClass();
+ while (!hasAnnotation && clazz != Object.class) {
+ hasAnnotation |= clazz.getAnnotation(annotationClass) != null;
+ clazz = clazz.getSuperclass();
+ }
+
+ return hasAnnotation;
+ }
+
+ /** Returns {@code true} if this device has the Thread feature supported. */
+ private static boolean hasThreadFeature() {
+ final Context context = ApplicationProvider.getApplicationContext();
+ return context.getSystemService(ThreadNetworkManager.class) != null;
+ }
+
+ /**
+ * Returns {@code true} if this device has the kernel IPv6 multicast routing feature enabled.
+ */
+ private static boolean hasIpv6MulticastRouting() {
+ // The kernel IPv6 multicast routing (i.e. IPV6_MROUTE) is enabled on kernel version
+ // android14-5.15.0 and later
+ return isKernelVersionAtLeast(KERNEL_VERSION_MULTICAST_ROUTING_SUPPORTED)
+ && isKernelAndroidVersionAtLeast(
+ KERNEL_ANDROID_VERSION_MULTICAST_ROUTING_SUPPORTED);
+ }
+
+ /**
+ * Returns {@code true} if the android version in the kernel version of this device is equal to
+ * or larger than the given {@code minVersion}.
+ */
+ private static boolean isKernelAndroidVersionAtLeast(int minVersion) {
+ final String osRelease = VintfRuntimeInfo.getOsRelease();
+ final Pattern pattern = Pattern.compile("android(\\d+)");
+ Matcher matcher = pattern.matcher(osRelease);
+
+ if (matcher.find()) {
+ int version = Integer.parseInt(matcher.group(1));
+ return (version >= minVersion);
+ }
+ return false;
+ }
+
+ /** Returns {@code true} if the simulation Thread device is supported. */
+ private static boolean hasSimulationThreadDevice() {
+ // Simulation radio is supported on only Cuttlefish
+ return SystemProperties.get("ro.product.model").startsWith("Cuttlefish");
+ }
+}