Merge "Check MAINLINE_NETWORK_STACK in ConnectivityService"
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index 2ea23ec..68ac46c 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -68,6 +68,7 @@
import java.lang.annotation.RetentionPolicy;
import java.net.InetAddress;
import java.net.InetSocketAddress;
+import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -177,10 +178,10 @@
* The lookup key for a {@link NetworkInfo} object. Retrieve with
* {@link android.content.Intent#getParcelableExtra(String)}.
*
- * @deprecated Since {@link NetworkInfo} can vary based on UID, applications
- * should always obtain network information through
- * {@link #getActiveNetworkInfo()}.
- * @see #EXTRA_NETWORK_TYPE
+ * @deprecated The {@link NetworkInfo} object is deprecated, as many of its properties
+ * can't accurately represent modern network characteristics.
+ * Please obtain information about networks from the {@link NetworkCapabilities}
+ * or {@link LinkProperties} objects instead.
*/
@Deprecated
public static final String EXTRA_NETWORK_INFO = "networkInfo";
@@ -189,7 +190,11 @@
* Network type which triggered a {@link #CONNECTIVITY_ACTION} broadcast.
*
* @see android.content.Intent#getIntExtra(String, int)
+ * @deprecated The network type is not rich enough to represent the characteristics
+ * of modern networks. Please use {@link NetworkCapabilities} instead,
+ * in particular the transports.
*/
+ @Deprecated
public static final String EXTRA_NETWORK_TYPE = "networkType";
/**
@@ -1243,9 +1248,13 @@
* is no current default network.
*
* {@hide}
+ * @deprecated please use {@link #getLinkProperties(Network)} on the return
+ * value of {@link #getActiveNetwork()} instead. In particular,
+ * this method will return non-null LinkProperties even if the
+ * app is blocked by policy from using this network.
*/
@RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
- @UnsupportedAppUsage
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 109783091)
public LinkProperties getActiveLinkProperties() {
try {
return mService.getActiveLinkProperties();
@@ -1881,7 +1890,8 @@
* @param callback A {@link SocketKeepalive.Callback}. Used for notifications about keepalive
* changes. Must be extended by applications that use this API.
*
- * @return A {@link SocketKeepalive} object, which can be used to control this keepalive object.
+ * @return A {@link SocketKeepalive} object that can be used to control the keepalive on the
+ * given socket.
**/
public SocketKeepalive createSocketKeepalive(@NonNull Network network,
@NonNull UdpEncapsulationSocket socket,
@@ -1910,6 +1920,8 @@
* @param callback A {@link SocketKeepalive.Callback}. Used for notifications about keepalive
* changes. Must be extended by applications that use this API.
*
+ * @return A {@link SocketKeepalive} object that can be used to control the keepalive on the
+ * given socket.
* @hide
*/
@SystemApi
@@ -1925,6 +1937,34 @@
}
/**
+ * Request that keepalives be started on a TCP socket.
+ * The socket must be established.
+ *
+ * @param network The {@link Network} the socket is on.
+ * @param socket The socket that needs to be kept alive.
+ * @param executor The executor on which callback will be invoked. This implementation assumes
+ * the provided {@link Executor} runs the callbacks in sequence with no
+ * concurrency. Failing this, no guarantee of correctness can be made. It is
+ * the responsibility of the caller to ensure the executor provides this
+ * guarantee. A simple way of creating such an executor is with the standard
+ * tool {@code Executors.newSingleThreadExecutor}.
+ * @param callback A {@link SocketKeepalive.Callback}. Used for notifications about keepalive
+ * changes. Must be extended by applications that use this API.
+ *
+ * @return A {@link SocketKeepalive} object that can be used to control the keepalive on the
+ * given socket.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD)
+ public SocketKeepalive createSocketKeepalive(@NonNull Network network,
+ @NonNull Socket socket,
+ @NonNull Executor executor,
+ @NonNull Callback callback) {
+ return new TcpSocketKeepalive(mService, network, socket, executor, callback);
+ }
+
+ /**
* Ensure that a network route exists to deliver traffic to the specified
* host via the specified network interface. An attempt to add a route that
* already exists is ignored, but treated as successful.
@@ -3867,6 +3907,25 @@
}
/**
+ * Requests that the system open the captive portal app with the specified extras.
+ *
+ * <p>This endpoint is exclusively for use by the NetworkStack and is protected by the
+ * corresponding permission.
+ * @param appExtras Extras to include in the app start intent.
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+ public void startCaptivePortalApp(Bundle appExtras) {
+ try {
+ mService.startCaptivePortalAppInternal(appExtras);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Determine whether the device is configured to avoid bad wifi.
* @hide
*/
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index 78fafeb..92a5839 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -27,6 +27,7 @@
import android.net.NetworkRequest;
import android.net.NetworkState;
import android.net.ProxyInfo;
+import android.os.Bundle;
import android.os.IBinder;
import android.os.Messenger;
import android.os.ParcelFileDescriptor;
@@ -167,6 +168,7 @@
void setAcceptUnvalidated(in Network network, boolean accept, boolean always);
void setAvoidUnvalidated(in Network network);
void startCaptivePortalApp(in Network network);
+ void startCaptivePortalAppInternal(in Bundle appExtras);
boolean getAvoidBadWifi();
int getMultipathPreference(in Network Network);
@@ -188,6 +190,9 @@
int intervalSeconds, in Messenger messenger, in IBinder binder, String srcAddr,
String dstAddr);
+ void startTcpKeepalive(in Network network, in FileDescriptor fd, int intervalSeconds,
+ in Messenger messenger, in IBinder binder);
+
void stopKeepalive(in Network network, int slot);
String getCaptivePortalServerUrl();
diff --git a/core/java/android/net/KeepalivePacketData.java b/core/java/android/net/KeepalivePacketData.java
index 16555d8..18726f7 100644
--- a/core/java/android/net/KeepalivePacketData.java
+++ b/core/java/android/net/KeepalivePacketData.java
@@ -96,7 +96,7 @@
out.writeByteArray(mPacket);
}
- private KeepalivePacketData(Parcel in) {
+ protected KeepalivePacketData(Parcel in) {
srcAddress = NetworkUtils.numericToInetAddress(in.readString());
dstAddress = NetworkUtils.numericToInetAddress(in.readString());
srcPort = in.readInt();
diff --git a/core/java/android/net/NattKeepalivePacketData.java b/core/java/android/net/NattKeepalivePacketData.java
index dd2b108..bdb246f 100644
--- a/core/java/android/net/NattKeepalivePacketData.java
+++ b/core/java/android/net/NattKeepalivePacketData.java
@@ -16,6 +16,9 @@
package android.net;
+import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS;
+import static android.net.SocketKeepalive.ERROR_INVALID_PORT;
+
import android.net.SocketKeepalive.InvalidPacketException;
import android.net.util.IpUtils;
import android.system.OsConstants;
@@ -44,11 +47,11 @@
throws InvalidPacketException {
if (!(srcAddress instanceof Inet4Address) || !(dstAddress instanceof Inet4Address)) {
- throw new InvalidPacketException(SocketKeepalive.ERROR_INVALID_IP_ADDRESS);
+ throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
}
if (dstPort != NattSocketKeepalive.NATT_PORT) {
- throw new InvalidPacketException(SocketKeepalive.ERROR_INVALID_PORT);
+ throw new InvalidPacketException(ERROR_INVALID_PORT);
}
int length = IPV4_HEADER_LENGTH + UDP_HEADER_LENGTH + 1;
diff --git a/core/java/android/net/NetworkAgent.java b/core/java/android/net/NetworkAgent.java
index dfb6d6f..7bef690 100644
--- a/core/java/android/net/NetworkAgent.java
+++ b/core/java/android/net/NetworkAgent.java
@@ -177,6 +177,26 @@
*/
public static final int EVENT_SOCKET_KEEPALIVE = BASE + 13;
+ // TODO: move the above 2 constants down so they are in order once merge conflicts are resolved
+ /**
+ * Sent by the KeepaliveTracker to NetworkAgent to add a packet filter.
+ *
+ * For TCP keepalive offloads, keepalive packets are sent by the firmware. However, because the
+ * remote site will send ACK packets in response to the keepalive packets, the firmware also
+ * needs to be configured to properly filter the ACKs to prevent the system from waking up.
+ * This does not happen with UDP, so this message is TCP-specific.
+ * arg1 = slot number of the keepalive to filter for.
+ * obj = the keepalive packet to send repeatedly.
+ */
+ public static final int CMD_ADD_KEEPALIVE_PACKET_FILTER = BASE + 16;
+
+ /**
+ * Sent by the KeepaliveTracker to NetworkAgent to remove a packet filter. See
+ * {@link #CMD_ADD_KEEPALIVE_PACKET_FILTER}.
+ * arg1 = slot number of the keepalive packet filter to remove.
+ */
+ public static final int CMD_REMOVE_KEEPALIVE_PACKET_FILTER = BASE + 17;
+
/**
* Sent by ConnectivityService to inform this network transport of signal strength thresholds
* that when crossed should trigger a system wakeup and a NetworkCapabilities update.
@@ -312,6 +332,14 @@
preventAutomaticReconnect();
break;
}
+ case CMD_ADD_KEEPALIVE_PACKET_FILTER: {
+ addKeepalivePacketFilter(msg);
+ break;
+ }
+ case CMD_REMOVE_KEEPALIVE_PACKET_FILTER: {
+ removeKeepalivePacketFilter(msg);
+ break;
+ }
}
}
@@ -461,6 +489,24 @@
}
/**
+ * Called by ConnectivityService to add specific packet filter to network hardware to block
+ * ACKs matching the sent keepalive packets. Implementations that support this feature must
+ * override this method.
+ */
+ protected void addKeepalivePacketFilter(Message msg) {
+ onSocketKeepaliveEvent(msg.arg1, SocketKeepalive.ERROR_HARDWARE_UNSUPPORTED);
+ }
+
+ /**
+ * Called by ConnectivityService to remove a packet filter installed with
+ * {@link #addKeepalivePacketFilter(Message)}. Implementations that support this feature
+ * must override this method.
+ */
+ protected void removeKeepalivePacketFilter(Message msg) {
+ onSocketKeepaliveEvent(msg.arg1, SocketKeepalive.ERROR_HARDWARE_UNSUPPORTED);
+ }
+
+ /**
* Called by ConnectivityService to inform this network transport of signal strength thresholds
* that when crossed should trigger a system wakeup and a NetworkCapabilities update.
*/
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index 07668a9..0ae29b1 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -71,6 +71,18 @@
throws SocketException;
/**
+ * Attaches a socket filter that drops all of incoming packets.
+ * @param fd the socket's {@link FileDescriptor}.
+ */
+ public static native void attachDropAllBPFFilter(FileDescriptor fd) throws SocketException;
+
+ /**
+ * Detaches a socket filter.
+ * @param fd the socket's {@link FileDescriptor}.
+ */
+ public static native void detachBPFFilter(FileDescriptor fd) throws SocketException;
+
+ /**
* Configures a socket for receiving ICMPv6 router solicitations and sending advertisements.
* @param fd the socket's {@link FileDescriptor}.
* @param ifIndex the interface index.
@@ -170,6 +182,16 @@
private static native void addArpEntry(byte[] ethAddr, byte[] netAddr, String ifname,
FileDescriptor fd) throws IOException;
+
+ /**
+ * Get the tcp repair window associated with the {@code fd}.
+ *
+ * @param fd the tcp socket's {@link FileDescriptor}.
+ * @return a {@link TcpRepairWindow} object indicates tcp window size.
+ */
+ public static native TcpRepairWindow getTcpRepairWindow(FileDescriptor fd)
+ throws ErrnoException;
+
/**
* @see Inet4AddressUtils#intToInet4AddressHTL(int)
* @deprecated Use either {@link Inet4AddressUtils#intToInet4AddressHTH(int)}
diff --git a/core/java/android/net/SocketKeepalive.java b/core/java/android/net/SocketKeepalive.java
index a47c11a..07728be 100644
--- a/core/java/android/net/SocketKeepalive.java
+++ b/core/java/android/net/SocketKeepalive.java
@@ -19,6 +19,7 @@
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
@@ -110,14 +111,42 @@
public static final int MAX_INTERVAL_SEC = 3600;
/**
+ * An exception that embarks an error code.
+ * @hide
+ */
+ public static class ErrorCodeException extends Exception {
+ public final int error;
+ public ErrorCodeException(final int error, final Throwable e) {
+ super(e);
+ this.error = error;
+ }
+ public ErrorCodeException(final int error) {
+ this.error = error;
+ }
+ }
+
+ /**
+ * This socket is invalid.
+ * See the error code for details, and the optional cause.
+ * @hide
+ */
+ public static class InvalidSocketException extends ErrorCodeException {
+ public InvalidSocketException(final int error, final Throwable e) {
+ super(error, e);
+ }
+ public InvalidSocketException(final int error) {
+ super(error);
+ }
+ }
+
+ /**
* This packet is invalid.
* See the error code for details.
* @hide
*/
- public static class InvalidPacketException extends Exception {
- public final int error;
- public InvalidPacketException(int error) {
- this.error = error;
+ public static class InvalidPacketException extends ErrorCodeException {
+ public InvalidPacketException(final int error) {
+ super(error);
}
}
@@ -127,7 +156,7 @@
@NonNull private final SocketKeepalive.Callback mCallback;
@NonNull private final Looper mLooper;
@NonNull final Messenger mMessenger;
- @NonNull Integer mSlot;
+ @Nullable Integer mSlot;
SocketKeepalive(@NonNull IConnectivityManager service, @NonNull Network network,
@NonNull Executor executor, @NonNull Callback callback) {
diff --git a/core/java/android/net/TcpKeepalivePacketData.java b/core/java/android/net/TcpKeepalivePacketData.java
new file mode 100644
index 0000000..f07dfb6
--- /dev/null
+++ b/core/java/android/net/TcpKeepalivePacketData.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2019 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;
+
+import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.SocketKeepalive.InvalidPacketException;
+import android.net.util.IpUtils;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.system.OsConstants;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Objects;
+
+/**
+ * Represents the actual tcp keep alive packets which will be used for hardware offload.
+ * @hide
+ */
+public class TcpKeepalivePacketData extends KeepalivePacketData implements Parcelable {
+ private static final String TAG = "TcpKeepalivePacketData";
+
+ /** TCP sequence number. */
+ public final int tcpSeq;
+
+ /** TCP ACK number. */
+ public final int tcpAck;
+
+ /** TCP RCV window. */
+ public final int tcpWnd;
+
+ /** TCP RCV window scale. */
+ public final int tcpWndScale;
+
+ private static final int IPV4_HEADER_LENGTH = 20;
+ private static final int IPV6_HEADER_LENGTH = 40;
+ private static final int TCP_HEADER_LENGTH = 20;
+
+ // This should only be constructed via static factory methods, such as
+ // tcpKeepalivePacket.
+ private TcpKeepalivePacketData(TcpSocketInfo tcpDetails, byte[] data)
+ throws InvalidPacketException {
+ super(tcpDetails.srcAddress, tcpDetails.srcPort, tcpDetails.dstAddress,
+ tcpDetails.dstPort, data);
+ tcpSeq = tcpDetails.seq;
+ tcpAck = tcpDetails.ack;
+ // In the packet, the window is shifted right by the window scale.
+ tcpWnd = tcpDetails.rcvWnd;
+ tcpWndScale = tcpDetails.rcvWndScale;
+ }
+
+ /**
+ * Factory method to create tcp keepalive packet structure.
+ */
+ public static TcpKeepalivePacketData tcpKeepalivePacket(
+ TcpSocketInfo tcpDetails) throws InvalidPacketException {
+ final byte[] packet;
+ if ((tcpDetails.srcAddress instanceof Inet4Address)
+ && (tcpDetails.dstAddress instanceof Inet4Address)) {
+ packet = buildV4Packet(tcpDetails);
+ } else {
+ // TODO: support ipv6
+ throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+ }
+
+ return new TcpKeepalivePacketData(tcpDetails, packet);
+ }
+
+ /**
+ * Build ipv4 tcp keepalive packet, not including the link-layer header.
+ */
+ // TODO : if this code is ever moved to the network stack, factorize constants with the ones
+ // over there.
+ private static byte[] buildV4Packet(TcpSocketInfo tcpDetails) {
+ final int length = IPV4_HEADER_LENGTH + TCP_HEADER_LENGTH;
+ ByteBuffer buf = ByteBuffer.allocate(length);
+ buf.order(ByteOrder.BIG_ENDIAN);
+ // IP version and TOS. TODO : fetch this from getsockopt(SOL_IP, IP_TOS)
+ buf.putShort((short) 0x4500);
+ buf.putShort((short) length);
+ buf.putInt(0x4000); // ID, flags=DF, offset
+ // TODO : fetch TTL from getsockopt(SOL_IP, IP_TTL)
+ buf.put((byte) 64);
+ buf.put((byte) OsConstants.IPPROTO_TCP);
+ final int ipChecksumOffset = buf.position();
+ buf.putShort((short) 0); // IP checksum
+ buf.put(tcpDetails.srcAddress.getAddress());
+ buf.put(tcpDetails.dstAddress.getAddress());
+ buf.putShort((short) tcpDetails.srcPort);
+ buf.putShort((short) tcpDetails.dstPort);
+ buf.putInt(tcpDetails.seq); // Sequence Number
+ buf.putInt(tcpDetails.ack); // ACK
+ buf.putShort((short) 0x5010); // TCP length=5, flags=ACK
+ buf.putShort((short) (tcpDetails.rcvWnd >> tcpDetails.rcvWndScale)); // Window size
+ final int tcpChecksumOffset = buf.position();
+ buf.putShort((short) 0); // TCP checksum
+ // URG is not set therefore the urgent pointer is not included
+ buf.putShort(ipChecksumOffset, IpUtils.ipChecksum(buf, 0));
+ buf.putShort(tcpChecksumOffset, IpUtils.tcpChecksum(
+ buf, 0, IPV4_HEADER_LENGTH, TCP_HEADER_LENGTH));
+
+ return buf.array();
+ }
+
+ // TODO: add buildV6Packet.
+
+ /** Represents tcp/ip information. */
+ // TODO: Replace TcpSocketInfo with TcpKeepalivePacketDataParcelable.
+ public static class TcpSocketInfo {
+ public final InetAddress srcAddress;
+ public final InetAddress dstAddress;
+ public final int srcPort;
+ public final int dstPort;
+ public final int seq;
+ public final int ack;
+ public final int rcvWnd;
+ public final int rcvWndScale;
+
+ public TcpSocketInfo(InetAddress sAddr, int sPort, InetAddress dAddr,
+ int dPort, int writeSeq, int readSeq, int rWnd, int rWndScale) {
+ srcAddress = sAddr;
+ dstAddress = dAddr;
+ srcPort = sPort;
+ dstPort = dPort;
+ seq = writeSeq;
+ ack = readSeq;
+ rcvWnd = rWnd;
+ rcvWndScale = rWndScale;
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable final Object o) {
+ if (!(o instanceof TcpKeepalivePacketData)) return false;
+ final TcpKeepalivePacketData other = (TcpKeepalivePacketData) o;
+ return this.srcAddress.equals(other.srcAddress)
+ && this.dstAddress.equals(other.dstAddress)
+ && this.srcPort == other.srcPort
+ && this.dstPort == other.dstPort
+ && this.tcpAck == other.tcpAck
+ && this.tcpSeq == other.tcpSeq
+ && this.tcpWnd == other.tcpWnd
+ && this.tcpWndScale == other.tcpWndScale;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(srcAddress, dstAddress, srcPort, dstPort, tcpAck, tcpSeq, tcpWnd,
+ tcpWndScale);
+ }
+
+ /* Parcelable Implementation. */
+ /* Note that this object implements parcelable (and needs to keep doing this as it inherits
+ * from a class that does), but should usually be parceled as a stable parcelable using
+ * the toStableParcelable() and fromStableParcelable() methods.
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Write to parcel. */
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(tcpSeq);
+ out.writeInt(tcpAck);
+ out.writeInt(tcpWnd);
+ out.writeInt(tcpWndScale);
+ }
+
+ private TcpKeepalivePacketData(Parcel in) {
+ super(in);
+ tcpSeq = in.readInt();
+ tcpAck = in.readInt();
+ tcpWnd = in.readInt();
+ tcpWndScale = in.readInt();
+ }
+
+ /** Parcelable Creator. */
+ public static final Parcelable.Creator<TcpKeepalivePacketData> CREATOR =
+ new Parcelable.Creator<TcpKeepalivePacketData>() {
+ public TcpKeepalivePacketData createFromParcel(Parcel in) {
+ return new TcpKeepalivePacketData(in);
+ }
+
+ public TcpKeepalivePacketData[] newArray(int size) {
+ return new TcpKeepalivePacketData[size];
+ }
+ };
+
+ /**
+ * Convert this TcpKeepalivePacketData to a TcpKeepalivePacketDataParcelable.
+ */
+ @NonNull
+ public TcpKeepalivePacketDataParcelable toStableParcelable() {
+ final TcpKeepalivePacketDataParcelable parcel = new TcpKeepalivePacketDataParcelable();
+ parcel.srcAddress = srcAddress.getAddress();
+ parcel.srcPort = srcPort;
+ parcel.dstAddress = dstAddress.getAddress();
+ parcel.dstPort = dstPort;
+ parcel.seq = tcpSeq;
+ parcel.ack = tcpAck;
+ return parcel;
+ }
+
+ @Override
+ public String toString() {
+ return "saddr: " + srcAddress
+ + " daddr: " + dstAddress
+ + " sport: " + srcPort
+ + " dport: " + dstPort
+ + " seq: " + tcpSeq
+ + " ack: " + tcpAck
+ + " wnd: " + tcpWnd
+ + " wndScale: " + tcpWndScale;
+ }
+}
diff --git a/core/java/android/net/TcpRepairWindow.java b/core/java/android/net/TcpRepairWindow.java
new file mode 100644
index 0000000..86034f0
--- /dev/null
+++ b/core/java/android/net/TcpRepairWindow.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 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;
+
+/**
+ * Corresponds to C's {@code struct tcp_repair_window} from
+ * include/uapi/linux/tcp.h
+ *
+ * @hide
+ */
+public final class TcpRepairWindow {
+ public final int sndWl1;
+ public final int sndWnd;
+ public final int maxWindow;
+ public final int rcvWnd;
+ public final int rcvWup;
+ public final int rcvWndScale;
+
+ /**
+ * Constructs an instance with the given field values.
+ */
+ public TcpRepairWindow(final int sndWl1, final int sndWnd, final int maxWindow,
+ final int rcvWnd, final int rcvWup, final int rcvWndScale) {
+ this.sndWl1 = sndWl1;
+ this.sndWnd = sndWnd;
+ this.maxWindow = maxWindow;
+ this.rcvWnd = rcvWnd;
+ this.rcvWup = rcvWup;
+ this.rcvWndScale = rcvWndScale;
+ }
+}
diff --git a/core/java/android/net/TcpSocketKeepalive.java b/core/java/android/net/TcpSocketKeepalive.java
new file mode 100644
index 0000000..8f6ee7b
--- /dev/null
+++ b/core/java/android/net/TcpSocketKeepalive.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2019 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;
+
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.net.Socket;
+import java.util.concurrent.Executor;
+
+/** @hide */
+final class TcpSocketKeepalive extends SocketKeepalive {
+
+ private final Socket mSocket;
+
+ TcpSocketKeepalive(@NonNull IConnectivityManager service,
+ @NonNull Network network,
+ @NonNull Socket socket,
+ @NonNull Executor executor,
+ @NonNull Callback callback) {
+ super(service, network, executor, callback);
+ mSocket = socket;
+ }
+
+ /**
+ * Starts keepalives. {@code mSocket} must be a connected TCP socket.
+ *
+ * - The application must not write to or read from the socket after calling this method, until
+ * onDataReceived, onStopped, or onError are called. If it does, the keepalive will fail
+ * with {@link #ERROR_SOCKET_NOT_IDLE}, or {@code #ERROR_INVALID_SOCKET} if the socket
+ * experienced an error (as in poll(2) returned POLLERR); if this happens, the data received
+ * from the socket may be invalid, and the socket can't be recovered.
+ * - If the socket has data in the send or receive buffer, then this call will fail with
+ * {@link #ERROR_SOCKET_NOT_IDLE} and can be retried after the data has been processed.
+ * An app could ensure this by using an application-layer protocol where it can receive
+ * acknowledgement that it will go into keepalive mode. It could then go into keepalive
+ * mode after having read the acknowledgement, draining the socket.
+ */
+ @Override
+ void startImpl(int intervalSec) {
+ try {
+ final FileDescriptor fd = mSocket.getFileDescriptor$();
+ mService.startTcpKeepalive(mNetwork, fd, intervalSec, mMessenger, new Binder());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error starting packet keepalive: ", e);
+ stopLooper();
+ }
+ }
+
+ @Override
+ void stopImpl() {
+ try {
+ if (mSlot != null) {
+ mService.stopKeepalive(mNetwork, mSlot);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error stopping packet keepalive: ", e);
+ stopLooper();
+ }
+ }
+}
diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp
index 7eddcfe..cfb2dd1 100644
--- a/core/jni/android_net_NetUtils.cpp
+++ b/core/jni/android_net_NetUtils.cpp
@@ -29,6 +29,7 @@
#include <net/if.h>
#include <linux/filter.h>
#include <linux/if_arp.h>
+#include <linux/tcp.h>
#include <netinet/ether.h>
#include <netinet/icmp6.h>
#include <netinet/ip.h>
@@ -226,6 +227,34 @@
}
}
+static void android_net_utils_attachDropAllBPFFilter(JNIEnv *env, jobject clazz, jobject javaFd)
+{
+ struct sock_filter filter_code[] = {
+ // Reject all.
+ BPF_STMT(BPF_RET | BPF_K, 0)
+ };
+ struct sock_fprog filter = {
+ sizeof(filter_code) / sizeof(filter_code[0]),
+ filter_code,
+ };
+
+ int fd = jniGetFDFromFileDescriptor(env, javaFd);
+ if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
+ }
+}
+
+static void android_net_utils_detachBPFFilter(JNIEnv *env, jobject clazz, jobject javaFd)
+{
+ int dummy = 0;
+ int fd = jniGetFDFromFileDescriptor(env, javaFd);
+ if (setsockopt(fd, SOL_SOCKET, SO_DETACH_FILTER, &dummy, sizeof(dummy)) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(SO_DETACH_FILTER): %s", strerror(errno));
+ }
+
+}
static void android_net_utils_setupRaSocket(JNIEnv *env, jobject clazz, jobject javaFd,
jint ifIndex)
{
@@ -458,6 +487,41 @@
return answer;
}
+static jobject android_net_utils_getTcpRepairWindow(JNIEnv *env, jobject thiz, jobject javaFd) {
+ if (javaFd == NULL) {
+ jniThrowNullPointerException(env, NULL);
+ return NULL;
+ }
+
+ int fd = jniGetFDFromFileDescriptor(env, javaFd);
+ struct tcp_repair_window trw = {};
+ socklen_t size = sizeof(trw);
+
+ // Obtain the parameters of the TCP repair window.
+ int rc = getsockopt(fd, IPPROTO_TCP, TCP_REPAIR_WINDOW, &trw, &size);
+ if (rc == -1) {
+ throwErrnoException(env, "getsockopt : TCP_REPAIR_WINDOW", errno);
+ return NULL;
+ }
+
+ struct tcp_info tcpinfo = {};
+ socklen_t tcpinfo_size = sizeof(tcp_info);
+
+ // Obtain the window scale from the tcp info structure. This contains a scale factor that
+ // should be applied to the window size.
+ rc = getsockopt(fd, IPPROTO_TCP, TCP_INFO, &tcpinfo, &tcpinfo_size);
+ if (rc == -1) {
+ throwErrnoException(env, "getsockopt : TCP_INFO", errno);
+ return NULL;
+ }
+
+ jclass class_TcpRepairWindow = env->FindClass("android/net/TcpRepairWindow");
+ jmethodID ctor = env->GetMethodID(class_TcpRepairWindow, "<init>", "(IIIIII)V");
+
+ return env->NewObject(class_TcpRepairWindow, ctor, trw.snd_wl1, trw.snd_wnd, trw.max_window,
+ trw.rcv_wnd, trw.rcv_wup, tcpinfo.tcpi_rcv_wscale);
+}
+
// ----------------------------------------------------------------------------
/*
@@ -475,6 +539,9 @@
{ "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDhcpFilter },
{ "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachRaFilter },
{ "attachControlPacketFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachControlPacketFilter },
+ { "attachDropAllBPFFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDropAllBPFFilter },
+ { "detachBPFFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_detachBPFFilter },
+ { "getTcpRepairWindow", "(Ljava/io/FileDescriptor;)Landroid/net/TcpRepairWindow;", (void*) android_net_utils_getTcpRepairWindow },
{ "setupRaSocket", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_setupRaSocket },
{ "resNetworkSend", "(I[BII)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkSend },
{ "resNetworkQuery", "(ILjava/lang/String;III)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkQuery },
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index d838cc6..f2d4ae2 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -3235,6 +3235,25 @@
});
}
+ /**
+ * NetworkStack endpoint to start the captive portal app. The NetworkStack needs to use this
+ * endpoint as it does not have INTERACT_ACROSS_USERS_FULL itself.
+ * @param appExtras Bundle to use as intent extras for the captive portal application.
+ * Must be treated as opaque to avoid preventing the captive portal app to
+ * update its arguments.
+ */
+ @Override
+ public void startCaptivePortalAppInternal(Bundle appExtras) {
+ mContext.checkCallingOrSelfPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+
+ final Intent appIntent = new Intent(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
+ appIntent.putExtras(appExtras);
+ appIntent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ Binder.withCleanCallingIdentity(() ->
+ mContext.startActivityAsUser(appIntent, UserHandle.CURRENT));
+ }
+
public boolean avoidBadWifi() {
return mMultinetworkPolicyTracker.getAvoidBadWifi();
}
@@ -6334,6 +6353,14 @@
}
@Override
+ public void startTcpKeepalive(Network network, FileDescriptor fd, int intervalSeconds,
+ Messenger messenger, IBinder binder) {
+ enforceKeepalivePermission();
+ mKeepaliveTracker.startTcpKeepalive(
+ getNetworkAgentInfoForNetwork(network), fd, intervalSeconds, messenger, binder);
+ }
+
+ @Override
public void stopKeepalive(Network network, int slot) {
mHandler.sendMessage(mHandler.obtainMessage(
NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE, slot, SocketKeepalive.SUCCESS, network));
diff --git a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
index 07e28f9..6cff57d 100644
--- a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
+++ b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
@@ -17,6 +17,8 @@
package com.android.server.connectivity;
import static android.net.NattSocketKeepalive.NATT_PORT;
+import static android.net.NetworkAgent.CMD_ADD_KEEPALIVE_PACKET_FILTER;
+import static android.net.NetworkAgent.CMD_REMOVE_KEEPALIVE_PACKET_FILTER;
import static android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE;
import static android.net.NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE;
import static android.net.NetworkAgent.EVENT_SOCKET_KEEPALIVE;
@@ -37,6 +39,9 @@
import android.net.NetworkAgent;
import android.net.NetworkUtils;
import android.net.SocketKeepalive.InvalidPacketException;
+import android.net.SocketKeepalive.InvalidSocketException;
+import android.net.TcpKeepalivePacketData;
+import android.net.TcpKeepalivePacketData.TcpSocketInfo;
import android.net.util.IpUtils;
import android.os.Binder;
import android.os.Handler;
@@ -65,7 +70,7 @@
*
* Provides methods to stop and start keepalive requests, and keeps track of keepalives across all
* networks. This class is tightly coupled to ConnectivityService. It is not thread-safe and its
- * methods must be called only from the ConnectivityService handler thread.
+ * handle* methods must be called only from the ConnectivityService handler thread.
*/
public class KeepaliveTracker {
@@ -78,9 +83,12 @@
private final HashMap <NetworkAgentInfo, HashMap<Integer, KeepaliveInfo>> mKeepalives =
new HashMap<> ();
private final Handler mConnectivityServiceHandler;
+ @NonNull
+ private final TcpKeepaliveController mTcpController;
public KeepaliveTracker(Handler handler) {
mConnectivityServiceHandler = handler;
+ mTcpController = new TcpKeepaliveController(handler);
}
/**
@@ -96,20 +104,33 @@
private final int mUid;
private final int mPid;
private final NetworkAgentInfo mNai;
+ private final int mType;
+ private final FileDescriptor mFd;
- /** Keepalive slot. A small integer that identifies this keepalive among the ones handled
- * by this network. */
+ public static final int TYPE_NATT = 1;
+ public static final int TYPE_TCP = 2;
+
+ // Keepalive slot. A small integer that identifies this keepalive among the ones handled
+ // by this network.
private int mSlot = NO_KEEPALIVE;
// Packet data.
private final KeepalivePacketData mPacket;
private final int mInterval;
- // Whether the keepalive is started or not.
- public boolean isStarted;
+ // Whether the keepalive is started or not. The initial state is NOT_STARTED.
+ private static final int NOT_STARTED = 1;
+ private static final int STARTING = 2;
+ private static final int STARTED = 3;
+ private int mStartedState = NOT_STARTED;
- public KeepaliveInfo(Messenger messenger, IBinder binder, NetworkAgentInfo nai,
- KeepalivePacketData packet, int interval) {
+ KeepaliveInfo(@NonNull Messenger messenger,
+ @NonNull IBinder binder,
+ @NonNull NetworkAgentInfo nai,
+ @NonNull KeepalivePacketData packet,
+ int interval,
+ int type,
+ @NonNull FileDescriptor fd) {
mMessenger = messenger;
mBinder = binder;
mPid = Binder.getCallingPid();
@@ -118,6 +139,8 @@
mNai = nai;
mPacket = packet;
mInterval = interval;
+ mType = type;
+ mFd = fd;
try {
mBinder.linkToDeath(this, 0);
@@ -130,32 +153,40 @@
return mNai;
}
+ private String startedStateString(final int state) {
+ switch (state) {
+ case NOT_STARTED : return "NOT_STARTED";
+ case STARTING : return "STARTING";
+ case STARTED : return "STARTED";
+ }
+ throw new IllegalArgumentException("Unknown state");
+ }
+
public String toString() {
- return new StringBuffer("KeepaliveInfo [")
- .append(" network=").append(mNai.network)
- .append(" isStarted=").append(isStarted)
- .append(" ")
- .append(IpUtils.addressAndPortToString(mPacket.srcAddress, mPacket.srcPort))
- .append("->")
- .append(IpUtils.addressAndPortToString(mPacket.dstAddress, mPacket.dstPort))
- .append(" interval=" + mInterval)
- .append(" packetData=" + HexDump.toHexString(mPacket.getPacket()))
- .append(" uid=").append(mUid).append(" pid=").append(mPid)
- .append(" ]")
- .toString();
+ return "KeepaliveInfo ["
+ + " network=" + mNai.network
+ + " startedState=" + startedStateString(mStartedState)
+ + " "
+ + IpUtils.addressAndPortToString(mPacket.srcAddress, mPacket.srcPort)
+ + "->"
+ + IpUtils.addressAndPortToString(mPacket.dstAddress, mPacket.dstPort)
+ + " interval=" + mInterval
+ + " uid=" + mUid + " pid=" + mPid
+ + " packetData=" + HexDump.toHexString(mPacket.getPacket())
+ + " ]";
}
/** Sends a message back to the application via its SocketKeepalive.Callback. */
void notifyMessenger(int slot, int err) {
+ if (DBG) {
+ Log.d(TAG, "notify keepalive " + mSlot + " on " + mNai.network + " for " + err);
+ }
KeepaliveTracker.this.notifyMessenger(mMessenger, slot, err);
}
/** Called when the application process is killed. */
public void binderDied() {
- // Not called from ConnectivityService handler thread, so send it a message.
- mConnectivityServiceHandler.obtainMessage(
- NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE,
- mSlot, BINDER_DIED, mNai.network).sendToTarget();
+ stop(BINDER_DIED);
}
void unlinkDeathRecipient() {
@@ -202,7 +233,26 @@
int error = isValid();
if (error == SUCCESS) {
Log.d(TAG, "Starting keepalive " + mSlot + " on " + mNai.name());
- mNai.asyncChannel.sendMessage(CMD_START_SOCKET_KEEPALIVE, slot, mInterval, mPacket);
+ switch (mType) {
+ case TYPE_NATT:
+ mNai.asyncChannel
+ .sendMessage(CMD_START_SOCKET_KEEPALIVE, slot, mInterval, mPacket);
+ break;
+ case TYPE_TCP:
+ mTcpController.startSocketMonitor(mFd, this, mSlot);
+ mNai.asyncChannel
+ .sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER, slot, 0 /* Unused */,
+ mPacket);
+ // TODO: check result from apf and notify of failure as needed.
+ mNai.asyncChannel
+ .sendMessage(CMD_START_SOCKET_KEEPALIVE, slot, mInterval, mPacket);
+ break;
+ default:
+ Log.wtf(TAG, "Starting keepalive with unknown type: " + mType);
+ handleStopKeepalive(mNai, mSlot, error);
+ return;
+ }
+ mStartedState = STARTING;
} else {
handleStopKeepalive(mNai, mSlot, error);
return;
@@ -216,15 +266,27 @@
Log.e(TAG, "Cannot stop unowned keepalive " + mSlot + " on " + mNai.network);
}
}
- if (isStarted) {
+ if (NOT_STARTED != mStartedState) {
Log.d(TAG, "Stopping keepalive " + mSlot + " on " + mNai.name());
- mNai.asyncChannel.sendMessage(CMD_STOP_SOCKET_KEEPALIVE, mSlot);
+ if (mType == TYPE_NATT) {
+ mNai.asyncChannel.sendMessage(CMD_STOP_SOCKET_KEEPALIVE, mSlot);
+ } else if (mType == TYPE_TCP) {
+ mNai.asyncChannel.sendMessage(CMD_STOP_SOCKET_KEEPALIVE, mSlot);
+ mNai.asyncChannel.sendMessage(CMD_REMOVE_KEEPALIVE_PACKET_FILTER, mSlot);
+ mTcpController.stopSocketMonitor(mSlot);
+ } else {
+ Log.wtf(TAG, "Stopping keepalive with unknown type: " + mType);
+ }
}
// TODO: at the moment we unconditionally return failure here. In cases where the
// NetworkAgent is alive, should we ask it to reply, so it can return failure?
notifyMessenger(mSlot, reason);
unlinkDeathRecipient();
}
+
+ void onFileDescriptorInitiatedStop(final int socketKeepaliveReason) {
+ handleStopKeepalive(mNai, mSlot, socketKeepaliveReason);
+ }
}
void notifyMessenger(Messenger messenger, int slot, int err) {
@@ -328,20 +390,38 @@
return;
}
- if (reason == SUCCESS && !ki.isStarted) {
+ // This can be called in a number of situations :
+ // - startedState is STARTING.
+ // - reason is SUCCESS => go to STARTED.
+ // - reason isn't SUCCESS => it's an error starting. Go to NOT_STARTED and stop keepalive.
+ // - startedState is STARTED.
+ // - reason is SUCCESS => it's a success stopping. Go to NOT_STARTED and stop keepalive.
+ // - reason isn't SUCCESS => it's an error in exec. Go to NOT_STARTED and stop keepalive.
+ // The control is not supposed to ever come here if the state is NOT_STARTED. This is
+ // because in NOT_STARTED state, the code will switch to STARTING before sending messages
+ // to start, and the only way to NOT_STARTED is this function, through the edges outlined
+ // above : in all cases, keepalive gets stopped and can't restart without going into
+ // STARTING as messages are ordered. This also depends on the hardware processing the
+ // messages in order.
+ // TODO : clarify this code and get rid of mStartedState. Using a StateMachine is an
+ // option.
+ if (reason == SUCCESS && KeepaliveInfo.STARTING == ki.mStartedState) {
// Keepalive successfully started.
if (DBG) Log.d(TAG, "Started keepalive " + slot + " on " + nai.name());
- ki.isStarted = true;
+ ki.mStartedState = KeepaliveInfo.STARTED;
ki.notifyMessenger(slot, reason);
} else {
// Keepalive successfully stopped, or error.
- ki.isStarted = false;
+ ki.mStartedState = KeepaliveInfo.NOT_STARTED;
if (reason == SUCCESS) {
+ // The message indicated success stopping : don't call handleStopKeepalive.
if (DBG) Log.d(TAG, "Successfully stopped keepalive " + slot + " on " + nai.name());
} else {
+ // The message indicated some error trying to start or during the course of
+ // keepalive : do call handleStopKeepalive.
+ handleStopKeepalive(nai, slot, reason);
if (DBG) Log.d(TAG, "Keepalive " + slot + " on " + nai.name() + " error " + reason);
}
- handleStopKeepalive(nai, slot, reason);
}
}
@@ -379,10 +459,49 @@
notifyMessenger(messenger, NO_KEEPALIVE, e.error);
return;
}
- KeepaliveInfo ki = new KeepaliveInfo(messenger, binder, nai, packet, intervalSeconds);
- Log.d(TAG, "Created keepalive: " + ki.toString());
+ KeepaliveInfo ki = new KeepaliveInfo(messenger, binder, nai, packet, intervalSeconds,
+ KeepaliveInfo.TYPE_NATT, null);
mConnectivityServiceHandler.obtainMessage(
- CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
+ NetworkAgent.CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
+ }
+
+ /**
+ * Called by ConnectivityService to start TCP keepalive on a file descriptor.
+ *
+ * In order to offload keepalive for application correctly, sequence number, ack number and
+ * other fields are needed to form the keepalive packet. Thus, this function synchronously
+ * puts the socket into repair mode to get the necessary information. After the socket has been
+ * put into repair mode, the application cannot access the socket until reverted to normal.
+ *
+ * See {@link android.net.SocketKeepalive}.
+ **/
+ public void startTcpKeepalive(@Nullable NetworkAgentInfo nai,
+ @NonNull FileDescriptor fd,
+ int intervalSeconds,
+ @NonNull Messenger messenger,
+ @NonNull IBinder binder) {
+ if (nai == null) {
+ notifyMessenger(messenger, NO_KEEPALIVE, ERROR_INVALID_NETWORK);
+ return;
+ }
+
+ TcpKeepalivePacketData packet = null;
+ try {
+ TcpSocketInfo tsi = TcpKeepaliveController.switchToRepairMode(fd);
+ packet = TcpKeepalivePacketData.tcpKeepalivePacket(tsi);
+ } catch (InvalidPacketException | InvalidSocketException e) {
+ try {
+ TcpKeepaliveController.switchOutOfRepairMode(fd);
+ } catch (ErrnoException e1) {
+ Log.e(TAG, "Couldn't move fd out of repair mode after failure to start keepalive");
+ }
+ notifyMessenger(messenger, NO_KEEPALIVE, e.error);
+ return;
+ }
+ KeepaliveInfo ki = new KeepaliveInfo(messenger, binder, nai, packet, intervalSeconds,
+ KeepaliveInfo.TYPE_TCP, fd);
+ Log.d(TAG, "Created keepalive: " + ki.toString());
+ mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
}
/**
diff --git a/services/core/java/com/android/server/connectivity/TcpKeepaliveController.java b/services/core/java/com/android/server/connectivity/TcpKeepaliveController.java
new file mode 100644
index 0000000..8a9ac23
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/TcpKeepaliveController.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.connectivity;
+
+import static android.net.SocketKeepalive.DATA_RECEIVED;
+import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
+import static android.net.SocketKeepalive.ERROR_SOCKET_NOT_IDLE;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+import static android.system.OsConstants.FIONREAD;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.TIOCOUTQ;
+
+import android.annotation.NonNull;
+import android.net.NetworkUtils;
+import android.net.SocketKeepalive.InvalidSocketException;
+import android.net.TcpKeepalivePacketData.TcpSocketInfo;
+import android.net.TcpRepairWindow;
+import android.os.Handler;
+import android.os.MessageQueue;
+import android.os.Messenger;
+import android.system.ErrnoException;
+import android.system.Int32Ref;
+import android.system.Os;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+
+/**
+ * Manage tcp socket which offloads tcp keepalive.
+ *
+ * The input socket will be changed to repair mode and the application
+ * will not have permission to read/write data. If the application wants
+ * to write data, it must stop tcp keepalive offload to leave repair mode
+ * first. If a remote packet arrives, repair mode will be turned off and
+ * offload will be stopped. The application will receive a callback to know
+ * it can start reading data.
+ *
+ * {start,stop}SocketMonitor are thread-safe, but care must be taken in the
+ * order in which they are called. Please note that while calling
+ * {@link #startSocketMonitor(FileDescriptor, Messenger, int)} multiple times
+ * with either the same slot or the same FileDescriptor without stopping it in
+ * between will result in an exception, calling {@link #stopSocketMonitor(int)}
+ * multiple times with the same int is explicitly a no-op.
+ * Please also note that switching the socket to repair mode is not synchronized
+ * with either of these operations and has to be done in an orderly fashion
+ * with stopSocketMonitor. Take care in calling these in the right order.
+ * @hide
+ */
+public class TcpKeepaliveController {
+ private static final String TAG = "TcpKeepaliveController";
+ private static final boolean DBG = false;
+
+ private final MessageQueue mFdHandlerQueue;
+
+ private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+
+ // Reference include/uapi/linux/tcp.h
+ private static final int TCP_REPAIR = 19;
+ private static final int TCP_REPAIR_QUEUE = 20;
+ private static final int TCP_QUEUE_SEQ = 21;
+ private static final int TCP_NO_QUEUE = 0;
+ private static final int TCP_RECV_QUEUE = 1;
+ private static final int TCP_SEND_QUEUE = 2;
+ private static final int TCP_REPAIR_OFF = 0;
+ private static final int TCP_REPAIR_ON = 1;
+ // Reference include/uapi/linux/sockios.h
+ private static final int SIOCINQ = FIONREAD;
+ private static final int SIOCOUTQ = TIOCOUTQ;
+
+ /**
+ * Keeps track of packet listeners.
+ * Key: slot number of keepalive offload.
+ * Value: {@link FileDescriptor} being listened to.
+ */
+ @GuardedBy("mListeners")
+ private final SparseArray<FileDescriptor> mListeners = new SparseArray<>();
+
+ public TcpKeepaliveController(final Handler connectivityServiceHandler) {
+ mFdHandlerQueue = connectivityServiceHandler.getLooper().getQueue();
+ }
+
+ /**
+ * Switch the tcp socket to repair mode and query tcp socket information.
+ *
+ * @param fd the fd of socket on which to use keepalive offload
+ * @return a {@link TcpKeepalivePacketData#TcpSocketInfo} object for current
+ * tcp/ip information.
+ */
+ // TODO : make this private. It's far too confusing that this gets called from outside
+ // at a time that nobody can understand.
+ public static TcpSocketInfo switchToRepairMode(FileDescriptor fd)
+ throws InvalidSocketException {
+ if (DBG) Log.i(TAG, "switchToRepairMode to start tcp keepalive : " + fd);
+ final SocketAddress srcSockAddr;
+ final SocketAddress dstSockAddr;
+ final InetAddress srcAddress;
+ final InetAddress dstAddress;
+ final int srcPort;
+ final int dstPort;
+ int seq;
+ final int ack;
+ final TcpRepairWindow trw;
+
+ // Query source address and port.
+ try {
+ srcSockAddr = Os.getsockname(fd);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Get sockname fail: ", e);
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+ }
+ if (srcSockAddr instanceof InetSocketAddress) {
+ srcAddress = getAddress((InetSocketAddress) srcSockAddr);
+ srcPort = getPort((InetSocketAddress) srcSockAddr);
+ } else {
+ Log.e(TAG, "Invalid or mismatched SocketAddress");
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET);
+ }
+ // Query destination address and port.
+ try {
+ dstSockAddr = Os.getpeername(fd);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Get peername fail: ", e);
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+ }
+ if (dstSockAddr instanceof InetSocketAddress) {
+ dstAddress = getAddress((InetSocketAddress) dstSockAddr);
+ dstPort = getPort((InetSocketAddress) dstSockAddr);
+ } else {
+ Log.e(TAG, "Invalid or mismatched peer SocketAddress");
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET);
+ }
+
+ // Query sequence and ack number
+ dropAllIncomingPackets(fd, true);
+ try {
+ // Enter tcp repair mode.
+ Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_ON);
+ // Check if socket is idle.
+ if (!isSocketIdle(fd)) {
+ throw new InvalidSocketException(ERROR_SOCKET_NOT_IDLE);
+ }
+ // Query write sequence number from SEND_QUEUE.
+ Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_SEND_QUEUE);
+ seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);
+ // Query read sequence number from RECV_QUEUE.
+ Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_RECV_QUEUE);
+ ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);
+ // Switch to NO_QUEUE to prevent illegal socket read/write in repair mode.
+ Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_NO_QUEUE);
+ // Finally, check if socket is still idle. TODO : this check needs to move to
+ // after starting polling to prevent a race.
+ if (!isSocketIdle(fd)) {
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET);
+ }
+
+ // Query tcp window size.
+ trw = NetworkUtils.getTcpRepairWindow(fd);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Exception reading TCP state from socket", e);
+ try {
+ Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_OFF);
+ } catch (ErrnoException ex) {
+ Log.e(TAG, "Exception while turning off repair mode due to exception", ex);
+ }
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+ } finally {
+ dropAllIncomingPackets(fd, false);
+ }
+
+ // Keepalive sequence number is last sequence number - 1. If it couldn't be retrieved,
+ // then it must be set to -1, so decrement in all cases.
+ seq = seq - 1;
+
+ return new TcpSocketInfo(srcAddress, srcPort, dstAddress, dstPort, seq, ack, trw.rcvWnd,
+ trw.rcvWndScale);
+ }
+
+ /**
+ * Switch the tcp socket out of repair mode.
+ *
+ * @param fd the fd of socket to switch back to normal.
+ */
+ // TODO : make this private.
+ public static void switchOutOfRepairMode(@NonNull final FileDescriptor fd)
+ throws ErrnoException {
+ Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_OFF);
+ }
+
+ /**
+ * Start monitoring incoming packets.
+ *
+ * @param fd socket fd to monitor.
+ * @param messenger a callback to notify socket status.
+ * @param slot keepalive slot.
+ */
+ public void startSocketMonitor(@NonNull final FileDescriptor fd,
+ @NonNull final KeepaliveInfo ki, final int slot) {
+ synchronized (mListeners) {
+ if (null != mListeners.get(slot)) {
+ throw new IllegalArgumentException("This slot is already taken");
+ }
+ for (int i = 0; i < mListeners.size(); ++i) {
+ if (fd.equals(mListeners.valueAt(i))) {
+ throw new IllegalArgumentException("This fd is already registered");
+ }
+ }
+ mFdHandlerQueue.addOnFileDescriptorEventListener(fd, FD_EVENTS, (readyFd, events) -> {
+ // This can't be called twice because the queue guarantees that once the listener
+ // is unregistered it can't be called again, even for a message that arrived
+ // before it was unregistered.
+ final int reason;
+ if (0 != (events & EVENT_ERROR)) {
+ reason = ERROR_INVALID_SOCKET;
+ } else {
+ reason = DATA_RECEIVED;
+ }
+ ki.onFileDescriptorInitiatedStop(reason);
+ // The listener returns the new set of events to listen to. Because 0 means no
+ // event, the listener gets unregistered.
+ return 0;
+ });
+ mListeners.put(slot, fd);
+ }
+ }
+
+ /** Stop socket monitor */
+ // This slot may have been stopped automatically already because the socket received data,
+ // was closed on the other end or otherwise suffered some error. In this case, this function
+ // is a no-op.
+ public void stopSocketMonitor(final int slot) {
+ final FileDescriptor fd;
+ synchronized (mListeners) {
+ fd = mListeners.get(slot);
+ if (null == fd) return;
+ mListeners.remove(slot);
+ }
+ mFdHandlerQueue.removeOnFileDescriptorEventListener(fd);
+ try {
+ if (DBG) Log.d(TAG, "Moving socket out of repair mode for stop : " + fd);
+ switchOutOfRepairMode(fd);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot switch socket out of repair mode", e);
+ // Well, there is not much to do here to recover
+ }
+ }
+
+ private static InetAddress getAddress(InetSocketAddress inetAddr) {
+ return inetAddr.getAddress();
+ }
+
+ private static int getPort(InetSocketAddress inetAddr) {
+ return inetAddr.getPort();
+ }
+
+ private static boolean isSocketIdle(FileDescriptor fd) throws ErrnoException {
+ return isReceiveQueueEmpty(fd) && isSendQueueEmpty(fd);
+ }
+
+ private static boolean isReceiveQueueEmpty(FileDescriptor fd)
+ throws ErrnoException {
+ Int32Ref result = new Int32Ref(-1);
+ Os.ioctlInt(fd, SIOCINQ, result);
+ if (result.value != 0) {
+ Log.e(TAG, "Read queue has data");
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean isSendQueueEmpty(FileDescriptor fd)
+ throws ErrnoException {
+ Int32Ref result = new Int32Ref(-1);
+ Os.ioctlInt(fd, SIOCOUTQ, result);
+ if (result.value != 0) {
+ Log.e(TAG, "Write queue has data");
+ return false;
+ }
+ return true;
+ }
+
+ private static void dropAllIncomingPackets(FileDescriptor fd, boolean enable)
+ throws InvalidSocketException {
+ try {
+ if (enable) {
+ NetworkUtils.attachDropAllBPFFilter(fd);
+ } else {
+ NetworkUtils.detachBPFFilter(fd);
+ }
+ } catch (SocketException e) {
+ Log.e(TAG, "Socket Exception: ", e);
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+ }
+ }
+}
diff --git a/tests/net/java/android/net/LinkPropertiesTest.java b/tests/net/java/android/net/LinkPropertiesTest.java
index 299fbef..bdde096 100644
--- a/tests/net/java/android/net/LinkPropertiesTest.java
+++ b/tests/net/java/android/net/LinkPropertiesTest.java
@@ -22,18 +22,15 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import android.net.IpPrefix;
-import android.net.LinkAddress;
-import android.net.LinkProperties;
import android.net.LinkProperties.CompareResult;
import android.net.LinkProperties.ProvisioningChange;
-import android.net.RouteInfo;
-import android.os.Parcel;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.system.OsConstants;
import android.util.ArraySet;
+import com.android.internal.util.TestUtils;
+
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -849,18 +846,6 @@
assertEquals(new ArraySet<>(expectRemoved), (new ArraySet<>(result.removed)));
}
- private void assertParcelingIsLossless(LinkProperties source) {
- Parcel p = Parcel.obtain();
- source.writeToParcel(p, /* flags */ 0);
- p.setDataPosition(0);
- final byte[] marshalled = p.marshall();
- p = Parcel.obtain();
- p.unmarshall(marshalled, 0, marshalled.length);
- p.setDataPosition(0);
- LinkProperties dest = LinkProperties.CREATOR.createFromParcel(p);
- assertEquals(source, dest);
- }
-
@Test
public void testLinkPropertiesParcelable() throws Exception {
LinkProperties source = new LinkProperties();
@@ -882,12 +867,12 @@
source.setNat64Prefix(new IpPrefix("2001:db8:1:2:64:64::/96"));
- assertParcelingIsLossless(source);
+ TestUtils.assertParcelingIsLossless(source, LinkProperties.CREATOR);
}
@Test
public void testParcelUninitialized() throws Exception {
LinkProperties empty = new LinkProperties();
- assertParcelingIsLossless(empty);
+ TestUtils.assertParcelingIsLossless(empty, LinkProperties.CREATOR);
}
}
diff --git a/tests/net/java/android/net/TcpKeepalivePacketDataTest.java b/tests/net/java/android/net/TcpKeepalivePacketDataTest.java
new file mode 100644
index 0000000..1f2dd27
--- /dev/null
+++ b/tests/net/java/android/net/TcpKeepalivePacketDataTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2019 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;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.net.SocketKeepalive.InvalidPacketException;
+import android.net.TcpKeepalivePacketData.TcpSocketInfo;
+
+import com.android.internal.util.TestUtils;
+
+import libcore.net.InetAddressUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+@RunWith(JUnit4.class)
+public final class TcpKeepalivePacketDataTest {
+
+ @Before
+ public void setUp() {}
+
+ @Test
+ public void testV4TcpKeepalivePacket() {
+ final InetAddress srcAddr = InetAddressUtils.parseNumericAddress("192.168.0.1");
+ final InetAddress dstAddr = InetAddressUtils.parseNumericAddress("192.168.0.10");
+ final int srcPort = 1234;
+ final int dstPort = 4321;
+ final int seq = 0x11111111;
+ final int ack = 0x22222222;
+ final int wnd = 8000;
+ final int wndScale = 2;
+ TcpKeepalivePacketData resultData = null;
+ TcpSocketInfo testInfo = new TcpSocketInfo(
+ srcAddr, srcPort, dstAddr, dstPort, seq, ack, wnd, wndScale);
+ try {
+ resultData = TcpKeepalivePacketData.tcpKeepalivePacket(testInfo);
+ } catch (InvalidPacketException e) {
+ fail("InvalidPacketException: " + e);
+ }
+
+ assertEquals(testInfo.srcAddress, resultData.srcAddress);
+ assertEquals(testInfo.dstAddress, resultData.dstAddress);
+ assertEquals(testInfo.srcPort, resultData.srcPort);
+ assertEquals(testInfo.dstPort, resultData.dstPort);
+ assertEquals(testInfo.seq, resultData.tcpSeq);
+ assertEquals(testInfo.ack, resultData.tcpAck);
+ assertEquals(testInfo.rcvWndScale, resultData.tcpWndScale);
+
+ TestUtils.assertParcelingIsLossless(resultData, TcpKeepalivePacketData.CREATOR);
+
+ final byte[] packet = resultData.getPacket();
+ // IP version and TOS.
+ ByteBuffer buf = ByteBuffer.wrap(packet);
+ assertEquals(buf.getShort(), 0x4500);
+ // Source IP address.
+ byte[] ip = new byte[4];
+ buf = ByteBuffer.wrap(packet, 12, 4);
+ buf.get(ip);
+ assertArrayEquals(ip, srcAddr.getAddress());
+ // Destination IP address.
+ buf = ByteBuffer.wrap(packet, 16, 4);
+ buf.get(ip);
+ assertArrayEquals(ip, dstAddr.getAddress());
+
+ buf = ByteBuffer.wrap(packet, 20, 12);
+ // Source port.
+ assertEquals(buf.getShort(), srcPort);
+ // Destination port.
+ assertEquals(buf.getShort(), dstPort);
+ // Sequence number.
+ assertEquals(buf.getInt(), seq);
+ // Ack.
+ assertEquals(buf.getInt(), ack);
+ // Window size.
+ buf = ByteBuffer.wrap(packet, 34, 2);
+ assertEquals(buf.getShort(), wnd >> wndScale);
+ }
+
+ //TODO: add ipv6 test when ipv6 supported
+
+ @Test
+ public void testParcel() throws Exception {
+ final InetAddress srcAddr = InetAddresses.parseNumericAddress("192.168.0.1");
+ final InetAddress dstAddr = InetAddresses.parseNumericAddress("192.168.0.10");
+ final int srcPort = 1234;
+ final int dstPort = 4321;
+ final int sequence = 0x11111111;
+ final int ack = 0x22222222;
+ final int wnd = 48_000;
+ final int wndScale = 2;
+ TcpKeepalivePacketData testData = null;
+ TcpKeepalivePacketDataParcelable resultData = null;
+ TcpSocketInfo testInfo = new TcpSocketInfo(
+ srcAddr, srcPort, dstAddr, dstPort, sequence, ack, wnd, wndScale);
+ testData = TcpKeepalivePacketData.tcpKeepalivePacket(testInfo);
+ resultData = testData.toStableParcelable();
+ assertArrayEquals(resultData.srcAddress, srcAddr.getAddress());
+ assertArrayEquals(resultData.dstAddress, dstAddr.getAddress());
+ assertEquals(resultData.srcPort, srcPort);
+ assertEquals(resultData.dstPort, dstPort);
+ assertEquals(resultData.seq, sequence);
+ assertEquals(resultData.ack, ack);
+ }
+}