Additionally, no longer try to transition from within a State's
enter() method (this can encounter Log.wtf()s). Introduce some
CMD_JUMP_* commands and use deferMessage().
Test: as follows
- built
- flashed
- booted
- runtest frameworks-net passes (except for IpConnectivityMetricsTest failures)
- manual changing from DHCP to static configurations works:
2017-12-11T19:06:19.082 - INVOKE onLinkPropertiesChange({{InterfaceName: wlan0 LinkAddresses: [] Routes: [] DnsAddresses: [] Domains: null MTU: 0}})
Bug: 69800563
Bug: 70394432
Change-Id: Ice249a48b66806c0270ec3f11dd2e8e387d4e29b
1746 lines
67 KiB
Java
1746 lines
67 KiB
Java
/*
|
||
* Copyright (C) 2017 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.ip;
|
||
|
||
import com.android.internal.util.MessageUtils;
|
||
import com.android.internal.util.WakeupMessage;
|
||
|
||
import android.content.Context;
|
||
import android.net.DhcpResults;
|
||
import android.net.INetd;
|
||
import android.net.IpPrefix;
|
||
import android.net.LinkAddress;
|
||
import android.net.LinkProperties.ProvisioningChange;
|
||
import android.net.LinkProperties;
|
||
import android.net.Network;
|
||
import android.net.ProxyInfo;
|
||
import android.net.RouteInfo;
|
||
import android.net.StaticIpConfiguration;
|
||
import android.net.apf.ApfCapabilities;
|
||
import android.net.apf.ApfFilter;
|
||
import android.net.dhcp.DhcpClient;
|
||
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;
|
||
import android.os.Message;
|
||
import android.os.RemoteException;
|
||
import android.os.ServiceManager;
|
||
import android.os.SystemClock;
|
||
import android.text.TextUtils;
|
||
import android.util.LocalLog;
|
||
import android.util.Log;
|
||
import android.util.SparseArray;
|
||
|
||
import com.android.internal.annotations.VisibleForTesting;
|
||
import com.android.internal.R;
|
||
import com.android.internal.util.IndentingPrintWriter;
|
||
import com.android.internal.util.IState;
|
||
import com.android.internal.util.Preconditions;
|
||
import com.android.internal.util.State;
|
||
import com.android.internal.util.StateMachine;
|
||
import com.android.server.net.NetlinkTracker;
|
||
|
||
import java.io.FileDescriptor;
|
||
import java.io.PrintWriter;
|
||
import java.net.Inet4Address;
|
||
import java.net.Inet6Address;
|
||
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;
|
||
import java.util.stream.Collectors;
|
||
|
||
|
||
/**
|
||
* IpClient
|
||
*
|
||
* This class provides the interface to IP-layer provisioning and maintenance
|
||
* functionality that can be used by transport layers like Wi-Fi, Ethernet,
|
||
* et cetera.
|
||
*
|
||
* [ Lifetime ]
|
||
* IpClient is designed to be instantiated as soon as the interface name is
|
||
* known and can be as long-lived as the class containing it (i.e. declaring
|
||
* it "private final" is okay).
|
||
*
|
||
* @hide
|
||
*/
|
||
public class IpClient extends StateMachine {
|
||
private static final boolean DBG = false;
|
||
|
||
// For message logging.
|
||
private static final Class[] sMessageClasses = { IpClient.class, DhcpClient.class };
|
||
private static final SparseArray<String> sWhatToString =
|
||
MessageUtils.findMessageNames(sMessageClasses);
|
||
|
||
/**
|
||
* Callbacks for handling IpClient events.
|
||
*/
|
||
public static class Callback {
|
||
// In order to receive onPreDhcpAction(), call #withPreDhcpAction()
|
||
// when constructing a ProvisioningConfiguration.
|
||
//
|
||
// Implementations of onPreDhcpAction() must call
|
||
// IpClient#completedPreDhcpAction() to indicate that DHCP is clear
|
||
// to proceed.
|
||
public void onPreDhcpAction() {}
|
||
public void onPostDhcpAction() {}
|
||
|
||
// This is purely advisory and not an indication of provisioning
|
||
// success or failure. This is only here for callers that want to
|
||
// expose DHCPv4 results to other APIs (e.g., WifiInfo#setInetAddress).
|
||
// DHCPv4 or static IPv4 configuration failure or success can be
|
||
// determined by whether or not the passed-in DhcpResults object is
|
||
// null or not.
|
||
public void onNewDhcpResults(DhcpResults dhcpResults) {}
|
||
|
||
public void onProvisioningSuccess(LinkProperties newLp) {}
|
||
public void onProvisioningFailure(LinkProperties newLp) {}
|
||
|
||
// Invoked on LinkProperties changes.
|
||
public void onLinkPropertiesChange(LinkProperties newLp) {}
|
||
|
||
// Called when the internal IpReachabilityMonitor (if enabled) has
|
||
// detected the loss of a critical number of required neighbors.
|
||
public void onReachabilityLost(String logMsg) {}
|
||
|
||
// Called when the IpClient state machine terminates.
|
||
public void onQuit() {}
|
||
|
||
// Install an APF program to filter incoming packets.
|
||
public void installPacketFilter(byte[] filter) {}
|
||
|
||
// If multicast filtering cannot be accomplished with APF, this function will be called to
|
||
// actuate multicast filtering using another means.
|
||
public void setFallbackMulticastFilter(boolean enabled) {}
|
||
|
||
// Enabled/disable Neighbor Discover offload functionality. This is
|
||
// called, for example, whenever 464xlat is being started or stopped.
|
||
public void setNeighborDiscoveryOffload(boolean enable) {}
|
||
}
|
||
|
||
// Use a wrapper class to log in order to ensure complete and detailed
|
||
// logging. This method is lighter weight than annotations/reflection
|
||
// and has the following benefits:
|
||
//
|
||
// - No invoked method can be forgotten.
|
||
// Any new method added to IpClient.Callback must be overridden
|
||
// here or it will never be called.
|
||
//
|
||
// - No invoking call site can be forgotten.
|
||
// Centralized logging in this way means call sites don't need to
|
||
// remember to log, and therefore no call site can be forgotten.
|
||
//
|
||
// - No variation in log format among call sites.
|
||
// Encourages logging of any available arguments, and all call sites
|
||
// are necessarily logged identically.
|
||
//
|
||
// TODO: Find an lighter weight approach.
|
||
private class LoggingCallbackWrapper extends Callback {
|
||
private static final String PREFIX = "INVOKE ";
|
||
private final Callback mCallback;
|
||
|
||
public LoggingCallbackWrapper(Callback callback) {
|
||
mCallback = (callback != null) ? callback : new Callback();
|
||
}
|
||
|
||
private void log(String msg) {
|
||
mLog.log(PREFIX + msg);
|
||
}
|
||
|
||
@Override
|
||
public void onPreDhcpAction() {
|
||
mCallback.onPreDhcpAction();
|
||
log("onPreDhcpAction()");
|
||
}
|
||
@Override
|
||
public void onPostDhcpAction() {
|
||
mCallback.onPostDhcpAction();
|
||
log("onPostDhcpAction()");
|
||
}
|
||
@Override
|
||
public void onNewDhcpResults(DhcpResults dhcpResults) {
|
||
mCallback.onNewDhcpResults(dhcpResults);
|
||
log("onNewDhcpResults({" + dhcpResults + "})");
|
||
}
|
||
@Override
|
||
public void onProvisioningSuccess(LinkProperties newLp) {
|
||
mCallback.onProvisioningSuccess(newLp);
|
||
log("onProvisioningSuccess({" + newLp + "})");
|
||
}
|
||
@Override
|
||
public void onProvisioningFailure(LinkProperties newLp) {
|
||
mCallback.onProvisioningFailure(newLp);
|
||
log("onProvisioningFailure({" + newLp + "})");
|
||
}
|
||
@Override
|
||
public void onLinkPropertiesChange(LinkProperties newLp) {
|
||
mCallback.onLinkPropertiesChange(newLp);
|
||
log("onLinkPropertiesChange({" + newLp + "})");
|
||
}
|
||
@Override
|
||
public void onReachabilityLost(String logMsg) {
|
||
mCallback.onReachabilityLost(logMsg);
|
||
log("onReachabilityLost(" + logMsg + ")");
|
||
}
|
||
@Override
|
||
public void onQuit() {
|
||
mCallback.onQuit();
|
||
log("onQuit()");
|
||
}
|
||
@Override
|
||
public void installPacketFilter(byte[] filter) {
|
||
mCallback.installPacketFilter(filter);
|
||
log("installPacketFilter(byte[" + filter.length + "])");
|
||
}
|
||
@Override
|
||
public void setFallbackMulticastFilter(boolean enabled) {
|
||
mCallback.setFallbackMulticastFilter(enabled);
|
||
log("setFallbackMulticastFilter(" + enabled + ")");
|
||
}
|
||
@Override
|
||
public void setNeighborDiscoveryOffload(boolean enable) {
|
||
mCallback.setNeighborDiscoveryOffload(enable);
|
||
log("setNeighborDiscoveryOffload(" + enable + ")");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* This class encapsulates parameters to be passed to
|
||
* IpClient#startProvisioning(). A defensive copy is made by IpClient
|
||
* and the values specified herein are in force until IpClient#stop()
|
||
* is called.
|
||
*
|
||
* Example use:
|
||
*
|
||
* final ProvisioningConfiguration config =
|
||
* mIpClient.buildProvisioningConfiguration()
|
||
* .withPreDhcpAction()
|
||
* .withProvisioningTimeoutMs(36 * 1000)
|
||
* .build();
|
||
* mIpClient.startProvisioning(config);
|
||
* ...
|
||
* mIpClient.stop();
|
||
*
|
||
* The specified provisioning configuration will only be active until
|
||
* IpClient#stop() is called. Future calls to IpClient#startProvisioning()
|
||
* must specify the configuration again.
|
||
*/
|
||
public static class ProvisioningConfiguration {
|
||
// TODO: Delete this default timeout once those callers that care are
|
||
// fixed to pass in their preferred timeout.
|
||
//
|
||
// We pick 36 seconds so we can send DHCP requests at
|
||
//
|
||
// t=0, t=2, t=6, t=14, t=30
|
||
//
|
||
// allowing for 10% jitter.
|
||
private static final int DEFAULT_TIMEOUT_MS = 36 * 1000;
|
||
|
||
public static class Builder {
|
||
private ProvisioningConfiguration mConfig = new ProvisioningConfiguration();
|
||
|
||
public Builder withoutIPv4() {
|
||
mConfig.mEnableIPv4 = false;
|
||
return this;
|
||
}
|
||
|
||
public Builder withoutIPv6() {
|
||
mConfig.mEnableIPv6 = false;
|
||
return this;
|
||
}
|
||
|
||
public Builder withoutIpReachabilityMonitor() {
|
||
mConfig.mUsingIpReachabilityMonitor = false;
|
||
return this;
|
||
}
|
||
|
||
public Builder withPreDhcpAction() {
|
||
mConfig.mRequestedPreDhcpActionMs = DEFAULT_TIMEOUT_MS;
|
||
return this;
|
||
}
|
||
|
||
public Builder withPreDhcpAction(int dhcpActionTimeoutMs) {
|
||
mConfig.mRequestedPreDhcpActionMs = dhcpActionTimeoutMs;
|
||
return this;
|
||
}
|
||
|
||
public Builder withInitialConfiguration(InitialConfiguration initialConfig) {
|
||
mConfig.mInitialConfig = initialConfig;
|
||
return this;
|
||
}
|
||
|
||
public Builder withStaticConfiguration(StaticIpConfiguration staticConfig) {
|
||
mConfig.mStaticIpConfig = staticConfig;
|
||
return this;
|
||
}
|
||
|
||
public Builder withApfCapabilities(ApfCapabilities apfCapabilities) {
|
||
mConfig.mApfCapabilities = apfCapabilities;
|
||
return this;
|
||
}
|
||
|
||
public Builder withProvisioningTimeoutMs(int timeoutMs) {
|
||
mConfig.mProvisioningTimeoutMs = timeoutMs;
|
||
return this;
|
||
}
|
||
|
||
public Builder withRandomMacAddress() {
|
||
mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_EUI64;
|
||
return this;
|
||
}
|
||
|
||
public Builder withStableMacAddress() {
|
||
mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;
|
||
return this;
|
||
}
|
||
|
||
public Builder withNetwork(Network network) {
|
||
mConfig.mNetwork = network;
|
||
return this;
|
||
}
|
||
|
||
public Builder withDisplayName(String displayName) {
|
||
mConfig.mDisplayName = displayName;
|
||
return this;
|
||
}
|
||
|
||
public ProvisioningConfiguration build() {
|
||
return new ProvisioningConfiguration(mConfig);
|
||
}
|
||
}
|
||
|
||
/* package */ boolean mEnableIPv4 = true;
|
||
/* package */ boolean mEnableIPv6 = true;
|
||
/* package */ boolean mUsingIpReachabilityMonitor = true;
|
||
/* package */ int mRequestedPreDhcpActionMs;
|
||
/* package */ InitialConfiguration mInitialConfig;
|
||
/* package */ StaticIpConfiguration mStaticIpConfig;
|
||
/* package */ ApfCapabilities mApfCapabilities;
|
||
/* package */ int mProvisioningTimeoutMs = DEFAULT_TIMEOUT_MS;
|
||
/* package */ int mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;
|
||
/* package */ Network mNetwork = null;
|
||
/* package */ String mDisplayName = null;
|
||
|
||
public ProvisioningConfiguration() {} // used by Builder
|
||
|
||
public ProvisioningConfiguration(ProvisioningConfiguration other) {
|
||
mEnableIPv4 = other.mEnableIPv4;
|
||
mEnableIPv6 = other.mEnableIPv6;
|
||
mUsingIpReachabilityMonitor = other.mUsingIpReachabilityMonitor;
|
||
mRequestedPreDhcpActionMs = other.mRequestedPreDhcpActionMs;
|
||
mInitialConfig = InitialConfiguration.copy(other.mInitialConfig);
|
||
mStaticIpConfig = other.mStaticIpConfig;
|
||
mApfCapabilities = other.mApfCapabilities;
|
||
mProvisioningTimeoutMs = other.mProvisioningTimeoutMs;
|
||
mIPv6AddrGenMode = other.mIPv6AddrGenMode;
|
||
mNetwork = other.mNetwork;
|
||
mDisplayName = other.mDisplayName;
|
||
}
|
||
|
||
@Override
|
||
public String toString() {
|
||
return new StringJoiner(", ", getClass().getSimpleName() + "{", "}")
|
||
.add("mEnableIPv4: " + mEnableIPv4)
|
||
.add("mEnableIPv6: " + mEnableIPv6)
|
||
.add("mUsingIpReachabilityMonitor: " + mUsingIpReachabilityMonitor)
|
||
.add("mRequestedPreDhcpActionMs: " + mRequestedPreDhcpActionMs)
|
||
.add("mInitialConfig: " + mInitialConfig)
|
||
.add("mStaticIpConfig: " + mStaticIpConfig)
|
||
.add("mApfCapabilities: " + mApfCapabilities)
|
||
.add("mProvisioningTimeoutMs: " + mProvisioningTimeoutMs)
|
||
.add("mIPv6AddrGenMode: " + mIPv6AddrGenMode)
|
||
.add("mNetwork: " + mNetwork)
|
||
.add("mDisplayName: " + mDisplayName)
|
||
.toString();
|
||
}
|
||
|
||
public boolean isValid() {
|
||
return (mInitialConfig == null) || mInitialConfig.isValid();
|
||
}
|
||
}
|
||
|
||
public static class InitialConfiguration {
|
||
public final Set<LinkAddress> ipAddresses = new HashSet<>();
|
||
public final Set<IpPrefix> directlyConnectedRoutes = new HashSet<>();
|
||
public final Set<InetAddress> dnsServers = new HashSet<>();
|
||
public Inet4Address gateway; // WiFi legacy behavior with static ipv4 config
|
||
|
||
public static InitialConfiguration copy(InitialConfiguration config) {
|
||
if (config == null) {
|
||
return null;
|
||
}
|
||
InitialConfiguration configCopy = new InitialConfiguration();
|
||
configCopy.ipAddresses.addAll(config.ipAddresses);
|
||
configCopy.directlyConnectedRoutes.addAll(config.directlyConnectedRoutes);
|
||
configCopy.dnsServers.addAll(config.dnsServers);
|
||
return configCopy;
|
||
}
|
||
|
||
@Override
|
||
public String toString() {
|
||
return String.format(
|
||
"InitialConfiguration(IPs: {%s}, prefixes: {%s}, DNS: {%s}, v4 gateway: %s)",
|
||
join(", ", ipAddresses), join(", ", directlyConnectedRoutes),
|
||
join(", ", dnsServers), gateway);
|
||
}
|
||
|
||
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()))) {
|
||
return false;
|
||
}
|
||
}
|
||
// For every dns server, there must be at least one prefix containing that address.
|
||
for (InetAddress addr : dnsServers) {
|
||
if (!any(directlyConnectedRoutes, (p) -> p.contains(addr))) {
|
||
return false;
|
||
}
|
||
}
|
||
// All IPv6 LinkAddresses have an RFC7421-suitable prefix length
|
||
// (read: compliant with RFC4291#section2.5.4).
|
||
if (any(ipAddresses, not(InitialConfiguration::isPrefixLengthCompliant))) {
|
||
return false;
|
||
}
|
||
// If directlyConnectedRoutes contains an IPv6 default route
|
||
// then ipAddresses MUST contain at least one non-ULA GUA.
|
||
if (any(directlyConnectedRoutes, InitialConfiguration::isIPv6DefaultRoute)
|
||
&& all(ipAddresses, not(InitialConfiguration::isIPv6GUA))) {
|
||
return false;
|
||
}
|
||
// The prefix length of routes in directlyConnectedRoutes be within reasonable
|
||
// bounds for IPv6: /48-/64 just as we’d accept in RIOs.
|
||
if (any(directlyConnectedRoutes, not(InitialConfiguration::isPrefixLengthCompliant))) {
|
||
return false;
|
||
}
|
||
// There no more than one IPv4 address
|
||
if (ipAddresses.stream().filter(Inet4Address.class::isInstance).count() > 1) {
|
||
return false;
|
||
}
|
||
|
||
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.isIPv4() || isCompliantIPv6PrefixLength(addr.getPrefixLength());
|
||
}
|
||
|
||
private static boolean isPrefixLengthCompliant(IpPrefix prefix) {
|
||
return prefix.isIPv4() || isCompliantIPv6PrefixLength(prefix.getPrefixLength());
|
||
}
|
||
|
||
private static boolean isCompliantIPv6PrefixLength(int prefixLength) {
|
||
return (NetworkConstants.RFC6177_MIN_PREFIX_LENGTH <= prefixLength)
|
||
&& (prefixLength <= NetworkConstants.RFC7421_PREFIX_LENGTH);
|
||
}
|
||
|
||
private static boolean isIPv6DefaultRoute(IpPrefix prefix) {
|
||
return prefix.getAddress().equals(Inet6Address.ANY);
|
||
}
|
||
|
||
private static boolean isIPv6GUA(LinkAddress addr) {
|
||
return addr.isIPv6() && addr.isGlobalPreferred();
|
||
}
|
||
}
|
||
|
||
public static final String DUMP_ARG = "ipclient";
|
||
public static final String DUMP_ARG_CONFIRM = "confirm";
|
||
|
||
private static final int CMD_TERMINATE_AFTER_STOP = 1;
|
||
private static final int CMD_STOP = 2;
|
||
private static final int CMD_START = 3;
|
||
private static final int CMD_CONFIRM = 4;
|
||
private static final int EVENT_PRE_DHCP_ACTION_COMPLETE = 5;
|
||
// Sent by NetlinkTracker to communicate netlink events.
|
||
private static final int EVENT_NETLINK_LINKPROPERTIES_CHANGED = 6;
|
||
private static final int CMD_UPDATE_TCP_BUFFER_SIZES = 7;
|
||
private static final int CMD_UPDATE_HTTP_PROXY = 8;
|
||
private static final int CMD_SET_MULTICAST_FILTER = 9;
|
||
private static final int EVENT_PROVISIONING_TIMEOUT = 10;
|
||
private static final int EVENT_DHCPACTION_TIMEOUT = 11;
|
||
|
||
// Internal commands to use instead of trying to call transitionTo() inside
|
||
// a given State's enter() method. Calling transitionTo() from enter/exit
|
||
// encounters a Log.wtf() that can cause trouble on eng builds.
|
||
private static final int CMD_JUMP_STARTING_TO_RUNNING = 100;
|
||
private static final int CMD_JUMP_RUNNING_TO_STOPPING = 101;
|
||
private static final int CMD_JUMP_STOPPING_TO_STOPPED = 102;
|
||
|
||
private static final int MAX_LOG_RECORDS = 500;
|
||
private static final int MAX_PACKET_RECORDS = 100;
|
||
|
||
private static final boolean NO_CALLBACKS = false;
|
||
private static final boolean SEND_CALLBACKS = true;
|
||
|
||
// This must match the interface prefix in clatd.c.
|
||
// TODO: Revert this hack once IpClient and Nat464Xlat work in concert.
|
||
private static final String CLAT_PREFIX = "v4-";
|
||
|
||
private final State mStoppedState = new StoppedState();
|
||
private final State mStoppingState = new StoppingState();
|
||
private final State mStartedState = new StartedState();
|
||
private final State mRunningState = new RunningState();
|
||
|
||
private final String mTag;
|
||
private final Context mContext;
|
||
private final String mInterfaceName;
|
||
private final String mClatInterfaceName;
|
||
@VisibleForTesting
|
||
protected final Callback mCallback;
|
||
private final INetworkManagementService mNwService;
|
||
private final NetlinkTracker mNetlinkTracker;
|
||
private final WakeupMessage mProvisioningTimeoutAlarm;
|
||
private final WakeupMessage mDhcpActionTimeoutAlarm;
|
||
private final MultinetworkPolicyTracker mMultinetworkPolicyTracker;
|
||
private final SharedLog mLog;
|
||
private final LocalLog mConnectivityPacketLog;
|
||
private final MessageHandlingLogger mMsgStateLogger;
|
||
private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
|
||
private final InterfaceController mInterfaceCtrl;
|
||
|
||
private NetworkInterface mNetworkInterface;
|
||
|
||
/**
|
||
* Non-final member variables accessed only from within our StateMachine.
|
||
*/
|
||
private LinkProperties mLinkProperties;
|
||
private ProvisioningConfiguration mConfiguration;
|
||
private IpReachabilityMonitor mIpReachabilityMonitor;
|
||
private DhcpClient mDhcpClient;
|
||
private DhcpResults mDhcpResults;
|
||
private String mTcpBufferSizes;
|
||
private ProxyInfo mHttpProxy;
|
||
private ApfFilter mApfFilter;
|
||
private boolean mMulticastFiltering;
|
||
private long mStartTimeMillis;
|
||
|
||
public IpClient(Context context, String ifName, Callback callback) {
|
||
this(context, ifName, callback, INetworkManagementService.Stub.asInterface(
|
||
ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)),
|
||
NetdService.getInstance());
|
||
}
|
||
|
||
/**
|
||
* An expanded constructor, useful for dependency injection.
|
||
* TODO: migrate all test users to mock IpClient directly and remove this ctor.
|
||
*/
|
||
public IpClient(Context context, String ifName, Callback callback,
|
||
INetworkManagementService nwService) {
|
||
this(context, ifName, callback, nwService, NetdService.getInstance());
|
||
}
|
||
|
||
@VisibleForTesting
|
||
IpClient(Context context, String ifName, Callback callback,
|
||
INetworkManagementService nwService, INetd netd) {
|
||
super(IpClient.class.getSimpleName() + "." + ifName);
|
||
mTag = getName();
|
||
|
||
mContext = context;
|
||
mInterfaceName = ifName;
|
||
mClatInterfaceName = CLAT_PREFIX + ifName;
|
||
mCallback = new LoggingCallbackWrapper(callback);
|
||
mNwService = nwService;
|
||
|
||
mLog = new SharedLog(MAX_LOG_RECORDS, mTag);
|
||
mConnectivityPacketLog = new LocalLog(MAX_PACKET_RECORDS);
|
||
mMsgStateLogger = new MessageHandlingLogger();
|
||
|
||
mInterfaceCtrl = new InterfaceController(mInterfaceName, mNwService, netd, mLog);
|
||
|
||
mNetlinkTracker = new NetlinkTracker(
|
||
mInterfaceName,
|
||
new NetlinkTracker.Callback() {
|
||
@Override
|
||
public void update() {
|
||
sendMessage(EVENT_NETLINK_LINKPROPERTIES_CHANGED);
|
||
}
|
||
}) {
|
||
@Override
|
||
public void interfaceAdded(String iface) {
|
||
super.interfaceAdded(iface);
|
||
if (mClatInterfaceName.equals(iface)) {
|
||
mCallback.setNeighborDiscoveryOffload(false);
|
||
} else if (!mInterfaceName.equals(iface)) {
|
||
return;
|
||
}
|
||
|
||
final String msg = "interfaceAdded(" + iface +")";
|
||
logMsg(msg);
|
||
}
|
||
|
||
@Override
|
||
public void interfaceRemoved(String iface) {
|
||
super.interfaceRemoved(iface);
|
||
// TODO: Also observe mInterfaceName going down and take some
|
||
// kind of appropriate action.
|
||
if (mClatInterfaceName.equals(iface)) {
|
||
// TODO: consider sending a message to the IpClient main
|
||
// StateMachine thread, in case "NDO enabled" state becomes
|
||
// tied to more things that 464xlat operation.
|
||
mCallback.setNeighborDiscoveryOffload(true);
|
||
} else if (!mInterfaceName.equals(iface)) {
|
||
return;
|
||
}
|
||
|
||
final String msg = "interfaceRemoved(" + iface +")";
|
||
logMsg(msg);
|
||
}
|
||
|
||
private void logMsg(String msg) {
|
||
Log.d(mTag, msg);
|
||
getHandler().post(() -> { mLog.log("OBSERVED " + msg); });
|
||
}
|
||
};
|
||
|
||
mLinkProperties = new LinkProperties();
|
||
mLinkProperties.setInterfaceName(mInterfaceName);
|
||
|
||
mMultinetworkPolicyTracker = new MultinetworkPolicyTracker(mContext, getHandler(),
|
||
() -> { mLog.log("OBSERVED AvoidBadWifi changed"); });
|
||
|
||
mProvisioningTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
|
||
mTag + ".EVENT_PROVISIONING_TIMEOUT", EVENT_PROVISIONING_TIMEOUT);
|
||
mDhcpActionTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
|
||
mTag + ".EVENT_DHCPACTION_TIMEOUT", EVENT_DHCPACTION_TIMEOUT);
|
||
|
||
// Anything the StateMachine may access must have been instantiated
|
||
// before this point.
|
||
configureAndStartStateMachine();
|
||
|
||
// Anything that may send messages to the StateMachine must only be
|
||
// configured to do so after the StateMachine has started (above).
|
||
startStateMachineUpdaters();
|
||
}
|
||
|
||
private void configureAndStartStateMachine() {
|
||
addState(mStoppedState);
|
||
addState(mStartedState);
|
||
addState(mRunningState, mStartedState);
|
||
addState(mStoppingState);
|
||
|
||
setInitialState(mStoppedState);
|
||
|
||
super.start();
|
||
}
|
||
|
||
private void startStateMachineUpdaters() {
|
||
try {
|
||
mNwService.registerObserver(mNetlinkTracker);
|
||
} catch (RemoteException e) {
|
||
logError("Couldn't register NetlinkTracker: %s", e);
|
||
}
|
||
|
||
mMultinetworkPolicyTracker.start();
|
||
}
|
||
|
||
private void stopStateMachineUpdaters() {
|
||
try {
|
||
mNwService.unregisterObserver(mNetlinkTracker);
|
||
} catch (RemoteException e) {
|
||
logError("Couldn't unregister NetlinkTracker: %s", e);
|
||
}
|
||
|
||
mMultinetworkPolicyTracker.shutdown();
|
||
}
|
||
|
||
@Override
|
||
protected void onQuitting() {
|
||
mCallback.onQuit();
|
||
}
|
||
|
||
// Shut down this IpClient instance altogether.
|
||
public void shutdown() {
|
||
stop();
|
||
sendMessage(CMD_TERMINATE_AFTER_STOP);
|
||
}
|
||
|
||
public static ProvisioningConfiguration.Builder buildProvisioningConfiguration() {
|
||
return new ProvisioningConfiguration.Builder();
|
||
}
|
||
|
||
public void startProvisioning(ProvisioningConfiguration req) {
|
||
if (!req.isValid()) {
|
||
doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
|
||
return;
|
||
}
|
||
|
||
getNetworkInterface();
|
||
|
||
mCallback.setNeighborDiscoveryOffload(true);
|
||
sendMessage(CMD_START, new ProvisioningConfiguration(req));
|
||
}
|
||
|
||
// TODO: Delete this.
|
||
public void startProvisioning(StaticIpConfiguration staticIpConfig) {
|
||
startProvisioning(buildProvisioningConfiguration()
|
||
.withStaticConfiguration(staticIpConfig)
|
||
.build());
|
||
}
|
||
|
||
public void startProvisioning() {
|
||
startProvisioning(new ProvisioningConfiguration());
|
||
}
|
||
|
||
public void stop() {
|
||
sendMessage(CMD_STOP);
|
||
}
|
||
|
||
public void confirmConfiguration() {
|
||
sendMessage(CMD_CONFIRM);
|
||
}
|
||
|
||
public void completedPreDhcpAction() {
|
||
sendMessage(EVENT_PRE_DHCP_ACTION_COMPLETE);
|
||
}
|
||
|
||
/**
|
||
* Set the TCP buffer sizes to use.
|
||
*
|
||
* This may be called, repeatedly, at any time before or after a call to
|
||
* #startProvisioning(). The setting is cleared upon calling #stop().
|
||
*/
|
||
public void setTcpBufferSizes(String tcpBufferSizes) {
|
||
sendMessage(CMD_UPDATE_TCP_BUFFER_SIZES, tcpBufferSizes);
|
||
}
|
||
|
||
/**
|
||
* Set the HTTP Proxy configuration to use.
|
||
*
|
||
* This may be called, repeatedly, at any time before or after a call to
|
||
* #startProvisioning(). The setting is cleared upon calling #stop().
|
||
*/
|
||
public void setHttpProxy(ProxyInfo proxyInfo) {
|
||
sendMessage(CMD_UPDATE_HTTP_PROXY, proxyInfo);
|
||
}
|
||
|
||
/**
|
||
* Enable or disable the multicast filter. Attempts to use APF to accomplish the filtering,
|
||
* if not, Callback.setFallbackMulticastFilter() is called.
|
||
*/
|
||
public void setMulticastFilter(boolean enabled) {
|
||
sendMessage(CMD_SET_MULTICAST_FILTER, enabled);
|
||
}
|
||
|
||
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
|
||
if (args != null && args.length > 0 && DUMP_ARG_CONFIRM.equals(args[0])) {
|
||
// Execute confirmConfiguration() and take no further action.
|
||
confirmConfiguration();
|
||
return;
|
||
}
|
||
|
||
// Thread-unsafe access to mApfFilter but just used for debugging.
|
||
final ApfFilter apfFilter = mApfFilter;
|
||
final ProvisioningConfiguration provisioningConfig = mConfiguration;
|
||
final ApfCapabilities apfCapabilities = (provisioningConfig != null)
|
||
? provisioningConfig.mApfCapabilities : null;
|
||
|
||
IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
|
||
pw.println(mTag + " APF dump:");
|
||
pw.increaseIndent();
|
||
if (apfFilter != null) {
|
||
apfFilter.dump(pw);
|
||
} else {
|
||
pw.print("No active ApfFilter; ");
|
||
if (provisioningConfig == null) {
|
||
pw.println("IpClient not yet started.");
|
||
} else if (apfCapabilities == null || apfCapabilities.apfVersionSupported == 0) {
|
||
pw.println("Hardware does not support APF.");
|
||
} else {
|
||
pw.println("ApfFilter not yet started, APF capabilities: " + apfCapabilities);
|
||
}
|
||
}
|
||
pw.decreaseIndent();
|
||
|
||
pw.println();
|
||
pw.println(mTag + " current ProvisioningConfiguration:");
|
||
pw.increaseIndent();
|
||
pw.println(Objects.toString(provisioningConfig, "N/A"));
|
||
pw.decreaseIndent();
|
||
|
||
final IpReachabilityMonitor iprm = mIpReachabilityMonitor;
|
||
if (iprm != null) {
|
||
pw.println();
|
||
pw.println(mTag + " current IpReachabilityMonitor state:");
|
||
pw.increaseIndent();
|
||
iprm.dump(pw);
|
||
pw.decreaseIndent();
|
||
}
|
||
|
||
pw.println();
|
||
pw.println(mTag + " StateMachine dump:");
|
||
pw.increaseIndent();
|
||
mLog.dump(fd, pw, args);
|
||
pw.decreaseIndent();
|
||
|
||
pw.println();
|
||
pw.println(mTag + " connectivity packet log:");
|
||
pw.println();
|
||
pw.println("Debug with python and scapy via:");
|
||
pw.println("shell$ python");
|
||
pw.println(">>> from scapy import all as scapy");
|
||
pw.println(">>> scapy.Ether(\"<paste_hex_string>\".decode(\"hex\")).show2()");
|
||
pw.println();
|
||
|
||
pw.increaseIndent();
|
||
mConnectivityPacketLog.readOnlyLocalLog().dump(fd, pw, args);
|
||
pw.decreaseIndent();
|
||
}
|
||
|
||
|
||
/**
|
||
* Internals.
|
||
*/
|
||
|
||
@Override
|
||
protected String getWhatToString(int what) {
|
||
return sWhatToString.get(what, "UNKNOWN: " + Integer.toString(what));
|
||
}
|
||
|
||
@Override
|
||
protected String getLogRecString(Message msg) {
|
||
final String logLine = String.format(
|
||
"%s/%d %d %d %s [%s]",
|
||
mInterfaceName, mNetworkInterface == null ? -1 : mNetworkInterface.getIndex(),
|
||
msg.arg1, msg.arg2, Objects.toString(msg.obj), mMsgStateLogger);
|
||
|
||
final String richerLogLine = getWhatToString(msg.what) + " " + logLine;
|
||
mLog.log(richerLogLine);
|
||
if (DBG) {
|
||
Log.d(mTag, richerLogLine);
|
||
}
|
||
|
||
mMsgStateLogger.reset();
|
||
return logLine;
|
||
}
|
||
|
||
@Override
|
||
protected boolean recordLogRec(Message msg) {
|
||
// Don't log EVENT_NETLINK_LINKPROPERTIES_CHANGED. They can be noisy,
|
||
// and we already log any LinkProperties change that results in an
|
||
// invocation of IpClient.Callback#onLinkPropertiesChange().
|
||
final boolean shouldLog = (msg.what != EVENT_NETLINK_LINKPROPERTIES_CHANGED);
|
||
if (!shouldLog) {
|
||
mMsgStateLogger.reset();
|
||
}
|
||
return shouldLog;
|
||
}
|
||
|
||
private void logError(String fmt, Object... args) {
|
||
final String msg = "ERROR " + String.format(fmt, args);
|
||
Log.e(mTag, msg);
|
||
mLog.log(msg);
|
||
}
|
||
|
||
private void getNetworkInterface() {
|
||
try {
|
||
mNetworkInterface = NetworkInterface.getByName(mInterfaceName);
|
||
} catch (SocketException | NullPointerException e) {
|
||
// TODO: throw new IllegalStateException.
|
||
logError("Failed to get interface object: %s", e);
|
||
}
|
||
}
|
||
|
||
// This needs to be called with care to ensure that our LinkProperties
|
||
// are in sync with the actual LinkProperties of the interface. For example,
|
||
// we should only call this if we know for sure that there are no IP addresses
|
||
// assigned to the interface, etc.
|
||
private void resetLinkProperties() {
|
||
mNetlinkTracker.clearLinkProperties();
|
||
mConfiguration = null;
|
||
mDhcpResults = null;
|
||
mTcpBufferSizes = "";
|
||
mHttpProxy = null;
|
||
|
||
mLinkProperties = new LinkProperties();
|
||
mLinkProperties.setInterfaceName(mInterfaceName);
|
||
}
|
||
|
||
private void recordMetric(final int type) {
|
||
if (mStartTimeMillis <= 0) { Log.wtf(mTag, "Start time undefined!"); }
|
||
final long duration = SystemClock.elapsedRealtime() - mStartTimeMillis;
|
||
mMetricsLog.log(mInterfaceName, new IpManagerEvent(type, duration));
|
||
}
|
||
|
||
// For now: use WifiStateMachine's historical notion of provisioned.
|
||
@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.
|
||
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
|
||
// LinkProperties.compareProvisioning() or some other single function that
|
||
// takes two LinkProperties objects and returns a ProvisioningChange
|
||
// 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) {
|
||
ProvisioningChange delta;
|
||
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;
|
||
} else if (wasProvisioned && isProvisioned) {
|
||
delta = ProvisioningChange.STILL_PROVISIONED;
|
||
} else if (!wasProvisioned && !isProvisioned) {
|
||
delta = ProvisioningChange.STILL_NOT_PROVISIONED;
|
||
} else {
|
||
// (wasProvisioned && !isProvisioned)
|
||
//
|
||
// Note that this is true even if we lose a configuration element
|
||
// (e.g., a default gateway) that would not be required to advance
|
||
// into provisioned state. This is intended: if we have a default
|
||
// router and we lose it, that's a sure sign of a problem, but if
|
||
// we connect to a network with no IPv4 DNS servers, we consider
|
||
// that to be a network without DNS servers and connect anyway.
|
||
//
|
||
// See the comment below.
|
||
delta = ProvisioningChange.LOST_PROVISIONING;
|
||
}
|
||
|
||
final boolean lostIPv6 = oldLp.isIPv6Provisioned() && !newLp.isIPv6Provisioned();
|
||
final boolean lostIPv4Address = oldLp.hasIPv4Address() && !newLp.hasIPv4Address();
|
||
final boolean lostIPv6Router = oldLp.hasIPv6DefaultRoute() && !newLp.hasIPv6DefaultRoute();
|
||
|
||
// If bad wifi avoidance is disabled, then ignore IPv6 loss of
|
||
// provisioning. Otherwise, when a hotspot that loses Internet
|
||
// access sends out a 0-lifetime RA to its clients, the clients
|
||
// will disconnect and then reconnect, avoiding the bad hotspot,
|
||
// instead of getting stuck on the bad hotspot. http://b/31827713 .
|
||
//
|
||
// This is incorrect because if the hotspot then regains Internet
|
||
// access with a different prefix, TCP connections on the
|
||
// deprecated addresses will remain stuck.
|
||
//
|
||
// Note that we can still be disconnected by IpReachabilityMonitor
|
||
// if the IPv6 default gateway (but not the IPv6 DNS servers; see
|
||
// accompanying code in IpReachabilityMonitor) is unreachable.
|
||
final boolean ignoreIPv6ProvisioningLoss = !mMultinetworkPolicyTracker.getAvoidBadWifi();
|
||
|
||
// Additionally:
|
||
//
|
||
// Partial configurations (e.g., only an IPv4 address with no DNS
|
||
// servers and no default route) are accepted as long as DHCPv4
|
||
// succeeds. On such a network, isProvisioned() will always return
|
||
// false, because the configuration is not complete, but we want to
|
||
// connect anyway. It might be a disconnected network such as a
|
||
// Chromecast or a wireless printer, for example.
|
||
//
|
||
// Because on such a network isProvisioned() will always return false,
|
||
// delta will never be LOST_PROVISIONING. So check for loss of
|
||
// provisioning here too.
|
||
if (lostIPv4Address || (lostIPv6 && !ignoreIPv6ProvisioningLoss)) {
|
||
delta = ProvisioningChange.LOST_PROVISIONING;
|
||
}
|
||
|
||
// Additionally:
|
||
//
|
||
// If the previous link properties had a global IPv6 address and an
|
||
// IPv6 default route then also consider the loss of that default route
|
||
// to be a loss of provisioning. See b/27962810.
|
||
if (oldLp.hasGlobalIPv6Address() && (lostIPv6Router && !ignoreIPv6ProvisioningLoss)) {
|
||
delta = ProvisioningChange.LOST_PROVISIONING;
|
||
}
|
||
|
||
return delta;
|
||
}
|
||
|
||
private void dispatchCallback(ProvisioningChange delta, LinkProperties newLp) {
|
||
switch (delta) {
|
||
case GAINED_PROVISIONING:
|
||
if (DBG) { Log.d(mTag, "onProvisioningSuccess()"); }
|
||
recordMetric(IpManagerEvent.PROVISIONING_OK);
|
||
mCallback.onProvisioningSuccess(newLp);
|
||
break;
|
||
|
||
case LOST_PROVISIONING:
|
||
if (DBG) { Log.d(mTag, "onProvisioningFailure()"); }
|
||
recordMetric(IpManagerEvent.PROVISIONING_FAIL);
|
||
mCallback.onProvisioningFailure(newLp);
|
||
break;
|
||
|
||
default:
|
||
if (DBG) { Log.d(mTag, "onLinkPropertiesChange()"); }
|
||
mCallback.onLinkPropertiesChange(newLp);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Updates all IpClient-related state concerned with LinkProperties.
|
||
// Returns a ProvisioningChange for possibly notifying other interested
|
||
// parties that are not fronted by IpClient.
|
||
private ProvisioningChange setLinkProperties(LinkProperties newLp) {
|
||
if (mApfFilter != null) {
|
||
mApfFilter.setLinkProperties(newLp);
|
||
}
|
||
if (mIpReachabilityMonitor != null) {
|
||
mIpReachabilityMonitor.updateLinkProperties(newLp);
|
||
}
|
||
|
||
ProvisioningChange delta = compareProvisioning(mLinkProperties, newLp);
|
||
mLinkProperties = new LinkProperties(newLp);
|
||
|
||
if (delta == ProvisioningChange.GAINED_PROVISIONING) {
|
||
// TODO: Add a proper ProvisionedState and cancel the alarm in
|
||
// its enter() method.
|
||
mProvisioningTimeoutAlarm.cancel();
|
||
}
|
||
|
||
return delta;
|
||
}
|
||
|
||
private LinkProperties assembleLinkProperties() {
|
||
// [1] Create a new LinkProperties object to populate.
|
||
LinkProperties newLp = new LinkProperties();
|
||
newLp.setInterfaceName(mInterfaceName);
|
||
|
||
// [2] Pull in data from netlink:
|
||
// - IPv4 addresses
|
||
// - IPv6 addresses
|
||
// - IPv6 routes
|
||
// - IPv6 DNS servers
|
||
//
|
||
// N.B.: this is fundamentally race-prone and should be fixed by
|
||
// changing NetlinkTracker from a hybrid edge/level model to an
|
||
// edge-only model, or by giving IpClient its own netlink socket(s)
|
||
// so as to track all required information directly.
|
||
LinkProperties netlinkLinkProperties = mNetlinkTracker.getLinkProperties();
|
||
newLp.setLinkAddresses(netlinkLinkProperties.getLinkAddresses());
|
||
for (RouteInfo route : netlinkLinkProperties.getRoutes()) {
|
||
newLp.addRoute(route);
|
||
}
|
||
addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers());
|
||
|
||
// [3] Add in data from DHCPv4, if available.
|
||
//
|
||
// mDhcpResults is never shared with any other owner so we don't have
|
||
// to worry about concurrent modification.
|
||
if (mDhcpResults != null) {
|
||
for (RouteInfo route : mDhcpResults.getRoutes(mInterfaceName)) {
|
||
newLp.addRoute(route);
|
||
}
|
||
addAllReachableDnsServers(newLp, mDhcpResults.dnsServers);
|
||
newLp.setDomains(mDhcpResults.domains);
|
||
|
||
if (mDhcpResults.mtu != 0) {
|
||
newLp.setMtu(mDhcpResults.mtu);
|
||
}
|
||
}
|
||
|
||
// [4] Add in TCP buffer sizes and HTTP Proxy config, if available.
|
||
if (!TextUtils.isEmpty(mTcpBufferSizes)) {
|
||
newLp.setTcpBufferSizes(mTcpBufferSizes);
|
||
}
|
||
if (mHttpProxy != null) {
|
||
newLp.setHttpProxy(mHttpProxy);
|
||
}
|
||
|
||
// [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 (DBG) {
|
||
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;
|
||
}
|
||
|
||
private static void addAllReachableDnsServers(
|
||
LinkProperties lp, Iterable<InetAddress> dnses) {
|
||
// TODO: Investigate deleting this reachability check. We should be
|
||
// able to pass everything down to netd and let netd do evaluation
|
||
// and RFC6724-style sorting.
|
||
for (InetAddress dns : dnses) {
|
||
if (!dns.isAnyLocalAddress() && lp.isReachable(dns)) {
|
||
lp.addDnsServer(dns);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Returns false if we have lost provisioning, true otherwise.
|
||
private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
|
||
final LinkProperties newLp = assembleLinkProperties();
|
||
if (Objects.equals(newLp, mLinkProperties)) {
|
||
return true;
|
||
}
|
||
final ProvisioningChange delta = setLinkProperties(newLp);
|
||
if (sendCallbacks) {
|
||
dispatchCallback(delta, newLp);
|
||
}
|
||
return (delta != ProvisioningChange.LOST_PROVISIONING);
|
||
}
|
||
|
||
private void handleIPv4Success(DhcpResults dhcpResults) {
|
||
mDhcpResults = new DhcpResults(dhcpResults);
|
||
final LinkProperties newLp = assembleLinkProperties();
|
||
final ProvisioningChange delta = setLinkProperties(newLp);
|
||
|
||
if (DBG) {
|
||
Log.d(mTag, "onNewDhcpResults(" + Objects.toString(dhcpResults) + ")");
|
||
}
|
||
mCallback.onNewDhcpResults(dhcpResults);
|
||
dispatchCallback(delta, newLp);
|
||
}
|
||
|
||
private void handleIPv4Failure() {
|
||
// TODO: Investigate deleting this clearIPv4Address() call.
|
||
//
|
||
// DhcpClient will send us CMD_CLEAR_LINKADDRESS in all circumstances
|
||
// that could trigger a call to this function. If we missed handling
|
||
// that message in StartedState for some reason we would still clear
|
||
// any addresses upon entry to StoppedState.
|
||
mInterfaceCtrl.clearIPv4Address();
|
||
mDhcpResults = null;
|
||
if (DBG) { Log.d(mTag, "onNewDhcpResults(null)"); }
|
||
mCallback.onNewDhcpResults(null);
|
||
|
||
handleProvisioningFailure();
|
||
}
|
||
|
||
private void handleProvisioningFailure() {
|
||
final LinkProperties newLp = assembleLinkProperties();
|
||
ProvisioningChange delta = setLinkProperties(newLp);
|
||
// If we've gotten here and we're still not provisioned treat that as
|
||
// a total loss of provisioning.
|
||
//
|
||
// Either (a) static IP configuration failed or (b) DHCPv4 failed AND
|
||
// there was no usable IPv6 obtained before a non-zero provisioning
|
||
// timeout expired.
|
||
//
|
||
// Regardless: GAME OVER.
|
||
if (delta == ProvisioningChange.STILL_NOT_PROVISIONED) {
|
||
delta = ProvisioningChange.LOST_PROVISIONING;
|
||
}
|
||
|
||
dispatchCallback(delta, newLp);
|
||
if (delta == ProvisioningChange.LOST_PROVISIONING) {
|
||
transitionTo(mStoppingState);
|
||
}
|
||
}
|
||
|
||
private void doImmediateProvisioningFailure(int failureType) {
|
||
logError("onProvisioningFailure(): %s", failureType);
|
||
recordMetric(failureType);
|
||
mCallback.onProvisioningFailure(new LinkProperties(mLinkProperties));
|
||
}
|
||
|
||
private boolean startIPv4() {
|
||
// If we have a StaticIpConfiguration attempt to apply it and
|
||
// handle the result accordingly.
|
||
if (mConfiguration.mStaticIpConfig != null) {
|
||
if (mInterfaceCtrl.setIPv4Address(mConfiguration.mStaticIpConfig.ipAddress)) {
|
||
handleIPv4Success(new DhcpResults(mConfiguration.mStaticIpConfig));
|
||
} else {
|
||
return false;
|
||
}
|
||
} else {
|
||
// Start DHCPv4.
|
||
mDhcpClient = DhcpClient.makeDhcpClient(mContext, IpClient.this, mInterfaceName);
|
||
mDhcpClient.registerForPreDhcpNotification();
|
||
mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private boolean startIPv6() {
|
||
return mInterfaceCtrl.setIPv6PrivacyExtensions(true) &&
|
||
mInterfaceCtrl.setIPv6AddrGenModeIfSupported(mConfiguration.mIPv6AddrGenMode) &&
|
||
mInterfaceCtrl.enableIPv6();
|
||
}
|
||
|
||
private boolean applyInitialConfig(InitialConfiguration config) {
|
||
// TODO: also support specifying a static IPv4 configuration in InitialConfiguration.
|
||
for (LinkAddress addr : findAll(config.ipAddresses, LinkAddress::isIPv6)) {
|
||
if (!mInterfaceCtrl.addAddress(addr)) return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private boolean startIpReachabilityMonitor() {
|
||
try {
|
||
mIpReachabilityMonitor = new IpReachabilityMonitor(
|
||
mContext,
|
||
mInterfaceName,
|
||
getHandler(),
|
||
mLog,
|
||
new IpReachabilityMonitor.Callback() {
|
||
@Override
|
||
public void notifyLost(InetAddress ip, String logMsg) {
|
||
mCallback.onReachabilityLost(logMsg);
|
||
}
|
||
},
|
||
mMultinetworkPolicyTracker);
|
||
} catch (IllegalArgumentException iae) {
|
||
// Failed to start IpReachabilityMonitor. Log it and call
|
||
// onProvisioningFailure() immediately.
|
||
//
|
||
// See http://b/31038971.
|
||
logError("IpReachabilityMonitor failure: %s", iae);
|
||
mIpReachabilityMonitor = null;
|
||
}
|
||
|
||
return (mIpReachabilityMonitor != null);
|
||
}
|
||
|
||
private void stopAllIP() {
|
||
// We don't need to worry about routes, just addresses, because:
|
||
// - disableIpv6() will clear autoconf IPv6 routes as well, and
|
||
// - we don't get IPv4 routes from netlink
|
||
// so we neither react to nor need to wait for changes in either.
|
||
|
||
mInterfaceCtrl.disableIPv6();
|
||
mInterfaceCtrl.clearAllAddresses();
|
||
}
|
||
|
||
class StoppedState extends State {
|
||
@Override
|
||
public void enter() {
|
||
stopAllIP();
|
||
|
||
resetLinkProperties();
|
||
if (mStartTimeMillis > 0) {
|
||
// Completed a life-cycle; send a final empty LinkProperties
|
||
// (cleared in resetLinkProperties() above) and record an event.
|
||
mCallback.onLinkPropertiesChange(new LinkProperties(mLinkProperties));
|
||
recordMetric(IpManagerEvent.COMPLETE_LIFECYCLE);
|
||
mStartTimeMillis = 0;
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public boolean processMessage(Message msg) {
|
||
switch (msg.what) {
|
||
case CMD_TERMINATE_AFTER_STOP:
|
||
stopStateMachineUpdaters();
|
||
quit();
|
||
break;
|
||
|
||
case CMD_STOP:
|
||
break;
|
||
|
||
case CMD_START:
|
||
mConfiguration = (ProvisioningConfiguration) msg.obj;
|
||
transitionTo(mStartedState);
|
||
break;
|
||
|
||
case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
|
||
handleLinkPropertiesUpdate(NO_CALLBACKS);
|
||
break;
|
||
|
||
case CMD_UPDATE_TCP_BUFFER_SIZES:
|
||
mTcpBufferSizes = (String) msg.obj;
|
||
handleLinkPropertiesUpdate(NO_CALLBACKS);
|
||
break;
|
||
|
||
case CMD_UPDATE_HTTP_PROXY:
|
||
mHttpProxy = (ProxyInfo) msg.obj;
|
||
handleLinkPropertiesUpdate(NO_CALLBACKS);
|
||
break;
|
||
|
||
case CMD_SET_MULTICAST_FILTER:
|
||
mMulticastFiltering = (boolean) msg.obj;
|
||
break;
|
||
|
||
case DhcpClient.CMD_ON_QUIT:
|
||
// Everything is already stopped.
|
||
logError("Unexpected CMD_ON_QUIT (already stopped).");
|
||
break;
|
||
|
||
default:
|
||
return NOT_HANDLED;
|
||
}
|
||
|
||
mMsgStateLogger.handled(this, getCurrentState());
|
||
return HANDLED;
|
||
}
|
||
}
|
||
|
||
class StoppingState extends State {
|
||
@Override
|
||
public void enter() {
|
||
if (mDhcpClient == null) {
|
||
// There's no DHCPv4 for which to wait; proceed to stopped.
|
||
deferMessage(obtainMessage(CMD_JUMP_STOPPING_TO_STOPPED));
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public boolean processMessage(Message msg) {
|
||
switch (msg.what) {
|
||
case CMD_JUMP_STOPPING_TO_STOPPED:
|
||
transitionTo(mStoppedState);
|
||
break;
|
||
|
||
case CMD_STOP:
|
||
break;
|
||
|
||
case DhcpClient.CMD_CLEAR_LINKADDRESS:
|
||
mInterfaceCtrl.clearIPv4Address();
|
||
break;
|
||
|
||
case DhcpClient.CMD_ON_QUIT:
|
||
mDhcpClient = null;
|
||
transitionTo(mStoppedState);
|
||
break;
|
||
|
||
default:
|
||
deferMessage(msg);
|
||
}
|
||
|
||
mMsgStateLogger.handled(this, getCurrentState());
|
||
return HANDLED;
|
||
}
|
||
}
|
||
|
||
class StartedState extends State {
|
||
@Override
|
||
public void enter() {
|
||
mStartTimeMillis = SystemClock.elapsedRealtime();
|
||
|
||
if (mConfiguration.mProvisioningTimeoutMs > 0) {
|
||
final long alarmTime = SystemClock.elapsedRealtime() +
|
||
mConfiguration.mProvisioningTimeoutMs;
|
||
mProvisioningTimeoutAlarm.schedule(alarmTime);
|
||
}
|
||
|
||
if (readyToProceed()) {
|
||
deferMessage(obtainMessage(CMD_JUMP_STARTING_TO_RUNNING));
|
||
} else {
|
||
// Clear all IPv4 and IPv6 before proceeding to RunningState.
|
||
// Clean up any leftover state from an abnormal exit from
|
||
// tethering or during an IpClient restart.
|
||
stopAllIP();
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void exit() {
|
||
mProvisioningTimeoutAlarm.cancel();
|
||
}
|
||
|
||
@Override
|
||
public boolean processMessage(Message msg) {
|
||
switch (msg.what) {
|
||
case CMD_JUMP_STARTING_TO_RUNNING:
|
||
transitionTo(mRunningState);
|
||
break;
|
||
|
||
case CMD_STOP:
|
||
transitionTo(mStoppingState);
|
||
break;
|
||
|
||
case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
|
||
handleLinkPropertiesUpdate(NO_CALLBACKS);
|
||
if (readyToProceed()) {
|
||
transitionTo(mRunningState);
|
||
}
|
||
break;
|
||
|
||
case EVENT_PROVISIONING_TIMEOUT:
|
||
handleProvisioningFailure();
|
||
break;
|
||
|
||
default:
|
||
// It's safe to process messages out of order because the
|
||
// only message that can both
|
||
// a) be received at this time and
|
||
// b) affect provisioning state
|
||
// is EVENT_NETLINK_LINKPROPERTIES_CHANGED (handled above).
|
||
deferMessage(msg);
|
||
}
|
||
|
||
mMsgStateLogger.handled(this, getCurrentState());
|
||
return HANDLED;
|
||
}
|
||
|
||
private boolean readyToProceed() {
|
||
return (!mLinkProperties.hasIPv4Address() &&
|
||
!mLinkProperties.hasGlobalIPv6Address());
|
||
}
|
||
}
|
||
|
||
class RunningState extends State {
|
||
private ConnectivityPacketTracker mPacketTracker;
|
||
private boolean mDhcpActionInFlight;
|
||
|
||
@Override
|
||
public void enter() {
|
||
ApfFilter.ApfConfiguration apfConfig = new ApfFilter.ApfConfiguration();
|
||
apfConfig.apfCapabilities = mConfiguration.mApfCapabilities;
|
||
apfConfig.multicastFilter = mMulticastFiltering;
|
||
// Get the Configuration for ApfFilter from Context
|
||
apfConfig.ieee802_3Filter =
|
||
mContext.getResources().getBoolean(R.bool.config_apfDrop802_3Frames);
|
||
apfConfig.ethTypeBlackList =
|
||
mContext.getResources().getIntArray(R.array.config_apfEthTypeBlackList);
|
||
mApfFilter = ApfFilter.maybeCreate(apfConfig, mNetworkInterface, mCallback);
|
||
// TODO: investigate the effects of any multicast filtering racing/interfering with the
|
||
// rest of this IP configuration startup.
|
||
if (mApfFilter == null) {
|
||
mCallback.setFallbackMulticastFilter(mMulticastFiltering);
|
||
}
|
||
|
||
mPacketTracker = createPacketTracker();
|
||
if (mPacketTracker != null) mPacketTracker.start(mConfiguration.mDisplayName);
|
||
|
||
if (mConfiguration.mEnableIPv6 && !startIPv6()) {
|
||
doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV6);
|
||
enqueueJumpToStoppingState();
|
||
return;
|
||
}
|
||
|
||
if (mConfiguration.mEnableIPv4 && !startIPv4()) {
|
||
doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV4);
|
||
enqueueJumpToStoppingState();
|
||
return;
|
||
}
|
||
|
||
final 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);
|
||
enqueueJumpToStoppingState();
|
||
return;
|
||
}
|
||
|
||
if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) {
|
||
doImmediateProvisioningFailure(
|
||
IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR);
|
||
enqueueJumpToStoppingState();
|
||
return;
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void exit() {
|
||
stopDhcpAction();
|
||
|
||
if (mIpReachabilityMonitor != null) {
|
||
mIpReachabilityMonitor.stop();
|
||
mIpReachabilityMonitor = null;
|
||
}
|
||
|
||
if (mDhcpClient != null) {
|
||
mDhcpClient.sendMessage(DhcpClient.CMD_STOP_DHCP);
|
||
mDhcpClient.doQuit();
|
||
}
|
||
|
||
if (mPacketTracker != null) {
|
||
mPacketTracker.stop();
|
||
mPacketTracker = null;
|
||
}
|
||
|
||
if (mApfFilter != null) {
|
||
mApfFilter.shutdown();
|
||
mApfFilter = null;
|
||
}
|
||
|
||
resetLinkProperties();
|
||
}
|
||
|
||
private void enqueueJumpToStoppingState() {
|
||
deferMessage(obtainMessage(CMD_JUMP_RUNNING_TO_STOPPING));
|
||
}
|
||
|
||
private ConnectivityPacketTracker createPacketTracker() {
|
||
try {
|
||
return new ConnectivityPacketTracker(
|
||
getHandler(), mNetworkInterface, mConnectivityPacketLog);
|
||
} catch (IllegalArgumentException e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private void ensureDhcpAction() {
|
||
if (!mDhcpActionInFlight) {
|
||
mCallback.onPreDhcpAction();
|
||
mDhcpActionInFlight = true;
|
||
final long alarmTime = SystemClock.elapsedRealtime() +
|
||
mConfiguration.mRequestedPreDhcpActionMs;
|
||
mDhcpActionTimeoutAlarm.schedule(alarmTime);
|
||
}
|
||
}
|
||
|
||
private void stopDhcpAction() {
|
||
mDhcpActionTimeoutAlarm.cancel();
|
||
if (mDhcpActionInFlight) {
|
||
mCallback.onPostDhcpAction();
|
||
mDhcpActionInFlight = false;
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public boolean processMessage(Message msg) {
|
||
switch (msg.what) {
|
||
case CMD_JUMP_RUNNING_TO_STOPPING:
|
||
case CMD_STOP:
|
||
transitionTo(mStoppingState);
|
||
break;
|
||
|
||
case CMD_START:
|
||
logError("ALERT: START received in StartedState. Please fix caller.");
|
||
break;
|
||
|
||
case CMD_CONFIRM:
|
||
// TODO: Possibly introduce a second type of confirmation
|
||
// that both probes (a) on-link neighbors and (b) does
|
||
// a DHCPv4 RENEW. We used to do this on Wi-Fi framework
|
||
// roams.
|
||
if (mIpReachabilityMonitor != null) {
|
||
mIpReachabilityMonitor.probeAll();
|
||
}
|
||
break;
|
||
|
||
case EVENT_PRE_DHCP_ACTION_COMPLETE:
|
||
// It's possible to reach here if, for example, someone
|
||
// calls completedPreDhcpAction() after provisioning with
|
||
// a static IP configuration.
|
||
if (mDhcpClient != null) {
|
||
mDhcpClient.sendMessage(DhcpClient.CMD_PRE_DHCP_ACTION_COMPLETE);
|
||
}
|
||
break;
|
||
|
||
case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
|
||
if (!handleLinkPropertiesUpdate(SEND_CALLBACKS)) {
|
||
transitionTo(mStoppingState);
|
||
}
|
||
break;
|
||
|
||
case CMD_UPDATE_TCP_BUFFER_SIZES:
|
||
mTcpBufferSizes = (String) msg.obj;
|
||
// This cannot possibly change provisioning state.
|
||
handleLinkPropertiesUpdate(SEND_CALLBACKS);
|
||
break;
|
||
|
||
case CMD_UPDATE_HTTP_PROXY:
|
||
mHttpProxy = (ProxyInfo) msg.obj;
|
||
// This cannot possibly change provisioning state.
|
||
handleLinkPropertiesUpdate(SEND_CALLBACKS);
|
||
break;
|
||
|
||
case CMD_SET_MULTICAST_FILTER: {
|
||
mMulticastFiltering = (boolean) msg.obj;
|
||
if (mApfFilter != null) {
|
||
mApfFilter.setMulticastFilter(mMulticastFiltering);
|
||
} else {
|
||
mCallback.setFallbackMulticastFilter(mMulticastFiltering);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case EVENT_DHCPACTION_TIMEOUT:
|
||
stopDhcpAction();
|
||
break;
|
||
|
||
case DhcpClient.CMD_PRE_DHCP_ACTION:
|
||
if (mConfiguration.mRequestedPreDhcpActionMs > 0) {
|
||
ensureDhcpAction();
|
||
} else {
|
||
sendMessage(EVENT_PRE_DHCP_ACTION_COMPLETE);
|
||
}
|
||
break;
|
||
|
||
case DhcpClient.CMD_CLEAR_LINKADDRESS:
|
||
mInterfaceCtrl.clearIPv4Address();
|
||
break;
|
||
|
||
case DhcpClient.CMD_CONFIGURE_LINKADDRESS: {
|
||
final LinkAddress ipAddress = (LinkAddress) msg.obj;
|
||
if (mInterfaceCtrl.setIPv4Address(ipAddress)) {
|
||
mDhcpClient.sendMessage(DhcpClient.EVENT_LINKADDRESS_CONFIGURED);
|
||
} else {
|
||
logError("Failed to set IPv4 address.");
|
||
dispatchCallback(ProvisioningChange.LOST_PROVISIONING,
|
||
new LinkProperties(mLinkProperties));
|
||
transitionTo(mStoppingState);
|
||
}
|
||
break;
|
||
}
|
||
|
||
// This message is only received when:
|
||
//
|
||
// a) initial address acquisition succeeds,
|
||
// b) renew succeeds or is NAK'd,
|
||
// c) rebind succeeds or is NAK'd, or
|
||
// c) the lease expires,
|
||
//
|
||
// but never when initial address acquisition fails. The latter
|
||
// condition is now governed by the provisioning timeout.
|
||
case DhcpClient.CMD_POST_DHCP_ACTION:
|
||
stopDhcpAction();
|
||
|
||
switch (msg.arg1) {
|
||
case DhcpClient.DHCP_SUCCESS:
|
||
handleIPv4Success((DhcpResults) msg.obj);
|
||
break;
|
||
case DhcpClient.DHCP_FAILURE:
|
||
handleIPv4Failure();
|
||
break;
|
||
default:
|
||
logError("Unknown CMD_POST_DHCP_ACTION status: %s", msg.arg1);
|
||
}
|
||
break;
|
||
|
||
case DhcpClient.CMD_ON_QUIT:
|
||
// DHCPv4 quit early for some reason.
|
||
logError("Unexpected CMD_ON_QUIT.");
|
||
mDhcpClient = null;
|
||
break;
|
||
|
||
default:
|
||
return NOT_HANDLED;
|
||
}
|
||
|
||
mMsgStateLogger.handled(this, getCurrentState());
|
||
return HANDLED;
|
||
}
|
||
}
|
||
|
||
private static class MessageHandlingLogger {
|
||
public String processedInState;
|
||
public String receivedInState;
|
||
|
||
public void reset() {
|
||
processedInState = null;
|
||
receivedInState = null;
|
||
}
|
||
|
||
public void handled(State processedIn, IState receivedIn) {
|
||
processedInState = processedIn.getClass().getSimpleName();
|
||
receivedInState = receivedIn.getName();
|
||
}
|
||
|
||
public String toString() {
|
||
return String.format("rcvd_in=%s, proc_in=%s",
|
||
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());
|
||
}
|
||
}
|