diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/android/net/dhcp/DhcpLease.java | 37 | ||||
-rw-r--r-- | src/android/net/dhcp/DhcpLeaseRepository.java | 66 | ||||
-rw-r--r-- | src/android/net/dhcp/DhcpServer.java | 21 | ||||
-rw-r--r-- | src/android/net/ip/IpNeighborMonitor.java | 241 | ||||
-rw-r--r-- | src/android/net/util/FdEventsReader.java | 259 | ||||
-rw-r--r-- | src/android/net/util/PacketReader.java | 59 | ||||
-rw-r--r-- | src/com/android/networkstack/netlink/TcpInfo.java | 132 | ||||
-rw-r--r-- | src/com/android/networkstack/netlink/TcpSocketTracker.java | 16 | ||||
-rw-r--r-- | src/com/android/server/connectivity/NetworkMonitor.java | 311 |
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); + } } |