summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/android/net/dhcp/DhcpLease.java37
-rw-r--r--src/android/net/dhcp/DhcpLeaseRepository.java66
-rw-r--r--src/android/net/dhcp/DhcpServer.java21
-rw-r--r--src/android/net/ip/IpNeighborMonitor.java241
-rw-r--r--src/android/net/util/FdEventsReader.java259
-rw-r--r--src/android/net/util/PacketReader.java59
-rw-r--r--src/com/android/networkstack/netlink/TcpInfo.java132
-rw-r--r--src/com/android/networkstack/netlink/TcpSocketTracker.java16
-rw-r--r--src/com/android/server/connectivity/NetworkMonitor.java311
9 files changed, 424 insertions, 718 deletions
diff --git a/src/android/net/dhcp/DhcpLease.java b/src/android/net/dhcp/DhcpLease.java
index 37d9cc0..3226f28 100644
--- a/src/android/net/dhcp/DhcpLease.java
+++ b/src/android/net/dhcp/DhcpLease.java
@@ -16,6 +16,8 @@
package android.net.dhcp;
+import static android.net.shared.Inet4AddressUtils.inet4AddressToIntHTH;
+
import android.net.MacAddress;
import android.os.SystemClock;
import android.text.TextUtils;
@@ -43,6 +45,7 @@ public class DhcpLease {
private final MacAddress mHwAddr;
@NonNull
private final Inet4Address mNetAddr;
+ private final int mPrefixLength;
/**
* Expiration time for the lease, to compare with {@link SystemClock#elapsedRealtime()}.
*/
@@ -51,10 +54,12 @@ public class DhcpLease {
private final String mHostname;
public DhcpLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr,
- @NonNull Inet4Address netAddr, long expTime, @Nullable String hostname) {
+ @NonNull Inet4Address netAddr, int prefixLength, long expTime,
+ @Nullable String hostname) {
mClientId = (clientId == null ? null : Arrays.copyOf(clientId, clientId.length));
mHwAddr = hwAddr;
mNetAddr = netAddr;
+ mPrefixLength = prefixLength;
mExpTime = expTime;
mHostname = hostname;
}
@@ -87,6 +92,10 @@ public class DhcpLease {
return mNetAddr;
}
+ public int getPrefixLength() {
+ return mPrefixLength;
+ }
+
public long getExpTime() {
return mExpTime;
}
@@ -99,7 +108,8 @@ public class DhcpLease {
* @return A {@link DhcpLease} with expiration time set to max(expTime, currentExpTime)
*/
public DhcpLease renewedLease(long expTime, @Nullable String hostname) {
- return new DhcpLease(mClientId, mHwAddr, mNetAddr, Math.max(expTime, mExpTime),
+ return new DhcpLease(mClientId, mHwAddr, mNetAddr, mPrefixLength,
+ Math.max(expTime, mExpTime),
(hostname == null ? mHostname : hostname));
}
@@ -125,13 +135,14 @@ public class DhcpLease {
return Arrays.equals(mClientId, other.mClientId)
&& mHwAddr.equals(other.mHwAddr)
&& mNetAddr.equals(other.mNetAddr)
+ && mPrefixLength == other.mPrefixLength
&& mExpTime == other.mExpTime
&& TextUtils.equals(mHostname, other.mHostname);
}
@Override
public int hashCode() {
- return Objects.hash(mClientId, mHwAddr, mNetAddr, mHostname, mExpTime);
+ return Objects.hash(mClientId, mHwAddr, mNetAddr, mPrefixLength, mHostname, mExpTime);
}
static String clientIdToString(byte[] bytes) {
@@ -147,8 +158,24 @@ public class DhcpLease {
@Override
public String toString() {
- return String.format("clientId: %s, hwAddr: %s, netAddr: %s, expTime: %d, hostname: %s",
+ return String.format("clientId: %s, hwAddr: %s, netAddr: %s/%d, expTime: %d,"
+ + "hostname: %s",
clientIdToString(mClientId), mHwAddr.toString(), inet4AddrToString(mNetAddr),
- mExpTime, mHostname);
+ mPrefixLength, mExpTime, mHostname);
+ }
+
+ /**
+ * Create a {@link DhcpLeaseParcelable} containing the information held in this lease.
+ */
+ public DhcpLeaseParcelable toParcelable() {
+ final DhcpLeaseParcelable p = new DhcpLeaseParcelable();
+ p.clientId = mClientId == null ? null : Arrays.copyOf(mClientId, mClientId.length);
+ p.hwAddr = mHwAddr.toByteArray();
+ p.netAddr = inet4AddressToIntHTH(mNetAddr);
+ p.prefixLength = mPrefixLength;
+ p.expTime = mExpTime;
+ p.hostname = mHostname;
+
+ return p;
}
}
diff --git a/src/android/net/dhcp/DhcpLeaseRepository.java b/src/android/net/dhcp/DhcpLeaseRepository.java
index 4e74dc8..1dc2f7f 100644
--- a/src/android/net/dhcp/DhcpLeaseRepository.java
+++ b/src/android/net/dhcp/DhcpLeaseRepository.java
@@ -31,6 +31,8 @@ import android.net.IpPrefix;
import android.net.MacAddress;
import android.net.dhcp.DhcpServer.Clock;
import android.net.util.SharedLog;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
import android.util.ArrayMap;
import androidx.annotation.NonNull;
@@ -45,6 +47,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
@@ -73,6 +76,7 @@ class DhcpLeaseRepository {
@NonNull
private Set<Inet4Address> mReservedAddrs;
private int mSubnetAddr;
+ private int mPrefixLength;
private int mSubnetMask;
private int mNumAddresses;
private long mLeaseTimeMs;
@@ -84,6 +88,9 @@ class DhcpLeaseRepository {
*/
private long mNextExpirationCheck = EXPIRATION_NEVER;
+ @NonNull
+ private RemoteCallbackList<IDhcpLeaseCallbacks> mLeaseCallbacks = new RemoteCallbackList<>();
+
static class DhcpLeaseException extends Exception {
DhcpLeaseException(String message) {
super(message);
@@ -131,27 +138,34 @@ class DhcpLeaseRepository {
long leaseTimeMs) {
mPrefix = prefix;
mReservedAddrs = Collections.unmodifiableSet(new HashSet<>(reservedAddrs));
- mSubnetMask = prefixLengthToV4NetmaskIntHTH(prefix.getPrefixLength());
+ mPrefixLength = prefix.getPrefixLength();
+ mSubnetMask = prefixLengthToV4NetmaskIntHTH(mPrefixLength);
mSubnetAddr = inet4AddressToIntHTH((Inet4Address) prefix.getAddress()) & mSubnetMask;
mNumAddresses = 1 << (IPV4_ADDR_BITS - prefix.getPrefixLength());
mLeaseTimeMs = leaseTimeMs;
- cleanMap(mCommittedLeases);
cleanMap(mDeclinedAddrs);
+ if (cleanMap(mCommittedLeases)) {
+ notifyLeasesChanged();
+ }
}
/**
* From a map keyed by {@link Inet4Address}, remove entries where the key is invalid (as
* specified by {@link #isValidAddress(Inet4Address)}), or is a reserved address.
+ * @return true iff at least one entry was removed.
*/
- private <T> void cleanMap(Map<Inet4Address, T> map) {
+ private <T> boolean cleanMap(Map<Inet4Address, T> map) {
final Iterator<Entry<Inet4Address, T>> it = map.entrySet().iterator();
+ boolean removed = false;
while (it.hasNext()) {
final Inet4Address addr = it.next().getKey();
if (!isValidAddress(addr) || mReservedAddrs.contains(addr)) {
it.remove();
+ removed = true;
}
}
+ return removed;
}
/**
@@ -181,7 +195,7 @@ class DhcpLeaseRepository {
mLog.log("Offering extended lease " + newLease);
// Do not update lease time in the map: the offer is not committed yet.
} else if (reqAddr != null && isValidAddress(reqAddr) && isAvailable(reqAddr)) {
- newLease = new DhcpLease(clientId, hwAddr, reqAddr, expTime, hostname);
+ newLease = new DhcpLease(clientId, hwAddr, reqAddr, mPrefixLength, expTime, hostname);
mLog.log("Offering requested lease " + newLease);
} else {
newLease = makeNewOffer(clientId, hwAddr, expTime, hostname);
@@ -267,7 +281,8 @@ class DhcpLeaseRepository {
if (assignedLease != null) {
if (sidSet && reqAddr != null) {
// Client in SELECTING state; remove any current lease before creating a new one.
- mCommittedLeases.remove(assignedLease.getNetAddr());
+ // Do not notify of change as it will be done when the new lease is committed.
+ removeLease(assignedLease.getNetAddr(), false /* notifyChange */);
} else if (!assignedLease.getNetAddr().equals(leaseAddr)) {
// reqAddr null (RENEWING/REBINDING): client renewing its own lease for clientAddr.
// reqAddr set with sid not set (INIT-REBOOT): client verifying configuration.
@@ -314,7 +329,7 @@ class DhcpLeaseRepository {
final DhcpLease lease;
if (currentLease == null) {
if (isValidAddress(addr) && !mReservedAddrs.contains(addr)) {
- lease = new DhcpLease(clientId, hwAddr, addr, expTime, hostname);
+ lease = new DhcpLease(clientId, hwAddr, addr, mPrefixLength, expTime, hostname);
} else {
throw new InvalidAddressException("Lease not found and address unavailable");
}
@@ -328,6 +343,13 @@ class DhcpLeaseRepository {
private void commitLease(@NonNull DhcpLease lease) {
mCommittedLeases.put(lease.getNetAddr(), lease);
maybeUpdateEarliestExpiration(lease.getExpTime());
+ notifyLeasesChanged();
+ }
+
+ private void removeLease(@NonNull Inet4Address address, boolean notifyChange) {
+ // Earliest expiration remains <= the first expiry time on remove, so no need to update it.
+ mCommittedLeases.remove(address);
+ if (notifyChange) notifyLeasesChanged();
}
/**
@@ -343,8 +365,8 @@ class DhcpLeaseRepository {
return false;
}
if (currentLease.matchesClient(clientId, hwAddr)) {
- mCommittedLeases.remove(addr);
mLog.log("Released lease " + currentLease);
+ removeLease(addr, true /* notifyChange */);
return true;
}
mLog.w(String.format("Not releasing lease %s: does not match client (cid %s, hwAddr %s)",
@@ -352,6 +374,24 @@ class DhcpLeaseRepository {
return false;
}
+ private void notifyLeasesChanged() {
+ final List<DhcpLeaseParcelable> leaseParcelables =
+ new ArrayList<>(mCommittedLeases.size());
+ for (DhcpLease committedLease : mCommittedLeases.values()) {
+ leaseParcelables.add(committedLease.toParcelable());
+ }
+
+ final int cbCount = mLeaseCallbacks.beginBroadcast();
+ for (int i = 0; i < cbCount; i++) {
+ try {
+ mLeaseCallbacks.getBroadcastItem(i).onLeasesChanged(leaseParcelables);
+ } catch (RemoteException e) {
+ mLog.e("Could not send lease callback", e);
+ }
+ }
+ mLeaseCallbacks.finishBroadcast();
+ }
+
public void markLeaseDeclined(@NonNull Inet4Address addr) {
if (mDeclinedAddrs.containsKey(addr) || !isValidAddress(addr)) {
mLog.logf("Not marking %s as declined: already declined or not assignable",
@@ -383,6 +423,14 @@ class DhcpLeaseRepository {
}
/**
+ * Add callbacks that will be called on leases update.
+ */
+ public void addLeaseCallbacks(@NonNull IDhcpLeaseCallbacks cb) {
+ Objects.requireNonNull(cb, "Callbacks must be non-null");
+ mLeaseCallbacks.register(cb);
+ }
+
+ /**
* Given the expiration time of a new committed lease or declined address, update
* {@link #mNextExpirationCheck} so it stays lower than or equal to the time for the first lease
* to expire.
@@ -541,7 +589,7 @@ class DhcpLeaseRepository {
for (int i = 0; i < mNumAddresses; i++) {
final Inet4Address addr = intToInet4AddressHTH(intAddr);
if (isAvailable(addr) && !mDeclinedAddrs.containsKey(addr)) {
- return new DhcpLease(clientId, hwAddr, addr, expTime, hostname);
+ return new DhcpLease(clientId, hwAddr, addr, mPrefixLength, expTime, hostname);
}
intAddr = getNextAddress(intAddr);
}
@@ -557,7 +605,7 @@ class DhcpLeaseRepository {
// However declined addresses may have been requested (typically by the machine that was
// already using the address) after being declined.
if (isAvailable(addr)) {
- return new DhcpLease(clientId, hwAddr, addr, expTime, hostname);
+ return new DhcpLease(clientId, hwAddr, addr, mPrefixLength, expTime, hostname);
}
}
diff --git a/src/android/net/dhcp/DhcpServer.java b/src/android/net/dhcp/DhcpServer.java
index 6aadc04..9e54a69 100644
--- a/src/android/net/dhcp/DhcpServer.java
+++ b/src/android/net/dhcp/DhcpServer.java
@@ -274,10 +274,22 @@ public class DhcpServer extends IDhcpServer.Stub {
*/
@Override
public void start(@Nullable INetworkStackStatusCallback cb) {
+ startWithCallbacks(cb, null);
+ }
+
+ /**
+ * Start listening for and responding to packets, with optional callbacks for lease events.
+ *
+ * <p>It is not legal to call this method more than once; in particular the server cannot be
+ * restarted after being stopped.
+ */
+ @Override
+ public void startWithCallbacks(@Nullable INetworkStackStatusCallback statusCb,
+ @Nullable IDhcpLeaseCallbacks leaseCb) {
mDeps.checkCaller();
mHandlerThread.start();
mHandler = new ServerHandler(mHandlerThread.getLooper());
- sendMessage(CMD_START_DHCP_SERVER, cb);
+ sendMessage(CMD_START_DHCP_SERVER, new Pair<>(statusCb, leaseCb));
}
/**
@@ -344,9 +356,14 @@ public class DhcpServer extends IDhcpServer.Stub {
cb = pair.second;
break;
case CMD_START_DHCP_SERVER:
+ final Pair<INetworkStackStatusCallback, IDhcpLeaseCallbacks> obj =
+ (Pair<INetworkStackStatusCallback, IDhcpLeaseCallbacks>) msg.obj;
+ cb = obj.first;
+ if (obj.second != null) {
+ mLeaseRepo.addLeaseCallbacks(obj.second);
+ }
mPacketListener = mDeps.makePacketListener();
mPacketListener.start();
- cb = (INetworkStackStatusCallback) msg.obj;
break;
case CMD_STOP_DHCP_SERVER:
if (mPacketListener != null) {
diff --git a/src/android/net/ip/IpNeighborMonitor.java b/src/android/net/ip/IpNeighborMonitor.java
deleted file mode 100644
index 803f2e6..0000000
--- a/src/android/net/ip/IpNeighborMonitor.java
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * 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 static android.net.netlink.NetlinkConstants.RTM_DELNEIGH;
-import static android.net.netlink.NetlinkConstants.hexify;
-import static android.net.netlink.NetlinkConstants.stringForNlMsgType;
-import static android.net.util.SocketUtils.makeNetlinkSocketAddress;
-import static android.system.OsConstants.AF_NETLINK;
-import static android.system.OsConstants.NETLINK_ROUTE;
-import static android.system.OsConstants.SOCK_DGRAM;
-import static android.system.OsConstants.SOCK_NONBLOCK;
-
-import android.net.MacAddress;
-import android.net.netlink.NetlinkErrorMessage;
-import android.net.netlink.NetlinkMessage;
-import android.net.netlink.NetlinkSocket;
-import android.net.netlink.RtNetlinkNeighborMessage;
-import android.net.netlink.StructNdMsg;
-import android.net.util.NetworkStackUtils;
-import android.net.util.PacketReader;
-import android.net.util.SharedLog;
-import android.os.Handler;
-import android.os.SystemClock;
-import android.system.ErrnoException;
-import android.system.Os;
-import android.system.OsConstants;
-import android.util.Log;
-
-import java.io.FileDescriptor;
-import java.net.InetAddress;
-import java.net.SocketAddress;
-import java.net.SocketException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.StringJoiner;
-
-
-/**
- * IpNeighborMonitor.
- *
- * Monitors the kernel rtnetlink neighbor notifications and presents to callers
- * NeighborEvents describing each event. Callers can provide a consumer instance
- * to both filter (e.g. by interface index and IP address) and handle the
- * generated NeighborEvents.
- *
- * @hide
- */
-public class IpNeighborMonitor extends PacketReader {
- private static final String TAG = IpNeighborMonitor.class.getSimpleName();
- private static final boolean DBG = false;
- private static final boolean VDBG = false;
-
- /**
- * Make the kernel perform neighbor reachability detection (IPv4 ARP or IPv6 ND)
- * for the given IP address on the specified interface index.
- *
- * @return 0 if the request was successfully passed to the kernel; otherwise return
- * a non-zero error code.
- */
- public static int startKernelNeighborProbe(int ifIndex, InetAddress ip) {
- final String msgSnippet = "probing ip=" + ip.getHostAddress() + "%" + ifIndex;
- if (DBG) { Log.d(TAG, msgSnippet); }
-
- final byte[] msg = RtNetlinkNeighborMessage.newNewNeighborMessage(
- 1, ip, StructNdMsg.NUD_PROBE, ifIndex, null);
-
- try {
- NetlinkSocket.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
- } catch (ErrnoException e) {
- Log.e(TAG, "Error " + msgSnippet + ": " + e);
- return -e.errno;
- }
-
- return 0;
- }
-
- public static class NeighborEvent {
- final long elapsedMs;
- final short msgType;
- final int ifindex;
- final InetAddress ip;
- final short nudState;
- final MacAddress macAddr;
-
- public NeighborEvent(long elapsedMs, short msgType, int ifindex, InetAddress ip,
- short nudState, MacAddress macAddr) {
- this.elapsedMs = elapsedMs;
- this.msgType = msgType;
- this.ifindex = ifindex;
- this.ip = ip;
- this.nudState = nudState;
- this.macAddr = macAddr;
- }
-
- boolean isConnected() {
- return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateConnected(nudState);
- }
-
- boolean isValid() {
- return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateValid(nudState);
- }
-
- @Override
- public String toString() {
- final StringJoiner j = new StringJoiner(",", "NeighborEvent{", "}");
- return j.add("@" + elapsedMs)
- .add(stringForNlMsgType(msgType))
- .add("if=" + ifindex)
- .add(ip.getHostAddress())
- .add(StructNdMsg.stringForNudState(nudState))
- .add("[" + macAddr + "]")
- .toString();
- }
- }
-
- public interface NeighborEventConsumer {
- // Every neighbor event received on the netlink socket is passed in
- // here. Subclasses should filter for events of interest.
- public void accept(NeighborEvent event);
- }
-
- private final SharedLog mLog;
- private final NeighborEventConsumer mConsumer;
-
- public IpNeighborMonitor(Handler h, SharedLog log, NeighborEventConsumer cb) {
- super(h, NetlinkSocket.DEFAULT_RECV_BUFSIZE);
- mLog = log.forSubComponent(TAG);
- mConsumer = (cb != null) ? cb : (event) -> { /* discard */ };
- }
-
- @Override
- protected FileDescriptor createFd() {
- FileDescriptor fd = null;
-
- try {
- fd = Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_NONBLOCK, NETLINK_ROUTE);
- Os.bind(fd, makeNetlinkSocketAddress(0, OsConstants.RTMGRP_NEIGH));
- NetlinkSocket.connectToKernel(fd);
-
- if (VDBG) {
- final SocketAddress nlAddr = Os.getsockname(fd);
- Log.d(TAG, "bound to sockaddr_nl{" + nlAddr.toString() + "}");
- }
- } catch (ErrnoException|SocketException e) {
- logError("Failed to create rtnetlink socket", e);
- NetworkStackUtils.closeSocketQuietly(fd);
- return null;
- }
-
- return fd;
- }
-
- @Override
- protected void handlePacket(byte[] recvbuf, int length) {
- final long whenMs = SystemClock.elapsedRealtime();
-
- final ByteBuffer byteBuffer = ByteBuffer.wrap(recvbuf, 0, length);
- byteBuffer.order(ByteOrder.nativeOrder());
-
- parseNetlinkMessageBuffer(byteBuffer, whenMs);
- }
-
- private void parseNetlinkMessageBuffer(ByteBuffer byteBuffer, long whenMs) {
- while (byteBuffer.remaining() > 0) {
- final int position = byteBuffer.position();
- final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer);
- if (nlMsg == null || nlMsg.getHeader() == null) {
- byteBuffer.position(position);
- mLog.e("unparsable netlink msg: " + hexify(byteBuffer));
- break;
- }
-
- if (nlMsg instanceof NetlinkErrorMessage) {
- mLog.e("netlink error: " + nlMsg);
- continue;
- } else if (!(nlMsg instanceof RtNetlinkNeighborMessage)) {
- mLog.i("non-rtnetlink neighbor msg: " + nlMsg);
- continue;
- }
-
- evaluateRtNetlinkNeighborMessage((RtNetlinkNeighborMessage) nlMsg, whenMs);
- }
- }
-
- private void evaluateRtNetlinkNeighborMessage(
- RtNetlinkNeighborMessage neighMsg, long whenMs) {
- final short msgType = neighMsg.getHeader().nlmsg_type;
- final StructNdMsg ndMsg = neighMsg.getNdHeader();
- if (ndMsg == null) {
- mLog.e("RtNetlinkNeighborMessage without ND message header!");
- return;
- }
-
- final int ifindex = ndMsg.ndm_ifindex;
- final InetAddress destination = neighMsg.getDestination();
- final short nudState =
- (msgType == RTM_DELNEIGH)
- ? StructNdMsg.NUD_NONE
- : ndMsg.ndm_state;
-
- final NeighborEvent event = new NeighborEvent(
- whenMs, msgType, ifindex, destination, nudState,
- getMacAddress(neighMsg.getLinkLayerAddress()));
-
- if (VDBG) {
- Log.d(TAG, neighMsg.toString());
- }
- if (DBG) {
- Log.d(TAG, event.toString());
- }
-
- mConsumer.accept(event);
- }
-
- private static MacAddress getMacAddress(byte[] linkLayerAddress) {
- if (linkLayerAddress != null) {
- try {
- return MacAddress.fromBytes(linkLayerAddress);
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Failed to parse link-layer address: " + hexify(linkLayerAddress));
- }
- }
-
- return null;
- }
-}
diff --git a/src/android/net/util/FdEventsReader.java b/src/android/net/util/FdEventsReader.java
deleted file mode 100644
index 5a1154f..0000000
--- a/src/android/net/util/FdEventsReader.java
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
- * Copyright (C) 2016 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.util;
-
-import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
-import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.os.MessageQueue;
-import android.system.ErrnoException;
-import android.system.OsConstants;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-
-
-/**
- * This class encapsulates the mechanics of registering a file descriptor
- * with a thread's Looper and handling read events (and errors).
- *
- * Subclasses MUST implement createFd() and SHOULD override handlePacket(). They MAY override
- * onStop() and onStart().
- *
- * Subclasses can expect a call life-cycle like the following:
- *
- * [1] when a client calls start(), createFd() is called, followed by the onStart() hook if all
- * goes well. Implementations may override onStart() for additional initialization.
- *
- * [2] yield, waiting for read event or error notification:
- *
- * [a] readPacket() && handlePacket()
- *
- * [b] if (no error):
- * goto 2
- * else:
- * goto 3
- *
- * [3] when a client calls stop(), the onStop() hook is called (unless already stopped or never
- * started). Implementations may override onStop() for additional cleanup.
- *
- * The packet receive buffer is recycled on every read call, so subclasses
- * should make any copies they would like inside their handlePacket()
- * implementation.
- *
- * All public methods MUST only be called from the same thread with which
- * the Handler constructor argument is associated.
- *
- * @param <BufferType> the type of the buffer used to read data.
- */
-public abstract class FdEventsReader<BufferType> {
- private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
- private static final int UNREGISTER_THIS_FD = 0;
-
- @NonNull
- private final Handler mHandler;
- @NonNull
- private final MessageQueue mQueue;
- @NonNull
- private final BufferType mBuffer;
- @Nullable
- private FileDescriptor mFd;
- private long mPacketsReceived;
-
- protected static void closeFd(FileDescriptor fd) {
- try {
- SocketUtils.closeSocket(fd);
- } catch (IOException ignored) {
- }
- }
-
- protected FdEventsReader(@NonNull Handler h, @NonNull BufferType buffer) {
- mHandler = h;
- mQueue = mHandler.getLooper().getQueue();
- mBuffer = buffer;
- }
-
- /** Start this FdEventsReader. */
- public boolean start() {
- if (!onCorrectThread()) {
- throw new IllegalStateException("start() called from off-thread");
- }
-
- return createAndRegisterFd();
- }
-
- /** Stop this FdEventsReader and destroy the file descriptor. */
- public void stop() {
- if (!onCorrectThread()) {
- throw new IllegalStateException("stop() called from off-thread");
- }
-
- unregisterAndDestroyFd();
- }
-
- @NonNull
- public Handler getHandler() {
- return mHandler;
- }
-
- protected abstract int recvBufSize(@NonNull BufferType buffer);
-
- /** Returns the size of the receive buffer. */
- public int recvBufSize() {
- return recvBufSize(mBuffer);
- }
-
- /**
- * Get the number of successful calls to {@link #readPacket(FileDescriptor, Object)}.
- *
- * <p>A call was successful if {@link #readPacket(FileDescriptor, Object)} returned a value > 0.
- */
- public final long numPacketsReceived() {
- return mPacketsReceived;
- }
-
- /**
- * Subclasses MUST create the listening socket here, including setting all desired socket
- * options, interface or address/port binding, etc. The socket MUST be created nonblocking.
- */
- @Nullable
- protected abstract FileDescriptor createFd();
-
- /**
- * Implementations MUST return the bytes read or throw an Exception.
- *
- * <p>The caller may throw a {@link ErrnoException} with {@link OsConstants#EAGAIN} or
- * {@link OsConstants#EINTR}, in which case {@link FdEventsReader} will ignore the buffer
- * contents and respectively wait for further input or retry the read immediately. For all other
- * exceptions, the {@link FdEventsReader} will be stopped with no more interactions with this
- * method.
- */
- protected abstract int readPacket(@NonNull FileDescriptor fd, @NonNull BufferType buffer)
- throws Exception;
-
- /**
- * Called by the main loop for every packet. Any desired copies of
- * |recvbuf| should be made in here, as the underlying byte array is
- * reused across all reads.
- */
- protected void handlePacket(@NonNull BufferType recvbuf, int length) {}
-
- /**
- * Called by the main loop to log errors. In some cases |e| may be null.
- */
- protected void logError(@NonNull String msg, @Nullable Exception e) {}
-
- /**
- * Called by start(), if successful, just prior to returning.
- */
- protected void onStart() {}
-
- /**
- * Called by stop() just prior to returning.
- */
- protected void onStop() {}
-
- private boolean createAndRegisterFd() {
- if (mFd != null) return true;
-
- try {
- mFd = createFd();
- } catch (Exception e) {
- logError("Failed to create socket: ", e);
- closeFd(mFd);
- mFd = null;
- }
-
- if (mFd == null) return false;
-
- mQueue.addOnFileDescriptorEventListener(
- mFd,
- FD_EVENTS,
- (fd, events) -> {
- // Always call handleInput() so read/recvfrom are given
- // a proper chance to encounter a meaningful errno and
- // perhaps log a useful error message.
- if (!isRunning() || !handleInput()) {
- unregisterAndDestroyFd();
- return UNREGISTER_THIS_FD;
- }
- return FD_EVENTS;
- });
- onStart();
- return true;
- }
-
- private boolean isRunning() {
- return (mFd != null) && mFd.valid();
- }
-
- // Keep trying to read until we get EAGAIN/EWOULDBLOCK or some fatal error.
- private boolean handleInput() {
- while (isRunning()) {
- final int bytesRead;
-
- try {
- bytesRead = readPacket(mFd, mBuffer);
- if (bytesRead < 1) {
- if (isRunning()) logError("Socket closed, exiting", null);
- break;
- }
- mPacketsReceived++;
- } catch (ErrnoException e) {
- if (e.errno == OsConstants.EAGAIN) {
- // We've read everything there is to read this time around.
- return true;
- } else if (e.errno == OsConstants.EINTR) {
- continue;
- } else {
- if (isRunning()) logError("readPacket error: ", e);
- break;
- }
- } catch (Exception e) {
- if (isRunning()) logError("readPacket error: ", e);
- break;
- }
-
- try {
- handlePacket(mBuffer, bytesRead);
- } catch (Exception e) {
- logError("handlePacket error: ", e);
- break;
- }
- }
-
- return false;
- }
-
- private void unregisterAndDestroyFd() {
- if (mFd == null) return;
-
- mQueue.removeOnFileDescriptorEventListener(mFd);
- closeFd(mFd);
- mFd = null;
- onStop();
- }
-
- private boolean onCorrectThread() {
- return (mHandler.getLooper() == Looper.myLooper());
- }
-}
diff --git a/src/android/net/util/PacketReader.java b/src/android/net/util/PacketReader.java
deleted file mode 100644
index 0be7187..0000000
--- a/src/android/net/util/PacketReader.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2018 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.util;
-
-import static java.lang.Math.max;
-
-import android.os.Handler;
-import android.system.Os;
-
-import java.io.FileDescriptor;
-
-/**
- * Specialization of {@link FdEventsReader} that reads packets into a byte array.
- *
- * TODO: rename this class to something more correctly descriptive (something
- * like [or less horrible than] FdReadEventsHandler?).
- */
-public abstract class PacketReader extends FdEventsReader<byte[]> {
-
- public static final int DEFAULT_RECV_BUF_SIZE = 2 * 1024;
-
- protected PacketReader(Handler h) {
- this(h, DEFAULT_RECV_BUF_SIZE);
- }
-
- protected PacketReader(Handler h, int recvBufSize) {
- super(h, new byte[max(recvBufSize, DEFAULT_RECV_BUF_SIZE)]);
- }
-
- @Override
- protected final int recvBufSize(byte[] buffer) {
- return buffer.length;
- }
-
- /**
- * Subclasses MAY override this to change the default read() implementation
- * in favour of, say, recvfrom().
- *
- * Implementations MUST return the bytes read or throw an Exception.
- */
- @Override
- protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception {
- return Os.read(fd, packetBuffer, 0, packetBuffer.length);
- }
-}
diff --git a/src/com/android/networkstack/netlink/TcpInfo.java b/src/com/android/networkstack/netlink/TcpInfo.java
index e6036b5..31a408f 100644
--- a/src/com/android/networkstack/netlink/TcpInfo.java
+++ b/src/com/android/networkstack/netlink/TcpInfo.java
@@ -22,11 +22,9 @@ import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
import java.util.Objects;
/**
@@ -91,27 +89,39 @@ public class TcpInfo {
}
private static final String TAG = "TcpInfo";
- private final Map<Field, Number> mFieldsValues;
+ @VisibleForTesting
+ static final int LOST_OFFSET = getFieldOffset(Field.LOST);
+ @VisibleForTesting
+ static final int RETRANSMITS_OFFSET = getFieldOffset(Field.RETRANSMITS);
+ @VisibleForTesting
+ static final int SEGS_IN_OFFSET = getFieldOffset(Field.SEGS_IN);
+ @VisibleForTesting
+ static final int SEGS_OUT_OFFSET = getFieldOffset(Field.SEGS_OUT);
+ final int mSegsIn;
+ final int mSegsOut;
+ final int mLost;
+ final int mRetransmits;
+
+ private static int getFieldOffset(@NonNull final Field needle) {
+ int offset = 0;
+ for (final Field field : Field.values()) {
+ if (field == needle) return offset;
+ offset += field.size;
+ }
+ throw new IllegalArgumentException("Unknown field");
+ }
private TcpInfo(@NonNull ByteBuffer bytes, int infolen) {
- final int start = bytes.position();
- final LinkedHashMap<Field, Number> fields = new LinkedHashMap<>();
- for (final Field field : Field.values()) {
- switch (field.size) {
- case Byte.BYTES:
- fields.put(field, getByte(bytes, start, infolen));
- break;
- case Integer.BYTES:
- fields.put(field, getInt(bytes, start, infolen));
- break;
- case Long.BYTES:
- fields.put(field, getLong(bytes, start, infolen));
- break;
- default:
- Log.e(TAG, "Unexpected size:" + field.size);
- }
+ // SEGS_IN is the last required field in the buffer, so if the buffer is long enough for
+ // SEGS_IN it's long enough for everything
+ if (SEGS_IN_OFFSET + Field.SEGS_IN.size > infolen) {
+ throw new IllegalArgumentException("Length " + infolen + " is less than required.");
}
- mFieldsValues = Collections.unmodifiableMap(fields);
+ final int start = bytes.position();
+ mSegsIn = bytes.getInt(start + SEGS_IN_OFFSET);
+ mSegsOut = bytes.getInt(start + SEGS_OUT_OFFSET);
+ mLost = bytes.getInt(start + LOST_OFFSET);
+ mRetransmits = bytes.get(start + RETRANSMITS_OFFSET);
// tcp_info structure grows over time as new fields are added. Jump to the end of the
// structure, as unknown fields might remain at the end of the structure if the tcp_info
// struct was expanded.
@@ -119,12 +129,11 @@ public class TcpInfo {
}
@VisibleForTesting
- TcpInfo(@NonNull Map<Field, Number> info) {
- final LinkedHashMap<Field, Number> fields = new LinkedHashMap<>();
- for (final Field field : Field.values()) {
- fields.put(field, info.get(field));
- }
- mFieldsValues = Collections.unmodifiableMap(fields);
+ TcpInfo(int retransmits, int lost, int segsOut, int segsIn) {
+ mRetransmits = retransmits;
+ mLost = lost;
+ mSegsOut = segsOut;
+ mSegsIn = segsIn;
}
/** Parse a TcpInfo from a giving ByteBuffer with a specific length. */
@@ -132,53 +141,13 @@ public class TcpInfo {
public static TcpInfo parse(@NonNull ByteBuffer bytes, int infolen) {
try {
return new TcpInfo(bytes, infolen);
- } catch (BufferUnderflowException | IllegalArgumentException e) {
+ } catch (BufferUnderflowException | BufferOverflowException | IllegalArgumentException
+ | IndexOutOfBoundsException e) {
Log.e(TAG, "parsing error.", e);
return null;
}
}
- /**
- * Helper function for handling different struct tcp_info versions in the kernel.
- */
- private static boolean isValidTargetPosition(int start, int len, int pos, int targetBytes)
- throws IllegalArgumentException {
- // Equivalent to new Range(start, start + len).contains(new Range(pos, pos + targetBytes))
- if (len < 0 || targetBytes < 0) throw new IllegalArgumentException();
- // Check that start < pos < start + len
- if (pos < start || pos > start + len) return false;
- // Pos is inside the range and targetBytes is positive. Offset is valid if end of 2nd range
- // is below end of 1st range.
- return pos + targetBytes <= start + len;
- }
-
- /** Get value for specific key. */
- @Nullable
- public Number getValue(@NonNull Field key) {
- return mFieldsValues.get(key);
- }
-
- @Nullable
- private static Byte getByte(@NonNull ByteBuffer buffer, int start, int len) {
- if (!isValidTargetPosition(start, len, buffer.position(), Byte.BYTES)) return null;
-
- return buffer.get();
- }
-
- @Nullable
- private static Integer getInt(@NonNull ByteBuffer buffer, int start, int len) {
- if (!isValidTargetPosition(start, len, buffer.position(), Integer.BYTES)) return null;
-
- return buffer.getInt();
- }
-
- @Nullable
- private static Long getLong(@NonNull ByteBuffer buffer, int start, int len) {
- if (!isValidTargetPosition(start, len, buffer.position(), Long.BYTES)) return null;
-
- return buffer.getLong();
- }
-
private static String decodeWscale(byte num) {
return String.valueOf((num >> 4) & 0x0f) + ":" + String.valueOf(num & 0x0f);
}
@@ -210,33 +179,18 @@ public class TcpInfo {
if (!(obj instanceof TcpInfo)) return false;
TcpInfo other = (TcpInfo) obj;
- for (final Field key : mFieldsValues.keySet()) {
- if (!Objects.equals(mFieldsValues.get(key), other.mFieldsValues.get(key))) {
- return false;
- }
- }
- return true;
+ return mSegsIn == other.mSegsIn && mSegsOut == other.mSegsOut
+ && mRetransmits == other.mRetransmits && mLost == other.mLost;
}
@Override
public int hashCode() {
- return Objects.hash(mFieldsValues.values().toArray());
+ return Objects.hash(mLost, mRetransmits, mSegsIn, mSegsOut);
}
@Override
public String toString() {
- String str = "TcpInfo{ ";
- for (final Field key : mFieldsValues.keySet()) {
- str += key.name().toLowerCase() + "=";
- if (key == Field.STATE) {
- str += getTcpStateName(mFieldsValues.get(key).intValue()) + " ";
- } else if (key == Field.WSCALE) {
- str += decodeWscale(mFieldsValues.get(key).byteValue()) + " ";
- } else {
- str += mFieldsValues.get(key) + " ";
- }
- }
- str += "}";
- return str;
+ return "TcpInfo{lost=" + mLost + ", retransmit=" + mRetransmits + ", received=" + mSegsIn
+ + ", sent=" + mSegsOut + "}";
}
}
diff --git a/src/com/android/networkstack/netlink/TcpSocketTracker.java b/src/com/android/networkstack/netlink/TcpSocketTracker.java
index 78813bd..f660f81 100644
--- a/src/com/android/networkstack/netlink/TcpSocketTracker.java
+++ b/src/com/android/networkstack/netlink/TcpSocketTracker.java
@@ -340,16 +340,16 @@ public class TcpSocketTracker {
return null;
}
- stat.sentCount = current.tcpInfo.getValue(TcpInfo.Field.SEGS_OUT).intValue();
- stat.receivedCount = current.tcpInfo.getValue(TcpInfo.Field.SEGS_IN).intValue();
- stat.lostCount = current.tcpInfo.getValue(TcpInfo.Field.LOST).intValue();
- stat.retransmitCount = current.tcpInfo.getValue(TcpInfo.Field.RETRANSMITS).intValue();
+ stat.sentCount = current.tcpInfo.mSegsOut;
+ stat.receivedCount = current.tcpInfo.mSegsIn;
+ stat.lostCount = current.tcpInfo.mLost;
+ stat.retransmitCount = current.tcpInfo.mRetransmits;
if (previous != null && previous.tcpInfo != null) {
- stat.sentCount -= previous.tcpInfo.getValue(TcpInfo.Field.SEGS_OUT).intValue();
- stat.receivedCount -= previous.tcpInfo.getValue(TcpInfo.Field.SEGS_IN).intValue();
- stat.lostCount -= previous.tcpInfo.getValue(TcpInfo.Field.LOST).intValue();
- stat.retransmitCount -= previous.tcpInfo.getValue(TcpInfo.Field.RETRANSMITS).intValue();
+ stat.sentCount -= previous.tcpInfo.mSegsOut;
+ stat.receivedCount -= previous.tcpInfo.mSegsIn;
+ stat.lostCount -= previous.tcpInfo.mLost;
+ stat.retransmitCount -= previous.tcpInfo.mRetransmits;
}
return stat;
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index e5f21eb..18b8c77 100644
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -139,25 +139,36 @@ import androidx.annotation.BoolRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
+import androidx.annotation.VisibleForTesting;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.RingBufferIndices;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.internal.util.TrafficStatsConstants;
import com.android.networkstack.R;
+import com.android.networkstack.apishim.CaptivePortalDataShim;
+import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
+import com.android.networkstack.apishim.NetworkInformationShimImpl;
import com.android.networkstack.apishim.ShimUtils;
+import com.android.networkstack.apishim.UnsupportedApiLevelException;
import com.android.networkstack.metrics.DataStallDetectionStats;
import com.android.networkstack.metrics.DataStallStatsUtils;
import com.android.networkstack.netlink.TcpSocketTracker;
import com.android.networkstack.util.DnsUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
+
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -165,12 +176,15 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Random;
import java.util.StringJoiner;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* {@hide}
@@ -191,6 +205,11 @@ public class NetworkMonitor extends StateMachine {
private static final int SOCKET_TIMEOUT_MS = 10000;
private static final int PROBE_TIMEOUT_MS = 3000;
+ private static final int CAPPORT_API_MAX_JSON_LENGTH = 4096;
+ private static final String ACCEPT_HEADER = "Accept";
+ private static final String CONTENT_TYPE_HEADER = "Content-Type";
+ private static final String CAPPORT_API_CONTENT_TYPE = "application/captive+json";
+
enum EvaluationResult {
VALIDATED(true),
CAPTIVE_PORTAL(false);
@@ -754,7 +773,12 @@ public class NetworkMonitor extends StateMachine {
maybeDisableHttpsProbing(true /* acceptPartial */);
break;
case EVENT_LINK_PROPERTIES_CHANGED:
+ final Uri oldCapportUrl = getCaptivePortalApiUrl(mLinkProperties);
mLinkProperties = (LinkProperties) message.obj;
+ final Uri newCapportUrl = getCaptivePortalApiUrl(mLinkProperties);
+ if (!Objects.equals(oldCapportUrl, newCapportUrl)) {
+ sendMessage(CMD_FORCE_REEVALUATION, NO_UID, 0);
+ }
break;
case EVENT_NETWORK_CAPABILITIES_CHANGED:
mNetworkCapabilities = (NetworkCapabilities) message.obj;
@@ -958,6 +982,8 @@ public class NetworkMonitor extends StateMachine {
// Being in the EvaluatingState State indicates the Network is being evaluated for internet
// connectivity, or that the user has indicated that this network is unwanted.
private class EvaluatingState extends State {
+ private Uri mEvaluatingCapportUrl;
+
@Override
public void enter() {
// If we have already started to track time spent in EvaluatingState
@@ -973,6 +999,7 @@ public class NetworkMonitor extends StateMachine {
}
mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS;
mEvaluateAttempts = 0;
+ mEvaluatingCapportUrl = getCaptivePortalApiUrl(mLinkProperties);
// Reset all current probe results to zero, but retain current validation state until
// validation succeeds or fails.
mEvaluationState.clearProbeResults();
@@ -1023,10 +1050,8 @@ public class NetworkMonitor extends StateMachine {
transitionTo(mProbingState);
return HANDLED;
case CMD_FORCE_REEVALUATION:
- // Before IGNORE_REEVALUATE_ATTEMPTS attempts are made,
- // ignore any re-evaluation requests. After, restart the
- // evaluation process via EvaluatingState#enter.
- return (mEvaluateAttempts < IGNORE_REEVALUATE_ATTEMPTS) ? HANDLED : NOT_HANDLED;
+ // The evaluation process restarts via EvaluatingState#enter.
+ return shouldAcceptForceRevalidation() ? NOT_HANDLED : HANDLED;
// Disable HTTPS probe and transition to EvaluatingPrivateDnsState because:
// 1. Network is connected and finish the network validation.
// 2. NetworkMonitor detects network is partial connectivity and user accepts it.
@@ -1039,6 +1064,15 @@ public class NetworkMonitor extends StateMachine {
}
}
+ private boolean shouldAcceptForceRevalidation() {
+ // If the captive portal URL has changed since the last evaluation attempt, always
+ // revalidate. Otherwise, ignore any re-evaluation requests before
+ // IGNORE_REEVALUATE_ATTEMPTS are made.
+ return mEvaluateAttempts >= IGNORE_REEVALUATE_ATTEMPTS
+ || !Objects.equals(
+ mEvaluatingCapportUrl, getCaptivePortalApiUrl(mLinkProperties));
+ }
+
@Override
public void exit() {
TrafficStats.clearThreadStatsUid();
@@ -1810,15 +1844,10 @@ public class NetworkMonitor extends StateMachine {
final int oldTag = TrafficStats.getAndSetThreadStatsTag(
TrafficStatsConstants.TAG_SYSTEM_PROBE);
try {
- urlConnection = (HttpURLConnection) mCleartextDnsNetwork.openConnection(url);
- urlConnection.setInstanceFollowRedirects(probeType == ValidationProbeEvent.PROBE_PAC);
- urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
- urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
- urlConnection.setRequestProperty("Connection", "close");
- urlConnection.setUseCaches(false);
- if (mCaptivePortalUserAgent != null) {
- urlConnection.setRequestProperty("User-Agent", mCaptivePortalUserAgent);
- }
+ // Follow redirects for PAC probes as such probes verify connectivity by fetching the
+ // PAC proxy file, which may be configured behind a redirect.
+ final boolean followRedirect = probeType == ValidationProbeEvent.PROBE_PAC;
+ urlConnection = makeProbeConnection(url, followRedirect);
// cannot read request header after connection
String requestHeader = urlConnection.getRequestProperties().toString();
@@ -1886,45 +1915,216 @@ public class NetworkMonitor extends StateMachine {
}
}
- private CaptivePortalProbeResult sendParallelHttpProbes(
- ProxyInfo proxy, URL httpsUrl, URL httpUrl) {
- // Number of probes to wait for. If a probe completes with a conclusive answer
- // it shortcuts the latch immediately by forcing the count to 0.
- final CountDownLatch latch = new CountDownLatch(2);
+ private HttpURLConnection makeProbeConnection(URL url, boolean followRedirects)
+ throws IOException {
+ final HttpURLConnection conn = (HttpURLConnection) mCleartextDnsNetwork.openConnection(url);
+ conn.setInstanceFollowRedirects(followRedirects);
+ conn.setConnectTimeout(SOCKET_TIMEOUT_MS);
+ conn.setReadTimeout(SOCKET_TIMEOUT_MS);
+ conn.setRequestProperty("Connection", "close");
+ conn.setUseCaches(false);
+ if (mCaptivePortalUserAgent != null) {
+ conn.setRequestProperty("User-Agent", mCaptivePortalUserAgent);
+ }
+ return conn;
+ }
- final class ProbeThread extends Thread {
- private final boolean mIsHttps;
- private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED;
+ @VisibleForTesting
+ @NonNull
+ protected static String readAsString(InputStream is, int maxLength, Charset charset)
+ throws IOException {
+ final InputStreamReader reader = new InputStreamReader(is, charset);
+ final char[] buffer = new char[1000];
+ final StringBuilder builder = new StringBuilder();
+ int totalReadLength = 0;
+ while (totalReadLength < maxLength) {
+ final int availableLength = Math.min(maxLength - totalReadLength, buffer.length);
+ final int currentLength = reader.read(buffer, 0, availableLength);
+ if (currentLength < 0) break; // EOF
- ProbeThread(boolean isHttps) {
- mIsHttps = isHttps;
- }
+ totalReadLength += currentLength;
+ builder.append(buffer, 0, currentLength);
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Attempt to extract the {@link Charset} of the response from its Content-Type header.
+ *
+ * <p>If the {@link Charset} cannot be extracted, UTF-8 is returned by default.
+ */
+ @VisibleForTesting
+ @NonNull
+ protected static Charset extractCharset(@Nullable String contentTypeHeader) {
+ if (contentTypeHeader == null) return StandardCharsets.UTF_8;
+ // See format in https://tools.ietf.org/html/rfc7231#section-3.1.1.1
+ final Pattern charsetPattern = Pattern.compile("; *charset=\"?([^ ;\"]+)\"?",
+ Pattern.CASE_INSENSITIVE);
+ final Matcher matcher = charsetPattern.matcher(contentTypeHeader);
+ if (!matcher.find()) return StandardCharsets.UTF_8;
+
+ try {
+ return Charset.forName(matcher.group(1));
+ } catch (IllegalArgumentException e) {
+ return StandardCharsets.UTF_8;
+ }
+ }
+
+ private abstract static class ProbeThread extends Thread {
+ private final CountDownLatch mLatch;
+ private final ProxyInfo mProxy;
+ private final URL mUrl;
+ protected final Uri mCaptivePortalApiUrl;
+
+ protected ProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url,
+ Uri captivePortalApiUrl) {
+ mLatch = latch;
+ mProxy = proxy;
+ mUrl = url;
+ mCaptivePortalApiUrl = captivePortalApiUrl;
+ }
+
+ private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED;
- public CaptivePortalProbeResult result() {
- return mResult;
+ public CaptivePortalProbeResult result() {
+ return mResult;
+ }
+
+ protected abstract CaptivePortalProbeResult sendProbe(ProxyInfo proxy, URL url);
+ public abstract boolean isConclusiveResult(CaptivePortalProbeResult result);
+
+ @Override
+ public void run() {
+ mResult = sendProbe(mProxy, mUrl);
+ if (isConclusiveResult(mResult)) {
+ // Stop waiting immediately if any probe is conclusive.
+ while (mLatch.getCount() > 0) {
+ mLatch.countDown();
+ }
}
+ // Signal this probe has completed.
+ mLatch.countDown();
+ }
+ }
- @Override
- public void run() {
- if (mIsHttps) {
- mResult =
- sendDnsAndHttpProbes(proxy, httpsUrl, ValidationProbeEvent.PROBE_HTTPS);
- } else {
- mResult = sendDnsAndHttpProbes(proxy, httpUrl, ValidationProbeEvent.PROBE_HTTP);
+ final class HttpsProbeThread extends ProbeThread {
+ HttpsProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) {
+ super(latch, proxy, url, captivePortalApiUrl);
+ }
+
+ @Override
+ protected CaptivePortalProbeResult sendProbe(ProxyInfo proxy, URL url) {
+ return sendDnsAndHttpProbes(proxy, url, ValidationProbeEvent.PROBE_HTTPS);
+ }
+
+ @Override
+ public boolean isConclusiveResult(CaptivePortalProbeResult result) {
+ // isPortal() is not expected on the HTTPS probe, but check it nonetheless.
+ // In case the capport API is available, the API is authoritative on whether there is
+ // a portal, so the HTTPS probe is not enough to conclude there is connectivity,
+ // and a determination will be made once the capport API probe returns. Note that the
+ // API can only force the system to detect a portal even if the HTTPS probe succeeds.
+ // It cannot force the system to detect no portal if the HTTPS probe fails.
+ return (result.isPortal() || result.isSuccessful()) && mCaptivePortalApiUrl == null;
+ }
+ }
+
+ final class HttpProbeThread extends ProbeThread {
+ private volatile CaptivePortalDataShim mCapportData;
+ HttpProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) {
+ super(latch, proxy, url, captivePortalApiUrl);
+ }
+
+ CaptivePortalDataShim getCaptivePortalData() {
+ return mCapportData;
+ }
+
+ private CaptivePortalDataShim tryCapportApiProbe() {
+ if (mCaptivePortalApiUrl == null) return null;
+ validationLog("Fetching captive portal data from " + mCaptivePortalApiUrl);
+
+ final String apiContent;
+ try {
+ final URL url = new URL(mCaptivePortalApiUrl.toString());
+ if (!"https".equals(url.getProtocol())) {
+ validationLog("Invalid captive portal API protocol: " + url.getProtocol());
+ return null;
}
- if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) {
- // Stop waiting immediately if https succeeds or if http finds a portal.
- while (latch.getCount() > 0) {
- latch.countDown();
- }
+
+ final HttpURLConnection conn = makeProbeConnection(
+ url, true /* followRedirects */);
+ conn.setRequestProperty(ACCEPT_HEADER, CAPPORT_API_CONTENT_TYPE);
+ final int responseCode = conn.getResponseCode();
+ if (responseCode != 200) {
+ validationLog("Non-200 API response code: " + conn.getResponseCode());
+ return null;
}
- // Signal this probe has completed.
- latch.countDown();
+ final Charset charset = extractCharset(conn.getHeaderField(CONTENT_TYPE_HEADER));
+ if (charset != StandardCharsets.UTF_8) {
+ validationLog("Invalid charset for capport API: " + charset);
+ return null;
+ }
+
+ apiContent = readAsString(conn.getInputStream(),
+ CAPPORT_API_MAX_JSON_LENGTH, charset);
+ } catch (IOException e) {
+ validationLog("I/O error reading capport data: " + e.getMessage());
+ return null;
+ }
+
+ try {
+ final JSONObject info = new JSONObject(apiContent);
+ return CaptivePortalDataShimImpl.fromJson(info);
+ } catch (JSONException e) {
+ validationLog("Could not parse capport API JSON: " + e.getMessage());
+ return null;
+ } catch (UnsupportedApiLevelException e) {
+ validationLog("Platform API too low to support capport API");
+ return null;
}
}
- final ProbeThread httpsProbe = new ProbeThread(true);
- final ProbeThread httpProbe = new ProbeThread(false);
+ @Override
+ protected CaptivePortalProbeResult sendProbe(ProxyInfo proxy, URL url) {
+ mCapportData = tryCapportApiProbe();
+ if (mCapportData != null && mCapportData.isCaptive()) {
+ if (mCapportData.getUserPortalUrl() == null) {
+ validationLog("Missing user-portal-url from capport response");
+ return sendDnsAndHttpProbes(proxy, url, ValidationProbeEvent.PROBE_HTTP);
+ }
+ final String loginUrlString = mCapportData.getUserPortalUrl().toString();
+ // Starting from R (where CaptivePortalData was introduced), the captive portal app
+ // delegates to NetworkMonitor for verifying when the network validates instead of
+ // probing the detectUrl. So pass the detectUrl to have the portal open on that,
+ // page; CaptivePortalLogin will not use it for probing.
+ return new CaptivePortalProbeResult(
+ CaptivePortalProbeResult.PORTAL_CODE,
+ loginUrlString /* redirectUrl */,
+ loginUrlString /* detectUrl */);
+ }
+
+ // If the API says it's not captive, still check for HTTP connectivity. This helps
+ // with partial connectivity detection, and a broken API saying that there is no
+ // redirect when there is one.
+ return sendDnsAndHttpProbes(proxy, url, ValidationProbeEvent.PROBE_HTTP);
+ }
+
+ @Override
+ public boolean isConclusiveResult(CaptivePortalProbeResult result) {
+ return result.isPortal();
+ }
+ }
+
+ private CaptivePortalProbeResult sendParallelHttpProbes(
+ ProxyInfo proxy, URL httpsUrl, URL httpUrl) {
+ // Number of probes to wait for. If a probe completes with a conclusive answer
+ // it shortcuts the latch immediately by forcing the count to 0.
+ final CountDownLatch latch = new CountDownLatch(2);
+
+ final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties);
+ final HttpsProbeThread httpsProbe = new HttpsProbeThread(latch, proxy, httpsUrl,
+ capportApiUrl);
+ final HttpProbeThread httpProbe = new HttpProbeThread(latch, proxy, httpUrl, capportApiUrl);
try {
httpsProbe.start();
@@ -1939,12 +2139,14 @@ public class NetworkMonitor extends StateMachine {
final CaptivePortalProbeResult httpResult = httpProbe.result();
// Look for a conclusive probe result first.
- if (httpResult.isPortal()) {
+ if (httpProbe.isConclusiveResult(httpResult)) {
+ maybeReportCaptivePortalData(httpProbe.getCaptivePortalData());
reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, httpResult);
return httpResult;
}
- // httpsResult.isPortal() is not expected, but check it nonetheless.
- if (httpsResult.isPortal() || httpsResult.isSuccessful()) {
+
+ if (httpsProbe.isConclusiveResult(httpsResult)) {
+ maybeReportCaptivePortalData(httpProbe.getCaptivePortalData());
reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTPS, httpsResult);
return httpsResult;
}
@@ -1963,6 +2165,7 @@ public class NetworkMonitor extends StateMachine {
// Otherwise wait until http and https probes completes and use their results.
try {
httpProbe.join();
+ maybeReportCaptivePortalData(httpProbe.getCaptivePortalData());
reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, httpProbe.result());
if (httpProbe.result().isPortal()) {
@@ -2463,6 +2666,18 @@ public class NetworkMonitor extends StateMachine {
mEvaluationState.noteProbeResult(probeResult, succeeded);
}
+ private void maybeReportCaptivePortalData(@Nullable CaptivePortalDataShim data) {
+ // Do not clear data even if it is null: access points should not stop serving the API, so
+ // if the API disappears this is treated as a temporary failure, and previous data should
+ // remain valid.
+ if (data == null) return;
+ try {
+ data.notifyChanged(mCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error notifying ConnectivityService of new capport data", e);
+ }
+ }
+
/**
* Interface for logging dns results.
*/
@@ -2490,4 +2705,8 @@ public class NetworkMonitor extends StateMachine {
return ((type & DATA_STALL_EVALUATION_TYPE_DNS) != 0)
? new DnsStallDetector(threshold) : null;
}
+
+ private static Uri getCaptivePortalApiUrl(LinkProperties lp) {
+ return NetworkInformationShimImpl.newInstance().getCaptivePortalApiUrl(lp);
+ }
}