Merge "IpManager: use InitialConfiguration for provisioning"
am: 5b218bdc8e
Change-Id: I131589680b68222e4fda4e9165ac2f50894ee28e
diff --git a/services/net/java/android/net/ip/IpManager.java b/services/net/java/android/net/ip/IpManager.java
index 46c1292..facdb85 100644
--- a/services/net/java/android/net/ip/IpManager.java
+++ b/services/net/java/android/net/ip/IpManager.java
@@ -36,6 +36,7 @@
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.IpManagerEvent;
import android.net.util.MultinetworkPolicyTracker;
+import android.net.util.NetdService;
import android.net.util.NetworkConstants;
import android.net.util.SharedLog;
import android.os.INetworkManagementService;
@@ -66,9 +67,11 @@
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
+import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Predicate;
@@ -423,6 +426,10 @@
}
public boolean isValid() {
+ if (ipAddresses.isEmpty()) {
+ return false;
+ }
+
// For every IP address, there must be at least one prefix containing that address.
for (LinkAddress addr : ipAddresses) {
if (!any(directlyConnectedRoutes, (p) -> p.contains(addr.getAddress()))) {
@@ -459,14 +466,47 @@
return true;
}
+ /**
+ * @return true if the given list of addressess and routes satisfies provisioning for this
+ * InitialConfiguration. LinkAddresses and RouteInfo objects are not compared with equality
+ * because addresses and routes seen by Netlink will contain additional fields like flags,
+ * interfaces, and so on. If this InitialConfiguration has no IP address specified, the
+ * provisioning check always fails.
+ *
+ * If the given list of routes is null, only addresses are taken into considerations.
+ */
+ public boolean isProvisionedBy(List<LinkAddress> addresses, List<RouteInfo> routes) {
+ if (ipAddresses.isEmpty()) {
+ return false;
+ }
+
+ for (LinkAddress addr : ipAddresses) {
+ if (!any(addresses, (addrSeen) -> addr.isSameAddressAs(addrSeen))) {
+ return false;
+ }
+ }
+
+ if (routes != null) {
+ for (IpPrefix prefix : directlyConnectedRoutes) {
+ if (!any(routes, (routeSeen) -> isDirectlyConnectedRoute(routeSeen, prefix))) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean isDirectlyConnectedRoute(RouteInfo route, IpPrefix prefix) {
+ return !route.hasGateway() && prefix.equals(route.getDestination());
+ }
+
private static boolean isPrefixLengthCompliant(LinkAddress addr) {
- return (addr.getAddress() instanceof Inet4Address)
- || isCompliantIPv6PrefixLength(addr.getPrefixLength());
+ return addr.isIPv4() || isCompliantIPv6PrefixLength(addr.getPrefixLength());
}
private static boolean isPrefixLengthCompliant(IpPrefix prefix) {
- return (prefix.getAddress() instanceof Inet4Address)
- || isCompliantIPv6PrefixLength(prefix.getPrefixLength());
+ return prefix.isIPv4() || isCompliantIPv6PrefixLength(prefix.getPrefixLength());
}
private static boolean isCompliantIPv6PrefixLength(int prefixLength) {
@@ -479,28 +519,7 @@
}
private static boolean isIPv6GUA(LinkAddress addr) {
- return (addr.getAddress() instanceof Inet6Address) && addr.isGlobalPreferred();
- }
-
- private static <T> boolean any(Iterable<T> coll, Predicate<T> fn) {
- for (T t : coll) {
- if (fn.test(t)) {
- return true;
- }
- }
- return false;
- }
-
- private static <T> boolean all(Iterable<T> coll, Predicate<T> fn) {
- return !any(coll, not(fn));
- }
-
- private static <T> Predicate<T> not(Predicate<T> fn) {
- return (t) -> !fn.test(t);
- }
-
- private static <T> String join(String delimiter, Collection<T> coll) {
- return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter));
+ return addr.isIPv6() && addr.isGlobalPreferred();
}
}
@@ -549,6 +568,7 @@
private final LocalLog mConnectivityPacketLog;
private final MessageHandlingLogger mMsgStateLogger;
private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
+ private final INetd mNetd;
private NetworkInterface mNetworkInterface;
@@ -568,14 +588,22 @@
public IpManager(Context context, String ifName, Callback callback) {
this(context, ifName, callback, INetworkManagementService.Stub.asInterface(
- ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)));
+ ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)),
+ NetdService.getInstance());
}
/**
* An expanded constructor, useful for dependency injection.
+ * TODO: migrate all test users to mock IpManager directly and remove this ctor.
*/
public IpManager(Context context, String ifName, Callback callback,
INetworkManagementService nwService) {
+ this(context, ifName, callback, nwService, NetdService.getInstance());
+ }
+
+ @VisibleForTesting
+ IpManager(Context context, String ifName, Callback callback,
+ INetworkManagementService nwService, INetd netd) {
super(IpManager.class.getSimpleName() + "." + ifName);
mTag = getName();
@@ -584,6 +612,7 @@
mClatInterfaceName = CLAT_PREFIX + ifName;
mCallback = new LoggingCallbackWrapper(callback);
mNwService = nwService;
+ mNetd = netd;
mLog = new SharedLog(MAX_LOG_RECORDS, mTag);
mConnectivityPacketLog = new LocalLog(MAX_PACKET_RECORDS);
@@ -886,10 +915,20 @@
}
// For now: use WifiStateMachine's historical notion of provisioned.
- private static boolean isProvisioned(LinkProperties lp) {
+ @VisibleForTesting
+ static boolean isProvisioned(LinkProperties lp, InitialConfiguration config) {
// For historical reasons, we should connect even if all we have is
// an IPv4 address and nothing else.
- return lp.isProvisioned() || lp.hasIPv4Address();
+ if (lp.hasIPv4Address() || lp.isProvisioned()) {
+ return true;
+ }
+ if (config == null) {
+ return false;
+ }
+
+ // When an InitialConfiguration is specified, ignore any difference with previous
+ // properties and instead check if properties observed match the desired properties.
+ return config.isProvisionedBy(lp.getLinkAddresses(), lp.getRoutes());
}
// TODO: Investigate folding all this into the existing static function
@@ -898,12 +937,11 @@
// object that is a correct and complete assessment of what changed, taking
// account of the asymmetries described in the comments in this function.
// Then switch to using it everywhere (IpReachabilityMonitor, etc.).
- private ProvisioningChange compareProvisioning(
- LinkProperties oldLp, LinkProperties newLp) {
+ private ProvisioningChange compareProvisioning(LinkProperties oldLp, LinkProperties newLp) {
ProvisioningChange delta;
-
- final boolean wasProvisioned = isProvisioned(oldLp);
- final boolean isProvisioned = isProvisioned(newLp);
+ InitialConfiguration config = mConfiguration != null ? mConfiguration.mInitialConfig : null;
+ final boolean wasProvisioned = isProvisioned(oldLp, config);
+ final boolean isProvisioned = isProvisioned(newLp, config);
if (!wasProvisioned && isProvisioned) {
delta = ProvisioningChange.GAINED_PROVISIONING;
@@ -1016,10 +1054,6 @@
return delta;
}
- private boolean linkPropertiesUnchanged(LinkProperties newLp) {
- return Objects.equals(newLp, mLinkProperties);
- }
-
private LinkProperties assembleLinkProperties() {
// [1] Create a new LinkProperties object to populate.
LinkProperties newLp = new LinkProperties();
@@ -1066,9 +1100,26 @@
newLp.setHttpProxy(mHttpProxy);
}
- if (VDBG) {
- Log.d(mTag, "newLp{" + newLp + "}");
+ // [5] Add data from InitialConfiguration
+ if (mConfiguration != null && mConfiguration.mInitialConfig != null) {
+ InitialConfiguration config = mConfiguration.mInitialConfig;
+ // Add InitialConfiguration routes and dns server addresses once all addresses
+ // specified in the InitialConfiguration have been observed with Netlink.
+ if (config.isProvisionedBy(newLp.getLinkAddresses(), null)) {
+ for (IpPrefix prefix : config.directlyConnectedRoutes) {
+ newLp.addRoute(new RouteInfo(prefix, null, mInterfaceName));
+ }
+ }
+ addAllReachableDnsServers(newLp, config.dnsServers);
}
+ final LinkProperties oldLp = mLinkProperties;
+ if (VDBG) {
+ Log.d(mTag, String.format("Netlink-seen LPs: %s, new LPs: %s; old LPs: %s",
+ netlinkLinkProperties, newLp, oldLp));
+ }
+
+ // TODO: also learn via netlink routes specified by an InitialConfiguration and specified
+ // from a static IP v4 config instead of manually patching them in in steps [3] and [5].
return newLp;
}
@@ -1087,7 +1138,7 @@
// Returns false if we have lost provisioning, true otherwise.
private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
final LinkProperties newLp = assembleLinkProperties();
- if (linkPropertiesUnchanged(newLp)) {
+ if (Objects.equals(newLp, mLinkProperties)) {
return true;
}
final ProvisioningChange delta = setLinkProperties(newLp);
@@ -1218,6 +1269,26 @@
return true;
}
+ private boolean applyInitialConfig(InitialConfiguration config) {
+ if (mNetd == null) {
+ logError("tried to add %s to %s but INetd was null", config, mInterfaceName);
+ return false;
+ }
+
+ // TODO: also support specifying a static IPv4 configuration in InitialConfiguration.
+ for (LinkAddress addr : findAll(config.ipAddresses, LinkAddress::isIPv6)) {
+ try {
+ mNetd.interfaceAddAddress(
+ mInterfaceName, addr.getAddress().getHostAddress(), addr.getPrefixLength());
+ } catch (ServiceSpecificException | RemoteException e) {
+ logError("failed to add %s to %s: %s", addr, mInterfaceName, e);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
private boolean startIpReachabilityMonitor() {
try {
mIpReachabilityMonitor = new IpReachabilityMonitor(
@@ -1446,6 +1517,14 @@
return;
}
+ InitialConfiguration config = mConfiguration.mInitialConfig;
+ if ((config != null) && !applyInitialConfig(config)) {
+ // TODO introduce a new IpManagerEvent constant to distinguish this error case.
+ doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
+ transitionTo(mStoppingState);
+ return;
+ }
+
if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) {
doImmediateProvisioningFailure(
IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR);
@@ -1652,4 +1731,39 @@
receivedInState, processedInState);
}
}
+
+ // TODO: extract out into CollectionUtils.
+ static <T> boolean any(Iterable<T> coll, Predicate<T> fn) {
+ for (T t : coll) {
+ if (fn.test(t)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static <T> boolean all(Iterable<T> coll, Predicate<T> fn) {
+ return !any(coll, not(fn));
+ }
+
+ static <T> Predicate<T> not(Predicate<T> fn) {
+ return (t) -> !fn.test(t);
+ }
+
+ static <T> String join(String delimiter, Collection<T> coll) {
+ return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter));
+ }
+
+ static <T> T find(Iterable<T> coll, Predicate<T> fn) {
+ for (T t: coll) {
+ if (fn.test(t)) {
+ return t;
+ }
+ }
+ return null;
+ }
+
+ static <T> List<T> findAll(Collection<T> coll, Predicate<T> fn) {
+ return coll.stream().filter(fn).collect(Collectors.toList());
+ }
}
diff --git a/tests/net/java/android/net/ip/IpManagerTest.java b/tests/net/java/android/net/ip/IpManagerTest.java
index dc77e22..541f91ad 100644
--- a/tests/net/java/android/net/ip/IpManagerTest.java
+++ b/tests/net/java/android/net/ip/IpManagerTest.java
@@ -17,6 +17,8 @@
package android.net.ip;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
@@ -32,8 +34,11 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
+import android.net.INetd;
import android.net.IpPrefix;
import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
import android.net.ip.IpManager.Callback;
import android.net.ip.IpManager.InitialConfiguration;
import android.net.ip.IpManager.ProvisioningConfiguration;
@@ -45,16 +50,20 @@
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.internal.R;
+import com.android.server.net.BaseNetworkObserver;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.List;
import java.util.HashSet;
import java.util.Set;
@@ -71,11 +80,14 @@
@Mock private Context mContext;
@Mock private INetworkManagementService mNMService;
+ @Mock private INetd mNetd;
@Mock private Resources mResources;
@Mock private Callback mCb;
@Mock private AlarmManager mAlarm;
private MockContentResolver mContentResolver;
+ BaseNetworkObserver mObserver;
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -91,9 +103,13 @@
}
private IpManager makeIpManager(String ifname) throws Exception {
- final IpManager ipm = new IpManager(mContext, ifname, mCb, mNMService);
+ final IpManager ipm = new IpManager(mContext, ifname, mCb, mNMService, mNetd);
verify(mNMService, timeout(100).times(1)).disableIpv6(ifname);
verify(mNMService, timeout(100).times(1)).clearInterfaceAddresses(ifname);
+ ArgumentCaptor<BaseNetworkObserver> arg =
+ ArgumentCaptor.forClass(BaseNetworkObserver.class);
+ verify(mNMService, times(1)).registerObserver(arg.capture());
+ mObserver = arg.getValue();
reset(mNMService);
return ipm;
}
@@ -131,6 +147,134 @@
}
@Test
+ public void testProvisioningWithInitialConfiguration() throws Exception {
+ final String iface = "test_wlan0";
+ final IpManager ipm = makeIpManager(iface);
+
+ String[] addresses = {
+ "fe80::a4be:f92:e1f7:22d1/64",
+ "fe80::f04a:8f6:6a32:d756/64",
+ "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"
+ };
+ String[] prefixes = { "fe80::/64", "fd2c:4e57:8e3c::/64" };
+
+ ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+ .withoutIPv4()
+ .withoutIpReachabilityMonitor()
+ .withInitialConfiguration(conf(links(addresses), prefixes(prefixes), ips()))
+ .build();
+
+ ipm.startProvisioning(config);
+ verify(mCb, times(1)).setNeighborDiscoveryOffload(true);
+ verify(mCb, timeout(100).times(1)).setFallbackMulticastFilter(false);
+ verify(mCb, never()).onProvisioningFailure(any());
+
+ for (String addr : addresses) {
+ String[] parts = addr.split("/");
+ verify(mNetd, timeout(100).times(1))
+ .interfaceAddAddress(iface, parts[0], Integer.parseInt(parts[1]));
+ }
+
+ final int lastAddr = addresses.length - 1;
+
+ // Add N - 1 addresses
+ for (int i = 0; i < lastAddr; i++) {
+ mObserver.addressUpdated(iface, new LinkAddress(addresses[i]));
+ verify(mCb, timeout(100).times(1)).onLinkPropertiesChange(any());
+ }
+
+ // Add Nth address
+ mObserver.addressUpdated(iface, new LinkAddress(addresses[lastAddr]));
+ LinkProperties want = linkproperties(links(addresses), routes(prefixes));
+ want.setInterfaceName(iface);
+ verify(mCb, timeout(100).times(1)).onProvisioningSuccess(eq(want));
+
+ ipm.stop();
+ verify(mNMService, timeout(100).times(1)).disableIpv6(iface);
+ verify(mNMService, timeout(100).times(1)).clearInterfaceAddresses(iface);
+ }
+
+ @Test
+ public void testIsProvisioned() throws Exception {
+ InitialConfiguration empty = conf(links(), prefixes());
+ IsProvisionedTestCase[] testcases = {
+ // nothing
+ notProvisionedCase(links(), routes(), dns(), null),
+ notProvisionedCase(links(), routes(), dns(), empty),
+
+ // IPv4
+ provisionedCase(links("192.0.2.12/24"), routes(), dns(), empty),
+
+ // IPv6
+ notProvisionedCase(
+ links("fe80::a4be:f92:e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"),
+ routes(), dns(), empty),
+ notProvisionedCase(
+ links("fe80::a4be:f92:e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"),
+ routes("fe80::/64", "fd2c:4e57:8e3c::/64"), dns("fd00:1234:5678::1000"), empty),
+ provisionedCase(
+ links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"),
+ routes("::/0"),
+ dns("2001:db8:dead:beef:f00::02"), empty),
+
+ // Initial configuration
+ provisionedCase(
+ links("fe80::e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"),
+ routes("fe80::/64", "fd2c:4e57:8e3c::/64"),
+ dns(),
+ conf(links("fe80::e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"),
+ prefixes( "fe80::/64", "fd2c:4e57:8e3c::/64"), ips()))
+ };
+
+ for (IsProvisionedTestCase testcase : testcases) {
+ if (IpManager.isProvisioned(testcase.lp, testcase.config) != testcase.isProvisioned) {
+ fail(testcase.errorMessage());
+ }
+ }
+ }
+
+ static class IsProvisionedTestCase {
+ boolean isProvisioned;
+ LinkProperties lp;
+ InitialConfiguration config;
+
+ String errorMessage() {
+ return String.format("expected %s with config %s to be %s, but was %s",
+ lp, config, provisioned(isProvisioned), provisioned(!isProvisioned));
+ }
+
+ static String provisioned(boolean isProvisioned) {
+ return isProvisioned ? "provisioned" : "not provisioned";
+ }
+ }
+
+ static IsProvisionedTestCase provisionedCase(Set<LinkAddress> lpAddrs, Set<RouteInfo> lpRoutes,
+ Set<InetAddress> lpDns, InitialConfiguration config) {
+ return provisioningTest(true, lpAddrs, lpRoutes, lpDns, config);
+ }
+
+ static IsProvisionedTestCase notProvisionedCase(Set<LinkAddress> lpAddrs,
+ Set<RouteInfo> lpRoutes, Set<InetAddress> lpDns, InitialConfiguration config) {
+ return provisioningTest(false, lpAddrs, lpRoutes, lpDns, config);
+ }
+
+ static IsProvisionedTestCase provisioningTest(boolean isProvisioned, Set<LinkAddress> lpAddrs,
+ Set<RouteInfo> lpRoutes, Set<InetAddress> lpDns, InitialConfiguration config) {
+ IsProvisionedTestCase testcase = new IsProvisionedTestCase();
+ testcase.isProvisioned = isProvisioned;
+ testcase.lp = new LinkProperties();
+ testcase.lp.setLinkAddresses(lpAddrs);
+ for (RouteInfo route : lpRoutes) {
+ testcase.lp.addRoute(route);
+ }
+ for (InetAddress dns : lpDns) {
+ testcase.lp.addDnsServer(dns);
+ }
+ testcase.config = config;
+ return testcase;
+ }
+
+ @Test
public void testInitialConfigurations() throws Exception {
InitialConfigurationTestCase[] testcases = {
validConf("valid IPv4 configuration",
@@ -152,6 +296,7 @@
prefixes("fd00:1234:5678::/48"),
dns("fd00:1234:5678::1000")),
+ invalidConf("empty configuration", links(), prefixes(), dns()),
invalidConf("v4 addr and dns not in any prefix",
links("192.0.2.12/24"), prefixes("198.51.100.0/24"), dns("192.0.2.2")),
invalidConf("v4 addr not in any prefix",
@@ -189,10 +334,9 @@
return String.format("%s: expected configuration %s to be %s, but was %s",
descr, config, validString(isValid), validString(!isValid));
}
- }
-
- static String validString(boolean isValid) {
- return isValid ? VALID : INVALID;
+ static String validString(boolean isValid) {
+ return isValid ? VALID : INVALID;
+ }
}
static InitialConfigurationTestCase validConf(String descr, Set<LinkAddress> links,
@@ -214,6 +358,19 @@
return testcase;
}
+ static LinkProperties linkproperties(Set<LinkAddress> addresses, Set<RouteInfo> routes) {
+ LinkProperties lp = new LinkProperties();
+ lp.setLinkAddresses(addresses);
+ for (RouteInfo route : routes) {
+ lp.addRoute(route);
+ }
+ return lp;
+ }
+
+ static InitialConfiguration conf(Set<LinkAddress> links, Set<IpPrefix> prefixes) {
+ return conf(links, prefixes, new HashSet<>());
+ }
+
static InitialConfiguration conf(
Set<LinkAddress> links, Set<IpPrefix> prefixes, Set<InetAddress> dns) {
InitialConfiguration conf = new InitialConfiguration();
@@ -223,6 +380,10 @@
return conf;
}
+ static Set<RouteInfo> routes(String... routes) {
+ return mapIntoSet(routes, (r) -> new RouteInfo(new IpPrefix(r)));
+ }
+
static Set<IpPrefix> prefixes(String... prefixes) {
return mapIntoSet(prefixes, IpPrefix::new);
}
@@ -254,4 +415,44 @@
interface Fn<A,B> {
B call(A a) throws Exception;
}
+
+ @Test
+ public void testAll() {
+ List<String> list1 = Arrays.asList();
+ List<String> list2 = Arrays.asList("foo");
+ List<String> list3 = Arrays.asList("bar", "baz");
+ List<String> list4 = Arrays.asList("foo", "bar", "baz");
+
+ assertTrue(IpManager.all(list1, (x) -> false));
+ assertFalse(IpManager.all(list2, (x) -> false));
+ assertTrue(IpManager.all(list3, (x) -> true));
+ assertTrue(IpManager.all(list2, (x) -> x.charAt(0) == 'f'));
+ assertFalse(IpManager.all(list4, (x) -> x.charAt(0) == 'f'));
+ }
+
+ @Test
+ public void testAny() {
+ List<String> list1 = Arrays.asList();
+ List<String> list2 = Arrays.asList("foo");
+ List<String> list3 = Arrays.asList("bar", "baz");
+ List<String> list4 = Arrays.asList("foo", "bar", "baz");
+
+ assertFalse(IpManager.any(list1, (x) -> true));
+ assertTrue(IpManager.any(list2, (x) -> true));
+ assertTrue(IpManager.any(list2, (x) -> x.charAt(0) == 'f'));
+ assertFalse(IpManager.any(list3, (x) -> x.charAt(0) == 'f'));
+ assertTrue(IpManager.any(list4, (x) -> x.charAt(0) == 'f'));
+ }
+
+ @Test
+ public void testFindAll() {
+ List<String> list1 = Arrays.asList();
+ List<String> list2 = Arrays.asList("foo");
+ List<String> list3 = Arrays.asList("foo", "bar", "baz");
+
+ assertEquals(list1, IpManager.findAll(list1, (x) -> true));
+ assertEquals(list1, IpManager.findAll(list3, (x) -> false));
+ assertEquals(list3, IpManager.findAll(list3, (x) -> true));
+ assertEquals(list2, IpManager.findAll(list3, (x) -> x.charAt(0) == 'f'));
+ }
}