diff options
| -rw-r--r-- | api/current.txt | 24 | ||||
| -rw-r--r-- | api/system-current.txt | 1 | ||||
| -rw-r--r-- | core/java/android/net/ConnectivityManager.java | 67 | ||||
| -rw-r--r-- | core/java/android/net/IConnectivityManager.aidl | 4 | ||||
| -rw-r--r-- | core/java/android/net/NattSocketKeepalive.java | 75 | ||||
| -rw-r--r-- | core/java/android/net/SocketKeepalive.java | 224 | ||||
| -rw-r--r-- | services/core/java/com/android/server/ConnectivityService.java | 12 | ||||
| -rw-r--r-- | services/core/java/com/android/server/connectivity/KeepaliveTracker.java | 109 | ||||
| -rw-r--r-- | tests/net/java/com/android/server/ConnectivityServiceTest.java | 222 |
9 files changed, 717 insertions, 21 deletions
diff --git a/api/current.txt b/api/current.txt index 24c9f0bfd684..ea4966b590bf 100644 --- a/api/current.txt +++ b/api/current.txt @@ -28481,6 +28481,7 @@ package android.net { public class ConnectivityManager { method public void addDefaultNetworkActiveListener(android.net.ConnectivityManager.OnNetworkActiveListener); method public boolean bindProcessToNetwork(@Nullable android.net.Network); + method public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull android.net.IpSecManager.UdpEncapsulationSocket, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback); method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) @Nullable public android.net.Network getActiveNetwork(); method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getActiveNetworkInfo(); method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo[] getAllNetworkInfo(); @@ -28985,6 +28986,29 @@ package android.net { ctor public SSLSessionCache(android.content.Context); } + public abstract class SocketKeepalive implements java.lang.AutoCloseable { + method public final void close(); + method public final void start(@IntRange(from=0xa, to=0xe10) int); + method public final void stop(); + field public static final int ERROR_HARDWARE_ERROR = -31; // 0xffffffe1 + field public static final int ERROR_HARDWARE_UNSUPPORTED = -30; // 0xffffffe2 + field public static final int ERROR_INVALID_INTERVAL = -24; // 0xffffffe8 + field public static final int ERROR_INVALID_IP_ADDRESS = -21; // 0xffffffeb + field public static final int ERROR_INVALID_LENGTH = -23; // 0xffffffe9 + field public static final int ERROR_INVALID_NETWORK = -20; // 0xffffffec + field public static final int ERROR_INVALID_PORT = -22; // 0xffffffea + field public static final int ERROR_INVALID_SOCKET = -25; // 0xffffffe7 + field public static final int ERROR_SOCKET_NOT_IDLE = -26; // 0xffffffe6 + } + + public static class SocketKeepalive.Callback { + ctor public SocketKeepalive.Callback(); + method public void onDataReceived(); + method public void onError(int); + method public void onStarted(); + method public void onStopped(); + } + public class TrafficStats { ctor public TrafficStats(); method public static void clearThreadStatsTag(); diff --git a/api/system-current.txt b/api/system-current.txt index 2e8f69f51d07..e9c48bde7d38 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -4022,6 +4022,7 @@ package android.net { } public class ConnectivityManager { + method @RequiresPermission("android.permission.PACKET_KEEPALIVE_OFFLOAD") public android.net.SocketKeepalive createNattKeepalive(@NonNull android.net.Network, @NonNull java.io.FileDescriptor, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback); method public boolean getAvoidBadWifi(); method @RequiresPermission(android.Manifest.permission.LOCAL_MAC_ADDRESS) public String getCaptivePortalServerUrl(); method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public boolean isTetheringSupported(); diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index c809ccad5907..5bb24bab6e48 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -15,6 +15,9 @@ */ package android.net; +import static android.net.IpSecManager.INVALID_RESOURCE_ID; + +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -28,6 +31,8 @@ import android.annotation.UnsupportedAppUsage; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.SocketKeepalive.Callback; import android.os.Binder; import android.os.Build; import android.os.Build.VERSION_CODES; @@ -58,6 +63,7 @@ import com.android.internal.util.Protocol; import libcore.net.event.NetworkEventDispatcher; +import java.io.FileDescriptor; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.InetAddress; @@ -66,6 +72,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.Executor; /** * Class that answers queries about the state of network connectivity. It also @@ -1699,6 +1706,8 @@ public class ConnectivityManager { * {@link PacketKeepaliveCallback#onStopped} if the operation was successful or * {@link PacketKeepaliveCallback#onError} if an error occurred. * + * @deprecated Use {@link SocketKeepalive} instead. + * * @hide */ public class PacketKeepalive { @@ -1802,6 +1811,8 @@ public class ConnectivityManager { /** * Starts an IPsec NAT-T keepalive packet with the specified parameters. * + * @deprecated Use {@link #createSocketKeepalive} instead. + * * @hide */ @UnsupportedAppUsage @@ -1821,6 +1832,62 @@ public class ConnectivityManager { } /** + * Request that keepalives be started on a IPsec NAT-T socket. + * + * @param network The {@link Network} the socket is on. + * @param socket The socket that needs to be kept alive. + * @param source The source address of the {@link UdpEncapsulationSocket}. + * @param destination The destination address of the {@link UdpEncapsulationSocket}. + * @param executor The executor on which callback will be invoked. The provided {@link Executor} + * must run callback sequentially, otherwise the order of callbacks cannot be + * guaranteed. + * @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. + **/ + public SocketKeepalive createSocketKeepalive(@NonNull Network network, + @NonNull UdpEncapsulationSocket socket, + @NonNull InetAddress source, + @NonNull InetAddress destination, + @NonNull @CallbackExecutor Executor executor, + @NonNull Callback callback) { + return new NattSocketKeepalive(mService, network, socket.getFileDescriptor(), + socket.getResourceId(), source, destination, executor, callback); + } + + /** + * Request that keepalives be started on a IPsec NAT-T socket file descriptor. Directly called + * by system apps which don't use IpSecService to create {@link UdpEncapsulationSocket}. + * + * @param network The {@link Network} the socket is on. + * @param fd The {@link FileDescriptor} that needs to be kept alive. The provided + * {@link FileDescriptor} must be bound to a port and the keepalives will be sent from + * that port. + * @param source The source address of the {@link UdpEncapsulationSocket}. + * @param destination The destination address of the {@link UdpEncapsulationSocket}. The + * keepalive packets will always be sent to port 4500 of the given {@code destination}. + * @param executor The executor on which callback will be invoked. The provided {@link Executor} + * must run callback sequentially, otherwise the order of callbacks cannot be + * guaranteed. + * @param callback A {@link SocketKeepalive.Callback}. Used for notifications about keepalive + * changes. Must be extended by applications that use this API. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) + public SocketKeepalive createNattKeepalive(@NonNull Network network, + @NonNull FileDescriptor fd, + @NonNull InetAddress source, + @NonNull InetAddress destination, + @NonNull @CallbackExecutor Executor executor, + @NonNull Callback callback) { + return new NattSocketKeepalive(mService, network, fd, INVALID_RESOURCE_ID /* Unused */, + source, destination, 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. diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 131925ec28e9..e97060a0a599 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -181,6 +181,10 @@ interface IConnectivityManager void startNattKeepalive(in Network network, int intervalSeconds, in Messenger messenger, in IBinder binder, String srcAddr, int srcPort, String dstAddr); + void startNattKeepaliveWithFd(in Network network, in FileDescriptor fd, int resourceId, + int intervalSeconds, in Messenger messenger, in IBinder binder, String srcAddr, + String dstAddr); + void stopKeepalive(in Network network, int slot); String getCaptivePortalServerUrl(); diff --git a/core/java/android/net/NattSocketKeepalive.java b/core/java/android/net/NattSocketKeepalive.java new file mode 100644 index 000000000000..88631aea3a88 --- /dev/null +++ b/core/java/android/net/NattSocketKeepalive.java @@ -0,0 +1,75 @@ +/* + * 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; + +import android.annotation.NonNull; +import android.os.Binder; +import android.os.RemoteException; +import android.util.Log; + +import java.io.FileDescriptor; +import java.net.InetAddress; +import java.util.concurrent.Executor; + +/** @hide */ +public final class NattSocketKeepalive extends SocketKeepalive { + /** The NAT-T destination port for IPsec */ + public static final int NATT_PORT = 4500; + + @NonNull private final InetAddress mSource; + @NonNull private final InetAddress mDestination; + @NonNull private final FileDescriptor mFd; + private final int mResourceId; + + NattSocketKeepalive(@NonNull IConnectivityManager service, + @NonNull Network network, + @NonNull FileDescriptor fd, + int resourceId, + @NonNull InetAddress source, + @NonNull InetAddress destination, + @NonNull Executor executor, + @NonNull Callback callback) { + super(service, network, executor, callback); + mSource = source; + mDestination = destination; + mFd = fd; + mResourceId = resourceId; + } + + @Override + void startImpl(int intervalSec) { + try { + mService.startNattKeepaliveWithFd(mNetwork, mFd, mResourceId, intervalSec, mMessenger, + new Binder(), mSource.getHostAddress(), mDestination.getHostAddress()); + } 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/java/android/net/SocketKeepalive.java b/core/java/android/net/SocketKeepalive.java new file mode 100644 index 000000000000..97d50f4bac05 --- /dev/null +++ b/core/java/android/net/SocketKeepalive.java @@ -0,0 +1,224 @@ +/* + * 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.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.Process; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.Executor; + +/** + * Allows applications to request that the system periodically send specific packets on their + * behalf, using hardware offload to save battery power. + * + * To request that the system send keepalives, call one of the methods that return a + * {@link SocketKeepalive} object, such as {@link ConnectivityManager#createSocketKeepalive}, + * passing in a non-null callback. If the {@link SocketKeepalive} is successfully + * started, the callback's {@code onStarted} method will be called. If an error occurs, + * {@code onError} will be called, specifying one of the {@code ERROR_*} constants in this + * class. + * + * To stop an existing keepalive, call {@link SocketKeepalive#stop}. The system will call + * {@link SocketKeepalive.Callback#onStopped} if the operation was successful or + * {@link SocketKeepalive.Callback#onError} if an error occurred. + */ +public abstract class SocketKeepalive implements AutoCloseable { + static final String TAG = "SocketKeepalive"; + + /** @hide */ + public static final int SUCCESS = 0; + + /** @hide */ + public static final int NO_KEEPALIVE = -1; + + /** @hide */ + public static final int DATA_RECEIVED = -2; + + /** @hide */ + public static final int BINDER_DIED = -10; + + /** The specified {@code Network} is not connected. */ + public static final int ERROR_INVALID_NETWORK = -20; + /** The specified IP addresses are invalid. For example, the specified source IP address is + * not configured on the specified {@code Network}. */ + public static final int ERROR_INVALID_IP_ADDRESS = -21; + /** The requested port is invalid. */ + public static final int ERROR_INVALID_PORT = -22; + /** The packet length is invalid (e.g., too long). */ + public static final int ERROR_INVALID_LENGTH = -23; + /** The packet transmission interval is invalid (e.g., too short). */ + public static final int ERROR_INVALID_INTERVAL = -24; + /** The target socket is invalid. */ + public static final int ERROR_INVALID_SOCKET = -25; + /** The target socket is not idle. */ + public static final int ERROR_SOCKET_NOT_IDLE = -26; + + /** The hardware does not support this request. */ + public static final int ERROR_HARDWARE_UNSUPPORTED = -30; + /** The hardware returned an error. */ + public static final int ERROR_HARDWARE_ERROR = -31; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "ERROR_" }, value = { + ERROR_INVALID_NETWORK, + ERROR_INVALID_IP_ADDRESS, + ERROR_INVALID_PORT, + ERROR_INVALID_LENGTH, + ERROR_INVALID_INTERVAL, + ERROR_INVALID_SOCKET, + ERROR_SOCKET_NOT_IDLE + }) + public @interface ErrorCode {} + + /** + * The minimum interval in seconds between keepalive packet transmissions. + * + * @hide + **/ + public static final int MIN_INTERVAL_SEC = 10; + + /** + * The maximum interval in seconds between keepalive packet transmissions. + * + * @hide + **/ + public static final int MAX_INTERVAL_SEC = 3600; + + @NonNull final IConnectivityManager mService; + @NonNull final Network mNetwork; + @NonNull private final Executor mExecutor; + @NonNull private final SocketKeepalive.Callback mCallback; + @NonNull private final Looper mLooper; + @NonNull final Messenger mMessenger; + @NonNull Integer mSlot; + + SocketKeepalive(@NonNull IConnectivityManager service, @NonNull Network network, + @NonNull Executor executor, @NonNull Callback callback) { + mService = service; + mNetwork = network; + mExecutor = executor; + mCallback = callback; + // TODO: 1. Use other thread modeling instead of create one thread for every instance to + // reduce the memory cost. + // 2. support restart. + // 3. Fix race condition which caused by rapidly start and stop. + HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND + + Process.THREAD_PRIORITY_LESS_FAVORABLE); + thread.start(); + mLooper = thread.getLooper(); + mMessenger = new Messenger(new Handler(mLooper) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case NetworkAgent.EVENT_PACKET_KEEPALIVE: + final int status = message.arg2; + try { + if (status == SUCCESS) { + if (mSlot == null) { + mSlot = message.arg1; + mExecutor.execute(() -> mCallback.onStarted()); + } else { + mSlot = null; + stopLooper(); + mExecutor.execute(() -> mCallback.onStopped()); + } + } else if (status == DATA_RECEIVED) { + stopLooper(); + mExecutor.execute(() -> mCallback.onDataReceived()); + } else { + stopLooper(); + mExecutor.execute(() -> mCallback.onError(status)); + } + } catch (Exception e) { + Log.e(TAG, "Exception in keepalive callback(" + status + ")", e); + } + break; + default: + Log.e(TAG, "Unhandled message " + Integer.toHexString(message.what)); + break; + } + } + }); + } + + /** + * Request that keepalive be started with the given {@code intervalSec}. See + * {@link SocketKeepalive}. + * + * @param intervalSec The target interval in seconds between keepalive packet transmissions. + * The interval should be between 10 seconds and 3600 seconds, otherwise + * {@link #ERROR_INVALID_INTERVAL} will be returned. + */ + public final void start(@IntRange(from = MIN_INTERVAL_SEC, to = MAX_INTERVAL_SEC) + int intervalSec) { + startImpl(intervalSec); + } + + abstract void startImpl(int intervalSec); + + /** @hide */ + protected void stopLooper() { + // TODO: remove this after changing thread modeling. + mLooper.quit(); + } + + /** + * Requests that keepalive be stopped. The application must wait for {@link Callback#onStopped} + * before using the object. See {@link SocketKeepalive}. + */ + public final void stop() { + stopImpl(); + } + + abstract void stopImpl(); + + /** + * Deactivate this {@link SocketKeepalive} and free allocated resources. The instance won't be + * usable again if {@code close()} is called. + */ + @Override + public final void close() { + stop(); + stopLooper(); + } + + /** + * The callback which app can use to learn the status changes of {@link SocketKeepalive}. See + * {@link SocketKeepalive}. + */ + public static class Callback { + /** The requested keepalive was successfully started. */ + public void onStarted() {} + /** The keepalive was successfully stopped. */ + public void onStopped() {} + /** An error occurred. */ + public void onError(@ErrorCode int error) {} + /** The keepalive on a TCP socket was stopped because the socket received data. */ + public void onDataReceived() {} + } +} diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index aa74600cc527..fda7279c45d1 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -73,6 +73,7 @@ import android.net.INetworkStatsService; import android.net.LinkProperties; import android.net.LinkProperties.CompareResult; import android.net.MatchAllNetworkSpecifier; +import android.net.NattSocketKeepalive; import android.net.Network; import android.net.NetworkAgent; import android.net.NetworkCapabilities; @@ -6185,6 +6186,17 @@ public class ConnectivityService extends IConnectivityManager.Stub } @Override + public void startNattKeepaliveWithFd(Network network, FileDescriptor fd, int resourceId, + int intervalSeconds, Messenger messenger, IBinder binder, String srcAddr, + String dstAddr) { + enforceKeepalivePermission(); + mKeepaliveTracker.startNattKeepalive( + getNetworkAgentInfoForNetwork(network), fd, resourceId, + intervalSeconds, messenger, binder, + srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT); + } + + @Override public void stopKeepalive(Network network, int slot) { mHandler.sendMessage(mHandler.obtainMessage( NetworkAgent.CMD_STOP_PACKET_KEEPALIVE, slot, PacketKeepalive.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 8a3cdcad0230..1559ba8ba883 100644 --- a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java +++ b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java @@ -16,40 +16,49 @@ package com.android.server.connectivity; -import com.android.internal.util.HexDump; -import com.android.internal.util.IndentingPrintWriter; -import com.android.server.connectivity.NetworkAgentInfo; -import android.net.ConnectivityManager; +// TODO: Clean up imports and remove references of PacketKeepalive constants. + +import static android.net.ConnectivityManager.PacketKeepalive.ERROR_INVALID_INTERVAL; +import static android.net.ConnectivityManager.PacketKeepalive.ERROR_INVALID_IP_ADDRESS; +import static android.net.ConnectivityManager.PacketKeepalive.ERROR_INVALID_NETWORK; +import static android.net.ConnectivityManager.PacketKeepalive.MIN_INTERVAL; +import static android.net.ConnectivityManager.PacketKeepalive.NATT_PORT; +import static android.net.ConnectivityManager.PacketKeepalive.NO_KEEPALIVE; +import static android.net.ConnectivityManager.PacketKeepalive.SUCCESS; +import static android.net.NetworkAgent.CMD_START_PACKET_KEEPALIVE; +import static android.net.NetworkAgent.CMD_STOP_PACKET_KEEPALIVE; +import static android.net.NetworkAgent.EVENT_PACKET_KEEPALIVE; +import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET; + +import android.annotation.NonNull; +import android.annotation.Nullable; import android.net.ConnectivityManager.PacketKeepalive; import android.net.KeepalivePacketData; -import android.net.LinkAddress; import android.net.NetworkAgent; import android.net.NetworkUtils; import android.net.util.IpUtils; import android.os.Binder; -import android.os.IBinder; import android.os.Handler; +import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.Process; import android.os.RemoteException; -import android.system.OsConstants; +import android.system.ErrnoException; +import android.system.Os; import android.util.Log; import android.util.Pair; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.net.Inet4Address; -import java.net.Inet6Address; +import com.android.internal.util.HexDump; +import com.android.internal.util.IndentingPrintWriter; + +import java.io.FileDescriptor; import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.util.ArrayList; import java.util.HashMap; -import static android.net.ConnectivityManager.PacketKeepalive.*; -import static android.net.NetworkAgent.CMD_START_PACKET_KEEPALIVE; -import static android.net.NetworkAgent.CMD_STOP_PACKET_KEEPALIVE; -import static android.net.NetworkAgent.EVENT_PACKET_KEEPALIVE; - /** * Manages packet keepalive requests. * @@ -300,7 +309,9 @@ public class KeepaliveTracker { } } - public void handleEventPacketKeepalive(NetworkAgentInfo nai, Message message) { + /** Handle keepalive events from lower layer. */ + public void handleEventPacketKeepalive(@NonNull NetworkAgentInfo nai, + @NonNull Message message) { int slot = message.arg1; int reason = message.arg2; @@ -330,8 +341,18 @@ public class KeepaliveTracker { } } - public void startNattKeepalive(NetworkAgentInfo nai, int intervalSeconds, Messenger messenger, - IBinder binder, String srcAddrString, int srcPort, String dstAddrString, int dstPort) { + /** + * Called when requesting that keepalives be started on a IPsec NAT-T socket. See + * {@link android.net.SocketKeepalive}. + **/ + public void startNattKeepalive(@Nullable NetworkAgentInfo nai, + int intervalSeconds, + @NonNull Messenger messenger, + @NonNull IBinder binder, + @NonNull String srcAddrString, + int srcPort, + @NonNull String dstAddrString, + int dstPort) { if (nai == null) { notifyMessenger(messenger, NO_KEEPALIVE, ERROR_INVALID_NETWORK); return; @@ -360,6 +381,56 @@ public class KeepaliveTracker { NetworkAgent.CMD_START_PACKET_KEEPALIVE, ki).sendToTarget(); } + /** + * Called when requesting that keepalives be started on a IPsec NAT-T socket. This function is + * identical to {@link #startNattKeepalive}, but also takes a {@code resourceId}, which is the + * resource index bound to the {@link UdpEncapsulationSocket} when creating by + * {@link com.android.server.IpSecService} to verify whether the given + * {@link UdpEncapsulationSocket} is legitimate. + **/ + public void startNattKeepalive(@Nullable NetworkAgentInfo nai, + @Nullable FileDescriptor fd, + int resourceId, + int intervalSeconds, + @NonNull Messenger messenger, + @NonNull IBinder binder, + @NonNull String srcAddrString, + @NonNull String dstAddrString, + int dstPort) { + // Ensure that the socket is created by IpSecService. + if (!isNattKeepaliveSocketValid(fd, resourceId)) { + notifyMessenger(messenger, NO_KEEPALIVE, ERROR_INVALID_SOCKET); + } + + // Get src port to adopt old API. + int srcPort = 0; + try { + final SocketAddress srcSockAddr = Os.getsockname(fd); + srcPort = ((InetSocketAddress) srcSockAddr).getPort(); + } catch (ErrnoException e) { + notifyMessenger(messenger, NO_KEEPALIVE, ERROR_INVALID_SOCKET); + } + + // Forward request to old API. + startNattKeepalive(nai, intervalSeconds, messenger, binder, srcAddrString, srcPort, + dstAddrString, dstPort); + } + + /** + * Verify if the IPsec NAT-T file descriptor and resource Id hold for IPsec keepalive is valid. + **/ + public static boolean isNattKeepaliveSocketValid(@Nullable FileDescriptor fd, int resourceId) { + // TODO: 1. confirm whether the fd is called from system api or created by IpSecService. + // 2. If the fd is created from the system api, check that it's bounded. And + // call dup to keep the fd open. + // 3. If the fd is created from IpSecService, check if the resource ID is valid. And + // hold the resource needed in IpSecService. + if (null == fd) { + return false; + } + return true; + } + public void dump(IndentingPrintWriter pw) { pw.println("Packet keepalives:"); pw.increaseIndent(); diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java index dda44819e664..b5d5f61b527e 100644 --- a/tests/net/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java @@ -107,6 +107,8 @@ import android.net.INetworkPolicyManager; import android.net.INetworkStatsService; import android.net.InterfaceConfiguration; import android.net.IpPrefix; +import android.net.IpSecManager; +import android.net.IpSecManager.UdpEncapsulationSocket; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.MatchAllNetworkSpecifier; @@ -122,6 +124,7 @@ import android.net.NetworkSpecifier; import android.net.NetworkStack; import android.net.NetworkUtils; import android.net.RouteInfo; +import android.net.SocketKeepalive; import android.net.UidRange; import android.net.metrics.IpConnectivityLog; import android.net.shared.NetworkMonitorUtils; @@ -186,6 +189,8 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -402,8 +407,8 @@ public class ConnectivityServiceTest { private final ConditionVariable mPreventReconnectReceived = new ConditionVariable(); private int mScore; private NetworkAgent mNetworkAgent; - private int mStartKeepaliveError = PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED; - private int mStopKeepaliveError = PacketKeepalive.NO_KEEPALIVE; + private int mStartKeepaliveError = SocketKeepalive.ERROR_HARDWARE_UNSUPPORTED; + private int mStopKeepaliveError = SocketKeepalive.NO_KEEPALIVE; private Integer mExpectedKeepaliveSlot = null; // Contains the redirectUrl from networkStatus(). Before reading, wait for // mNetworkStatusReceived. @@ -3548,6 +3553,80 @@ public class ConnectivityServiceTest { } } + private static class TestSocketKeepaliveCallback extends SocketKeepalive.Callback { + + public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR }; + + private class CallbackValue { + public CallbackType callbackType; + public int error; + + CallbackValue(CallbackType type) { + this.callbackType = type; + this.error = SocketKeepalive.SUCCESS; + assertTrue("onError callback must have error", type != CallbackType.ON_ERROR); + } + + CallbackValue(CallbackType type, int error) { + this.callbackType = type; + this.error = error; + assertEquals("error can only be set for onError", type, CallbackType.ON_ERROR); + } + + @Override + public boolean equals(Object o) { + return o instanceof CallbackValue + && this.callbackType == ((CallbackValue) o).callbackType + && this.error == ((CallbackValue) o).error; + } + + @Override + public String toString() { + return String.format("%s(%s, %d)", getClass().getSimpleName(), callbackType, + error); + } + } + + private LinkedBlockingQueue<CallbackValue> mCallbacks = new LinkedBlockingQueue<>(); + + @Override + public void onStarted() { + mCallbacks.add(new CallbackValue(CallbackType.ON_STARTED)); + } + + @Override + public void onStopped() { + mCallbacks.add(new CallbackValue(CallbackType.ON_STOPPED)); + } + + @Override + public void onError(int error) { + mCallbacks.add(new CallbackValue(CallbackType.ON_ERROR, error)); + } + + private void expectCallback(CallbackValue callbackValue) { + try { + assertEquals( + callbackValue, + mCallbacks.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } catch (InterruptedException e) { + fail(callbackValue.callbackType + " callback not seen after " + TIMEOUT_MS + " ms"); + } + } + + public void expectStarted() { + expectCallback(new CallbackValue(CallbackType.ON_STARTED)); + } + + public void expectStopped() { + expectCallback(new CallbackValue(CallbackType.ON_STOPPED)); + } + + public void expectError(int error) { + expectCallback(new CallbackValue(CallbackType.ON_ERROR, error)); + } + } + private Network connectKeepaliveNetwork(LinkProperties lp) { // Ensure the network is disconnected before we do anything. if (mWiFiNetworkAgent != null) { @@ -3695,6 +3774,145 @@ public class ConnectivityServiceTest { } @Test + public void testNattSocketKeepalives() throws Exception { + // TODO: 1. Move this outside of ConnectivityServiceTest. + // 2. Add helper function to test against newSingleThreadExecutor as well as inline + // executor. + // 3. Make test to verify that Nat-T keepalive socket is created by IpSecService. + final int srcPort = 12345; + final InetAddress myIPv4 = InetAddress.getByName("192.0.2.129"); + final InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35"); + final InetAddress myIPv6 = InetAddress.getByName("2001:db8::1"); + final InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8"); + final InetAddress dstIPv6 = InetAddress.getByName("2001:4860:4860::8888"); + + final int validKaInterval = 15; + final int invalidKaInterval = 9; + + final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE); + final UdpEncapsulationSocket testSocket = mIpSec.openUdpEncapsulationSocket(srcPort); + + final Executor executor = Executors.newSingleThreadExecutor(); + + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("wlan12"); + lp.addLinkAddress(new LinkAddress(myIPv6, 64)); + lp.addLinkAddress(new LinkAddress(myIPv4, 25)); + lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234"))); + lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254"))); + + Network notMyNet = new Network(61234); + Network myNet = connectKeepaliveNetwork(lp); + + TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(); + SocketKeepalive ka; + + // Attempt to start keepalives with invalid parameters and check for errors. + // Invalid network. + ka = mCm.createSocketKeepalive(notMyNet, testSocket, myIPv4, dstIPv4, executor, callback); + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK); + + // Invalid interval. + ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback); + ka.start(invalidKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_INTERVAL); + + // Invalid destination. + ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv6, executor, callback); + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + + // Invalid source; + ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv6, dstIPv4, executor, callback); + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + + // NAT-T is only supported for IPv4. + ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv6, dstIPv6, executor, callback); + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + + // Sanity check before testing started keepalive. + ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback); + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_HARDWARE_UNSUPPORTED); + + // Check that a started keepalive can be stopped. + mWiFiNetworkAgent.setStartKeepaliveError(SocketKeepalive.SUCCESS); + ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback); + ka.start(validKaInterval); + callback.expectStarted(); + mWiFiNetworkAgent.setStopKeepaliveError(SocketKeepalive.SUCCESS); + ka.stop(); + callback.expectStopped(); + + // Check that deleting the IP address stops the keepalive. + LinkProperties bogusLp = new LinkProperties(lp); + ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback); + ka.start(validKaInterval); + callback.expectStarted(); + bogusLp.removeLinkAddress(new LinkAddress(myIPv4, 25)); + bogusLp.addLinkAddress(new LinkAddress(notMyIPv4, 25)); + mWiFiNetworkAgent.sendLinkProperties(bogusLp); + callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + mWiFiNetworkAgent.sendLinkProperties(lp); + + // Check that a started keepalive is stopped correctly when the network disconnects. + ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback); + ka.start(validKaInterval); + callback.expectStarted(); + mWiFiNetworkAgent.disconnect(); + waitFor(mWiFiNetworkAgent.getDisconnectedCV()); + callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK); + + // ... and that stopping it after that has no adverse effects. + waitForIdle(); + final Network myNetAlias = myNet; + assertNull(mCm.getNetworkCapabilities(myNetAlias)); + ka.stop(); + + // Reconnect. + myNet = connectKeepaliveNetwork(lp); + mWiFiNetworkAgent.setStartKeepaliveError(SocketKeepalive.SUCCESS); + + // Check things work as expected when the keepalive is stopped and the network disconnects. + ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback); + ka.start(validKaInterval); + callback.expectStarted(); + ka.stop(); + mWiFiNetworkAgent.disconnect(); + waitFor(mWiFiNetworkAgent.getDisconnectedCV()); + waitForIdle(); + callback.expectStopped(); + + // Reconnect. + myNet = connectKeepaliveNetwork(lp); + mWiFiNetworkAgent.setStartKeepaliveError(SocketKeepalive.SUCCESS); + + // Check that keepalive slots start from 1 and increment. The first one gets slot 1. + mWiFiNetworkAgent.setExpectedKeepaliveSlot(1); + ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback); + ka.start(validKaInterval); + callback.expectStarted(); + + // The second one gets slot 2. + mWiFiNetworkAgent.setExpectedKeepaliveSlot(2); + final UdpEncapsulationSocket testSocket2 = mIpSec.openUdpEncapsulationSocket(6789); + TestSocketKeepaliveCallback callback2 = new TestSocketKeepaliveCallback(); + SocketKeepalive ka2 = + mCm.createSocketKeepalive(myNet, testSocket2, myIPv4, dstIPv4, executor, callback2); + ka2.start(validKaInterval); + callback2.expectStarted(); + + ka.stop(); + callback.expectStopped(); + + ka2.stop(); + callback2.expectStopped(); + } + + @Test public void testGetCaptivePortalServerUrl() throws Exception { String url = mCm.getCaptivePortalServerUrl(); assertEquals("http://connectivitycheck.gstatic.com/generate_204", url); |