diff options
-rw-r--r-- | core/java/android/net/ConnectivityManager.java | 136 | ||||
-rw-r--r-- | core/java/android/net/IConnectivityManager.aidl | 5 | ||||
-rw-r--r-- | core/java/android/net/NetworkAgent.java | 90 | ||||
-rw-r--r-- | services/core/Android.mk | 2 | ||||
-rw-r--r-- | services/core/java/com/android/server/ConnectivityService.java | 57 | ||||
-rw-r--r-- | services/core/java/com/android/server/connectivity/KeepalivePacketData.java | 136 | ||||
-rw-r--r-- | services/core/java/com/android/server/connectivity/KeepaliveTracker.java | 372 | ||||
-rw-r--r-- | services/net/java/android/net/util/IpUtils.java | 151 | ||||
-rw-r--r-- | services/tests/servicestests/src/android/net/IpUtilsTest.java | 162 |
9 files changed, 1104 insertions, 7 deletions
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index dc8ff8f..86a50c6 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -1206,6 +1206,142 @@ public class ConnectivityManager { return true; } + /** @hide */ + public static class PacketKeepaliveCallback { + /** The requested keepalive was successfully started. */ + public void onStarted() {} + /** The keepalive was successfully stopped. */ + public void onStopped() {} + /** An error occurred. */ + public void onError(int error) {} + } + + /** + * 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 ConnectivityManager.PacketKeepalive} object, such as {@link #startNattKeepalive}, + * passing in a non-null callback. If the callback 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 stop}. The system will call {@code onStopped} if + * the operation was successfull or {@code onError} if an error occurred. + * + * @hide + */ + public class PacketKeepalive { + + private static final String TAG = "PacketKeepalive"; + + /** @hide */ + public static final int SUCCESS = 0; + + /** @hide */ + public static final int NO_KEEPALIVE = -1; + + /** @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 hardware does not support this request. */ + public static final int ERROR_HARDWARE_UNSUPPORTED = -30; + + public static final int NATT_PORT = 4500; + + private final Network mNetwork; + private final PacketKeepaliveCallback mCallback; + private final Looper mLooper; + private final Messenger mMessenger; + + private volatile Integer mSlot; + + void stopLooper() { + mLooper.quit(); + } + + public void stop() { + try { + mService.stopKeepalive(mNetwork, mSlot); + } catch (RemoteException e) { + Log.e(TAG, "Error stopping packet keepalive: ", e); + stopLooper(); + } + } + + private PacketKeepalive(Network network, PacketKeepaliveCallback callback) { + checkNotNull(network, "network cannot be null"); + checkNotNull(callback, "callback cannot be null"); + mNetwork = network; + mCallback = callback; + HandlerThread thread = new HandlerThread(TAG); + 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: + int error = message.arg2; + try { + if (error == SUCCESS) { + if (mSlot == null) { + mSlot = message.arg1; + mCallback.onStarted(); + } else { + mSlot = null; + stopLooper(); + mCallback.onStopped(); + } + } else { + stopLooper(); + mCallback.onError(error); + } + } catch (Exception e) { + Log.e(TAG, "Exception in keepalive callback(" + error + ")", e); + } + break; + default: + Log.e(TAG, "Unhandled message " + Integer.toHexString(message.what)); + break; + } + } + }); + } + } + + /** + * Starts an IPsec NAT-T keepalive packet with the specified parameters. + * + * @hide + */ + public PacketKeepalive startNattKeepalive( + Network network, int intervalSeconds, PacketKeepaliveCallback callback, + InetAddress srcAddr, int srcPort, InetAddress dstAddr) { + final PacketKeepalive k = new PacketKeepalive(network, callback); + try { + mService.startNattKeepalive(network, intervalSeconds, k.mMessenger, new Binder(), + srcAddr.getHostAddress(), srcPort, dstAddr.getHostAddress()); + } catch (RemoteException e) { + Log.e(TAG, "Error starting packet keepalive: ", e); + k.stopLooper(); + return null; + } + return k; + } + /** * 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 diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 46c28a6..d4dd669 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -160,4 +160,9 @@ interface IConnectivityManager boolean setUnderlyingNetworksForVpn(in Network[] networks); void factoryReset(); + + void startNattKeepalive(in Network network, int intervalSeconds, in Messenger messenger, + in IBinder binder, String srcAddr, int srcPort, String dstAddr); + + void stopKeepalive(in Network network, int slot); } diff --git a/core/java/android/net/NetworkAgent.java b/core/java/android/net/NetworkAgent.java index e6fc1ea..85a584a 100644 --- a/core/java/android/net/NetworkAgent.java +++ b/core/java/android/net/NetworkAgent.java @@ -25,6 +25,7 @@ import android.util.Log; import com.android.internal.util.AsyncChannel; import com.android.internal.util.Protocol; +import android.net.ConnectivityManager.PacketKeepalive; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; @@ -143,11 +144,46 @@ public abstract class NetworkAgent extends Handler { */ public static final int CMD_SAVE_ACCEPT_UNVALIDATED = BASE + 9; - /** Sent by ConnectivityService to the NetworkAgent to inform the agent to pull + /** + * Sent by ConnectivityService to the NetworkAgent to inform the agent to pull * the underlying network connection for updated bandwidth information. */ public static final int CMD_REQUEST_BANDWIDTH_UPDATE = BASE + 10; + /** + * Sent by ConnectivityService to the NetworkAgent to request that the specified packet be sent + * periodically on the given interval. + * + * arg1 = the slot number of the keepalive to start + * arg2 = interval in seconds + * obj = KeepalivePacketData object describing the data to be sent + * + * Also used internally by ConnectivityService / KeepaliveTracker, with different semantics. + */ + public static final int CMD_START_PACKET_KEEPALIVE = BASE + 11; + + /** + * Requests that the specified keepalive packet be stopped. + * + * arg1 = slot number of the keepalive to stop. + * + * Also used internally by ConnectivityService / KeepaliveTracker, with different semantics. + */ + public static final int CMD_STOP_PACKET_KEEPALIVE = BASE + 12; + + /** + * Sent by the NetworkAgent to ConnectivityService to provide status on a packet keepalive + * request. This may either be the reply to a CMD_START_PACKET_KEEPALIVE, or an asynchronous + * error notification. + * + * This is also sent by KeepaliveTracker to the app's ConnectivityManager.PacketKeepalive to + * so that the app's PacketKeepaliveCallback methods can be called. + * + * arg1 = slot number of the keepalive + * arg2 = error code + */ + public static final int EVENT_PACKET_KEEPALIVE = BASE + 13; + public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni, NetworkCapabilities nc, LinkProperties lp, int score) { this(looper, context, logTag, ni, nc, lp, score, null); @@ -240,18 +276,41 @@ public abstract class NetworkAgent extends Handler { } case CMD_SAVE_ACCEPT_UNVALIDATED: { saveAcceptUnvalidated(msg.arg1 != 0); + break; + } + case CMD_START_PACKET_KEEPALIVE: { + startPacketKeepalive(msg); + break; + } + case CMD_STOP_PACKET_KEEPALIVE: { + stopPacketKeepalive(msg); + break; } } } private void queueOrSendMessage(int what, Object obj) { + queueOrSendMessage(what, 0, 0, obj); + } + + private void queueOrSendMessage(int what, int arg1, int arg2) { + queueOrSendMessage(what, arg1, arg2, null); + } + + private void queueOrSendMessage(int what, int arg1, int arg2, Object obj) { + Message msg = Message.obtain(); + msg.what = what; + msg.arg1 = arg1; + msg.arg2 = arg2; + msg.obj = obj; + queueOrSendMessage(msg); + } + + private void queueOrSendMessage(Message msg) { synchronized (mPreConnectedQueue) { if (mAsyncChannel != null) { - mAsyncChannel.sendMessage(what, obj); + mAsyncChannel.sendMessage(msg); } else { - Message msg = Message.obtain(); - msg.what = what; - msg.obj = obj; mPreConnectedQueue.add(msg); } } @@ -365,6 +424,27 @@ public abstract class NetworkAgent extends Handler { protected void saveAcceptUnvalidated(boolean accept) { } + /** + * Requests that the network hardware send the specified packet at the specified interval. + */ + protected void startPacketKeepalive(Message msg) { + onPacketKeepaliveEvent(msg.arg1, PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED); + } + + /** + * Requests that the network hardware send the specified packet at the specified interval. + */ + protected void stopPacketKeepalive(Message msg) { + onPacketKeepaliveEvent(msg.arg1, PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED); + } + + /** + * Called by the network when a packet keepalive event occurs. + */ + public void onPacketKeepaliveEvent(int slot, int reason) { + queueOrSendMessage(EVENT_PACKET_KEEPALIVE, slot, reason); + } + protected void log(String s) { Log.d(LOG_TAG, "NetworkAgent: " + s); } diff --git a/services/core/Android.mk b/services/core/Android.mk index 64b6134..666f2ff 100644 --- a/services/core/Android.mk +++ b/services/core/Android.mk @@ -9,7 +9,7 @@ LOCAL_SRC_FILES += \ java/com/android/server/EventLogTags.logtags \ java/com/android/server/am/EventLogTags.logtags -LOCAL_JAVA_LIBRARIES := telephony-common +LOCAL_JAVA_LIBRARIES := services.net telephony-common LOCAL_STATIC_JAVA_LIBRARIES := tzdata_update include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 9c767a5..08754c2 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -47,6 +47,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; import android.net.ConnectivityManager; +import android.net.ConnectivityManager.PacketKeepalive; import android.net.IConnectivityManager; import android.net.INetworkManagementEventObserver; import android.net.INetworkPolicyListener; @@ -117,6 +118,7 @@ import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.XmlUtils; import com.android.server.am.BatteryStatsService; import com.android.server.connectivity.DataConnectionStats; +import com.android.server.connectivity.KeepaliveTracker; import com.android.server.connectivity.NetworkDiagnostics; import com.android.server.connectivity.Nat464Xlat; import com.android.server.connectivity.NetworkAgentInfo; @@ -399,6 +401,8 @@ public class ConnectivityService extends IConnectivityManager.Stub TelephonyManager mTelephonyManager; + private KeepaliveTracker mKeepaliveTracker; + // sequence number for Networks; keep in sync with system/netd/NetworkController.cpp private final static int MIN_NET_ID = 100; // some reserved marks private final static int MAX_NET_ID = 65535; @@ -764,6 +768,8 @@ public class ConnectivityService extends IConnectivityManager.Stub mPacManager = new PacManager(mContext, mHandler, EVENT_PROXY_HAS_CHANGED); mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + + mKeepaliveTracker = new KeepaliveTracker(mHandler); } private NetworkRequest createInternetRequestForTransport(int transportType) { @@ -1449,6 +1455,10 @@ public class ConnectivityService extends IConnectivityManager.Stub "ConnectivityService"); } + private void enforceKeepalivePermission() { + mContext.enforceCallingPermission(KeepaliveTracker.PERMISSION, "ConnectivityService"); + } + public void sendConnectedBroadcast(NetworkInfo info) { enforceConnectivityInternalPermission(); sendGeneralBroadcast(info, CONNECTIVITY_ACTION); @@ -1840,10 +1850,13 @@ public class ConnectivityService extends IConnectivityManager.Stub pw.println(", last requested never"); } } - pw.println(); + pw.println(); mTethering.dump(fd, pw, args); + pw.println(); + mKeepaliveTracker.dump(pw); + if (mInetLog != null && mInetLog.size() > 0) { pw.println(); pw.println("Inet condition reports:"); @@ -2010,6 +2023,15 @@ public class ConnectivityService extends IConnectivityManager.Stub nai.networkMisc.acceptUnvalidated = (boolean) msg.obj; break; } + case NetworkAgent.EVENT_PACKET_KEEPALIVE: { + NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo); + if (nai == null) { + loge("EVENT_PACKET_KEEPALIVE from unknown NetworkAgent"); + break; + } + mKeepaliveTracker.handleEventPacketKeepalive(nai, msg); + break; + } case NetworkMonitor.EVENT_NETWORK_TESTED: { NetworkAgentInfo nai = (NetworkAgentInfo)msg.obj; if (isLiveNetworkAgent(nai, "EVENT_NETWORK_TESTED")) { @@ -2148,6 +2170,8 @@ public class ConnectivityService extends IConnectivityManager.Stub } notifyIfacesChanged(); notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOST); + mKeepaliveTracker.handleStopAllKeepalives(nai, + ConnectivityManager.PacketKeepalive.ERROR_INVALID_NETWORK); nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_DISCONNECTED); mNetworkAgentInfos.remove(msg.replyTo); updateClat(null, nai.linkProperties, nai); @@ -2509,6 +2533,19 @@ public class ConnectivityService extends IConnectivityManager.Stub handleMobileDataAlwaysOn(); break; } + // Sent by KeepaliveTracker to process an app request on the state machine thread. + case NetworkAgent.CMD_START_PACKET_KEEPALIVE: { + mKeepaliveTracker.handleStartKeepalive(msg); + break; + } + // Sent by KeepaliveTracker to process an app request on the state machine thread. + case NetworkAgent.CMD_STOP_PACKET_KEEPALIVE: { + NetworkAgentInfo nai = getNetworkAgentInfoForNetwork((Network) msg.obj); + int slot = msg.arg1; + int reason = msg.arg2; + mKeepaliveTracker.handleStopKeepalive(nai, slot, reason); + break; + } case EVENT_SYSTEM_READY: { for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) { nai.networkMonitor.systemReady = true; @@ -3863,6 +3900,8 @@ public class ConnectivityService extends IConnectivityManager.Stub notifyIfacesChanged(); notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_IP_CHANGED); } + + mKeepaliveTracker.handleCheckKeepalivesStillValid(networkAgent); } private void updateClat(LinkProperties newLp, LinkProperties oldLp, NetworkAgentInfo nai) { @@ -4663,6 +4702,22 @@ public class ConnectivityService extends IConnectivityManager.Stub } @Override + public void startNattKeepalive(Network network, int intervalSeconds, Messenger messenger, + IBinder binder, String srcAddr, int srcPort, String dstAddr) { + enforceKeepalivePermission(); + mKeepaliveTracker.startNattKeepalive( + getNetworkAgentInfoForNetwork(network), + intervalSeconds, messenger, binder, + srcAddr, srcPort, dstAddr, ConnectivityManager.PacketKeepalive.NATT_PORT); + } + + @Override + public void stopKeepalive(Network network, int slot) { + mHandler.sendMessage(mHandler.obtainMessage( + NetworkAgent.CMD_STOP_PACKET_KEEPALIVE, slot, PacketKeepalive.SUCCESS, network)); + } + + @Override public void factoryReset() { enforceConnectivityInternalPermission(); diff --git a/services/core/java/com/android/server/connectivity/KeepalivePacketData.java b/services/core/java/com/android/server/connectivity/KeepalivePacketData.java new file mode 100644 index 0000000..64b9399 --- /dev/null +++ b/services/core/java/com/android/server/connectivity/KeepalivePacketData.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2015 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 android.system.OsConstants; +import android.net.ConnectivityManager; +import android.net.NetworkUtils; +import android.net.util.IpUtils; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static android.net.ConnectivityManager.PacketKeepalive.*; + +/** + * Represents the actual packets that are sent by the + * {@link android.net.ConnectivityManager.PacketKeepalive} API. + * + * @hide + */ +public class KeepalivePacketData { + /** Protocol of the packet to send; one of the OsConstants.ETH_P_* values. */ + public final int protocol; + + /** Source IP address */ + public final InetAddress srcAddress; + + /** Destination IP address */ + public final InetAddress dstAddress; + + /** Source port */ + public final int srcPort; + + /** Destination port */ + public final int dstPort; + + /** Destination MAC address. Can change if routing changes. */ + public byte[] dstMac; + + /** Packet data. A raw byte string of packet data, not including the link-layer header. */ + public final byte[] data; + + private static final int IPV4_HEADER_LENGTH = 20; + private static final int UDP_HEADER_LENGTH = 8; + + protected KeepalivePacketData(InetAddress srcAddress, int srcPort, + InetAddress dstAddress, int dstPort, byte[] data) throws InvalidPacketException { + this.srcAddress = srcAddress; + this.dstAddress = dstAddress; + this.srcPort = srcPort; + this.dstPort = dstPort; + this.data = data; + + // Check we have two IP addresses of the same family. + if (srcAddress == null || dstAddress == null || + !srcAddress.getClass().getName().equals(dstAddress.getClass().getName())) { + } + + // Set the protocol. + if (this.dstAddress instanceof Inet4Address) { + this.protocol = OsConstants.ETH_P_IP; + } else if (this.dstAddress instanceof Inet6Address) { + this.protocol = OsConstants.ETH_P_IPV6; + } else { + throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS); + } + + // Check the ports. + if (!IpUtils.isValidUdpOrTcpPort(srcPort) || !IpUtils.isValidUdpOrTcpPort(dstPort)) { + throw new InvalidPacketException(ERROR_INVALID_PORT); + } + } + + public static class InvalidPacketException extends Exception { + final public int error; + public InvalidPacketException(int error) { + this.error = error; + } + } + + /** + * Creates an IPsec NAT-T keepalive packet with the specified parameters. + */ + public static KeepalivePacketData nattKeepalivePacket( + InetAddress srcAddress, int srcPort, + InetAddress dstAddress, int dstPort) throws InvalidPacketException { + + if (!(srcAddress instanceof Inet4Address)) { + throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS); + } + + if (dstPort != NATT_PORT) { + throw new InvalidPacketException(ERROR_INVALID_PORT); + } + + int length = IPV4_HEADER_LENGTH + UDP_HEADER_LENGTH + 1; + ByteBuffer buf = ByteBuffer.allocate(length); + buf.order(ByteOrder.BIG_ENDIAN); + buf.putShort((short) 0x4500); // IP version and TOS + buf.putShort((short) length); + buf.putInt(0); // ID, flags, offset + buf.put((byte) 64); // TTL + buf.put((byte) OsConstants.IPPROTO_UDP); + int ipChecksumOffset = buf.position(); + buf.putShort((short) 0); // IP checksum + buf.put(srcAddress.getAddress()); + buf.put(dstAddress.getAddress()); + buf.putShort((short) srcPort); + buf.putShort((short) dstPort); + buf.putShort((short) (length - 20)); // UDP length + int udpChecksumOffset = buf.position(); + buf.putShort((short) 0); // UDP checksum + buf.put((byte) 0xff); // NAT-T keepalive + buf.putShort(ipChecksumOffset, IpUtils.ipChecksum(buf, 0)); + buf.putShort(udpChecksumOffset, IpUtils.udpChecksum(buf, 0, IPV4_HEADER_LENGTH)); + + return new KeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, buf.array()); + } +} diff --git a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java new file mode 100644 index 0000000..c78f347 --- /dev/null +++ b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2015 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 com.android.internal.util.HexDump; +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.connectivity.KeepalivePacketData; +import com.android.server.connectivity.NetworkAgentInfo; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.PacketKeepalive; +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.Message; +import android.os.Messenger; +import android.os.Process; +import android.os.RemoteException; +import android.system.OsConstants; +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 java.net.InetAddress; +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. + * + * 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. + */ +public class KeepaliveTracker { + + private static final String TAG = "KeepaliveTracker"; + private static final boolean DBG = true; + + // TODO: Change this to a system-only permission. + public static final String PERMISSION = android.Manifest.permission.CHANGE_NETWORK_STATE; + + /** Keeps track of keepalive requests. */ + private final HashMap <NetworkAgentInfo, HashMap<Integer, KeepaliveInfo>> mKeepalives = + new HashMap<> (); + private final Handler mConnectivityServiceHandler; + + public KeepaliveTracker(Handler handler) { + mConnectivityServiceHandler = handler; + } + + /** + * Tracks information about a packet keepalive. + * + * All information about this keepalive is known at construction time except the slot number, + * which is only returned when the hardware has successfully started the keepalive. + */ + class KeepaliveInfo implements IBinder.DeathRecipient { + // Bookkeping data. + private final Messenger mMessenger; + private final IBinder mBinder; + private final int mUid; + private final int mPid; + private final NetworkAgentInfo mNai; + + /** Keepalive slot. A small integer that identifies this keepalive among the ones handled + * by this network. */ + private int mSlot = PacketKeepalive.NO_KEEPALIVE; + + // Packet data. + private final KeepalivePacketData mPacket; + private final int mInterval; + + // Whether the keepalive is started or not. + public boolean isStarted; + + public KeepaliveInfo(Messenger messenger, IBinder binder, NetworkAgentInfo nai, + KeepalivePacketData packet, int interval) { + mMessenger = messenger; + mBinder = binder; + mPid = Binder.getCallingPid(); + mUid = Binder.getCallingUid(); + + mNai = nai; + mPacket = packet; + mInterval = interval; + + try { + mBinder.linkToDeath(this, 0); + } catch (RemoteException e) { + binderDied(); + } + } + + public NetworkAgentInfo getNai() { + return mNai; + } + + 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(" data=" + HexDump.toHexString(mPacket.data)) + .append(" uid=").append(mUid).append(" pid=").append(mPid) + .append(" ]") + .toString(); + } + + /** Sends a message back to the application via its PacketKeepalive.Callback. */ + void notifyMessenger(int slot, int 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_PACKET_KEEPALIVE, + mSlot, PacketKeepalive.BINDER_DIED, mNai.network).sendToTarget(); + } + + void unlinkDeathRecipient() { + if (mBinder != null) { + mBinder.unlinkToDeath(this, 0); + } + } + + private int checkNetworkConnected() { + if (!mNai.networkInfo.isConnectedOrConnecting()) { + return ERROR_INVALID_NETWORK; + } + return SUCCESS; + } + + private int checkSourceAddress() { + // Check that we have the source address. + for (InetAddress address : mNai.linkProperties.getAddresses()) { + if (address.equals(mPacket.srcAddress)) { + return SUCCESS; + } + } + return ERROR_INVALID_IP_ADDRESS; + } + + private int checkInterval() { + return mInterval >= 20 ? SUCCESS : ERROR_INVALID_INTERVAL; + } + + private int isValid() { + synchronized (mNai) { + int error = checkInterval(); + if (error == SUCCESS) error = checkNetworkConnected(); + if (error == SUCCESS) error = checkSourceAddress(); + return error; + } + } + + void start(int slot) { + int error = isValid(); + if (error == SUCCESS) { + mSlot = slot; + Log.d(TAG, "Starting keepalive " + mSlot + " on " + mNai.name()); + mNai.asyncChannel.sendMessage(CMD_START_PACKET_KEEPALIVE, slot, mInterval, mPacket); + } else { + notifyMessenger(NO_KEEPALIVE, error); + return; + } + } + + void stop(int reason) { + int uid = Binder.getCallingUid(); + if (uid != mUid && uid != Process.SYSTEM_UID) { + if (DBG) { + Log.e(TAG, "Cannot stop unowned keepalive " + mSlot + " on " + mNai.network); + } + } + if (isStarted) { + Log.d(TAG, "Stopping keepalive " + mSlot + " on " + mNai.name()); + mNai.asyncChannel.sendMessage(CMD_STOP_PACKET_KEEPALIVE, mSlot); + } + notifyMessenger(mSlot, reason); + unlinkDeathRecipient(); + } + } + + void notifyMessenger(Messenger messenger, int slot, int err) { + Message message = Message.obtain(); + message.what = EVENT_PACKET_KEEPALIVE; + message.arg1 = slot; + message.arg2 = err; + message.obj = null; + try { + messenger.send(message); + } catch (RemoteException e) { + // Process died? + } + } + + private int findFirstFreeSlot(NetworkAgentInfo nai) { + HashMap networkKeepalives = mKeepalives.get(nai); + if (networkKeepalives == null) { + networkKeepalives = new HashMap<Integer, KeepaliveInfo>(); + mKeepalives.put(nai, networkKeepalives); + } + + // Find the lowest-numbered free slot. + int slot; + for (slot = 0; slot < networkKeepalives.size(); slot++) { + if (networkKeepalives.get(slot) == null) { + return slot; + } + } + // No free slot, pick one at the end. + + // HACK for broadcom hardware that does not support slot 0! + if (slot == 0) slot = 1; + return slot; + } + + public void handleStartKeepalive(Message message) { + KeepaliveInfo ki = (KeepaliveInfo) message.obj; + NetworkAgentInfo nai = ki.getNai(); + int slot = findFirstFreeSlot(nai); + mKeepalives.get(nai).put(slot, ki); + ki.start(slot); + } + + public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) { + HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai); + if (networkKeepalives != null) { + for (KeepaliveInfo ki : networkKeepalives.values()) { + ki.stop(reason); + } + networkKeepalives.clear(); + mKeepalives.remove(nai); + } + } + + public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) { + HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai); + if (networkKeepalives == null) { + Log.e(TAG, "Attempt to stop keepalive on nonexistent network " + nai.name()); + return; + } + KeepaliveInfo ki = networkKeepalives.get(slot); + if (ki == null) { + Log.e(TAG, "Attempt to stop nonexistent keepalive " + slot + " on " + nai.name()); + return; + } + ki.stop(reason); + networkKeepalives.remove(slot); + if (networkKeepalives.isEmpty()) { + mKeepalives.remove(nai); + } + } + + public void handleCheckKeepalivesStillValid(NetworkAgentInfo nai) { + HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai); + if (networkKeepalives != null) { + ArrayList<Pair<Integer, Integer>> invalidKeepalives = new ArrayList<>(); + for (int slot : networkKeepalives.keySet()) { + int error = networkKeepalives.get(slot).isValid(); + if (error != SUCCESS) { + invalidKeepalives.add(Pair.create(slot, error)); + } + } + for (Pair<Integer, Integer> slotAndError: invalidKeepalives) { + handleStopKeepalive(nai, slotAndError.first, slotAndError.second); + } + } + } + + public void handleEventPacketKeepalive(NetworkAgentInfo nai, Message message) { + int slot = message.arg1; + int reason = message.arg2; + + KeepaliveInfo ki = null; + try { + ki = mKeepalives.get(nai).get(slot); + } catch(NullPointerException e) {} + if (ki == null) { + Log.e(TAG, "Event for unknown keepalive " + slot + " on " + nai.name()); + return; + } + + if (reason == SUCCESS && !ki.isStarted) { + // Keepalive successfully started. + if (DBG) Log.d(TAG, "Started keepalive " + slot + " on " + nai.name()); + ki.isStarted = true; + ki.notifyMessenger(slot, reason); + } else { + // Keepalive successfully stopped, or error. + ki.isStarted = false; + if (reason == SUCCESS) { + if (DBG) Log.d(TAG, "Successfully stopped keepalive " + slot + " on " + nai.name()); + } else { + if (DBG) Log.d(TAG, "Keepalive " + slot + " on " + nai.name() + " error " + reason); + } + handleStopKeepalive(nai, slot, reason); + } + } + + public void startNattKeepalive(NetworkAgentInfo nai, int intervalSeconds, Messenger messenger, + IBinder binder, String srcAddrString, int srcPort, String dstAddrString, int dstPort) { + InetAddress srcAddress, dstAddress; + try { + srcAddress = NetworkUtils.numericToInetAddress(srcAddrString); + dstAddress = NetworkUtils.numericToInetAddress(dstAddrString); + } catch (IllegalArgumentException e) { + notifyMessenger(messenger, NO_KEEPALIVE, ERROR_INVALID_IP_ADDRESS); + return; + } + + KeepalivePacketData packet; + try { + packet = KeepalivePacketData.nattKeepalivePacket( + srcAddress, srcPort, dstAddress, NATT_PORT); + } catch (KeepalivePacketData.InvalidPacketException e) { + notifyMessenger(messenger, NO_KEEPALIVE, e.error); + return; + } + KeepaliveInfo ki = new KeepaliveInfo(messenger, binder, nai, packet, intervalSeconds); + Log.d(TAG, "Created keepalive: " + ki.toString()); + mConnectivityServiceHandler.obtainMessage( + NetworkAgent.CMD_START_PACKET_KEEPALIVE, ki).sendToTarget(); + } + + public void dump(IndentingPrintWriter pw) { + pw.println("Packet keepalives:"); + pw.increaseIndent(); + for (NetworkAgentInfo nai : mKeepalives.keySet()) { + pw.println(nai.name()); + pw.increaseIndent(); + for (int slot : mKeepalives.get(nai).keySet()) { + KeepaliveInfo ki = mKeepalives.get(nai).get(slot); + pw.println(slot + ": " + ki.toString()); + } + pw.decreaseIndent(); + } + pw.decreaseIndent(); + } +} diff --git a/services/net/java/android/net/util/IpUtils.java b/services/net/java/android/net/util/IpUtils.java new file mode 100644 index 0000000..e037c40 --- /dev/null +++ b/services/net/java/android/net/util/IpUtils.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.util; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.nio.BufferOverflowException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ShortBuffer; + +import static android.system.OsConstants.IPPROTO_TCP; +import static android.system.OsConstants.IPPROTO_UDP; + +/** + * @hide + */ +public class IpUtils { + /** + * Converts a signed short value to an unsigned int value. Needed + * because Java does not have unsigned types. + */ + private static int intAbs(short v) { + return v & 0xFFFF; + } + + /** + * Performs an IP checksum (used in IP header and across UDP + * payload) on the specified portion of a ByteBuffer. The seed + * allows the checksum to commence with a specified value. + */ + private static int checksum(ByteBuffer buf, int seed, int start, int end) { + int sum = seed; + final int bufPosition = buf.position(); + + // set position of original ByteBuffer, so that the ShortBuffer + // will be correctly initialized + buf.position(start); + ShortBuffer shortBuf = buf.asShortBuffer(); + + // re-set ByteBuffer position + buf.position(bufPosition); + + final int numShorts = (end - start) / 2; + for (int i = 0; i < numShorts; i++) { + sum += intAbs(shortBuf.get(i)); + } + start += numShorts * 2; + + // see if a singleton byte remains + if (end != start) { + short b = buf.get(start); + + // make it unsigned + if (b < 0) { + b += 256; + } + + sum += b * 256; + } + + sum = ((sum >> 16) & 0xFFFF) + (sum & 0xFFFF); + sum = ((sum + ((sum >> 16) & 0xFFFF)) & 0xFFFF); + int negated = ~sum; + return intAbs((short) negated); + } + + private static int pseudoChecksumIPv4( + ByteBuffer buf, int headerOffset, int protocol, int transportLen) { + int partial = protocol + transportLen; + partial += intAbs(buf.getShort(headerOffset + 12)); + partial += intAbs(buf.getShort(headerOffset + 14)); + partial += intAbs(buf.getShort(headerOffset + 16)); + partial += intAbs(buf.getShort(headerOffset + 18)); + return partial; + } + + private static int pseudoChecksumIPv6( + ByteBuffer buf, int headerOffset, int protocol, int transportLen) { + int partial = protocol + transportLen; + for (int offset = 8; offset < 40; offset += 2) { + partial += intAbs(buf.getShort(headerOffset + offset)); + } + return partial; + } + + private static byte ipversion(ByteBuffer buf, int headerOffset) { + return (byte) ((buf.get(headerOffset) & (byte) 0xf0) >> 4); + } + + public static short ipChecksum(ByteBuffer buf, int headerOffset) { + byte ihl = (byte) (buf.get(headerOffset) & 0x0f); + return (short) checksum(buf, 0, headerOffset, headerOffset + ihl * 4); + } + + private static short transportChecksum(ByteBuffer buf, int protocol, + int ipOffset, int transportOffset, int transportLen) { + if (transportLen < 0) { + throw new IllegalArgumentException("Transport length < 0: " + transportLen); + } + int sum; + byte ver = ipversion(buf, ipOffset); + if (ver == 4) { + sum = pseudoChecksumIPv4(buf, ipOffset, protocol, transportLen); + } else if (ver == 6) { + sum = pseudoChecksumIPv6(buf, ipOffset, protocol, transportLen); + } else { + throw new UnsupportedOperationException("Checksum must be IPv4 or IPv6"); + } + + sum = checksum(buf, sum, transportOffset, transportOffset + transportLen); + if (protocol == IPPROTO_UDP && sum == 0) { + sum = (short) 0xffff; + } + return (short) sum; + } + + public static short udpChecksum(ByteBuffer buf, int ipOffset, int transportOffset) { + int transportLen = intAbs(buf.getShort(transportOffset + 4)); + return transportChecksum(buf, IPPROTO_UDP, ipOffset, transportOffset, transportLen); + } + + public static short tcpChecksum(ByteBuffer buf, int ipOffset, int transportOffset, + int transportLen) { + return transportChecksum(buf, IPPROTO_TCP, ipOffset, transportOffset, transportLen); + } + + public static String addressAndPortToString(InetAddress address, int port) { + return String.format( + (address instanceof Inet6Address) ? "[%s]:%d" : "%s:%d", + address.getHostAddress(), port); + } + + public static boolean isValidUdpOrTcpPort(int port) { + return port > 0 && port < 65536; + } +} diff --git a/services/tests/servicestests/src/android/net/IpUtilsTest.java b/services/tests/servicestests/src/android/net/IpUtilsTest.java new file mode 100644 index 0000000..c2d1608 --- /dev/null +++ b/services/tests/servicestests/src/android/net/IpUtilsTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.util; + +import android.net.util.IpUtils; +import android.test.suitebuilder.annotation.SmallTest; + +import java.nio.ByteBuffer; + +import junit.framework.TestCase; + + +public class IpUtilsTest extends TestCase { + + private static final int IPV4_HEADER_LENGTH = 20; + private static final int IPV6_HEADER_LENGTH = 40; + private static final int TCP_HEADER_LENGTH = 20; + private static final int UDP_HEADER_LENGTH = 8; + private static final int IP_CHECKSUM_OFFSET = 10; + private static final int TCP_CHECKSUM_OFFSET = 16; + private static final int UDP_CHECKSUM_OFFSET = 6; + + private int getUnsignedByte(ByteBuffer buf, int offset) { + return buf.get(offset) & 0xff; + } + + private int getChecksum(ByteBuffer buf, int offset) { + return getUnsignedByte(buf, offset) * 256 + getUnsignedByte(buf, offset + 1); + } + + private void assertChecksumEquals(int expected, short actual) { + assertEquals(Integer.toHexString(expected), Integer.toHexString(actual & 0xffff)); + } + + // Generate test packets using Python code like this:: + // + // from scapy import all as scapy + // + // def JavaPacketDefinition(bytes): + // out = " ByteBuffer packet = ByteBuffer.wrap(new byte[] {\n " + // for i in xrange(len(bytes)): + // out += "(byte) 0x%02x" % ord(bytes[i]) + // if i < len(bytes) - 1: + // if i % 4 == 3: + // out += ",\n " + // else: + // out += ", " + // out += "\n });" + // return out + // + // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2") / + // scapy.UDP(sport=12345, dport=7) / + // "hello") + // print JavaPacketDefinition(str(packet)) + + @SmallTest + public void testIpv6TcpChecksum() throws Exception { + // packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80) / + // scapy.TCP(sport=12345, dport=7, + // seq=1692871236, ack=128376451, flags=16, + // window=32768) / + // "hello, world") + ByteBuffer packet = ByteBuffer.wrap(new byte[] { + (byte) 0x68, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x20, (byte) 0x06, (byte) 0x40, + (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, + (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02, + (byte) 0x30, (byte) 0x39, (byte) 0x00, (byte) 0x07, + (byte) 0x64, (byte) 0xe7, (byte) 0x2a, (byte) 0x44, + (byte) 0x07, (byte) 0xa6, (byte) 0xde, (byte) 0x83, + (byte) 0x50, (byte) 0x10, (byte) 0x80, (byte) 0x00, + (byte) 0xee, (byte) 0x71, (byte) 0x00, (byte) 0x00, + (byte) 0x68, (byte) 0x65, (byte) 0x6c, (byte) 0x6c, + (byte) 0x6f, (byte) 0x2c, (byte) 0x20, (byte) 0x77, + (byte) 0x6f, (byte) 0x72, (byte) 0x6c, (byte) 0x64 + }); + + // Check that a valid packet has checksum 0. + int transportLen = packet.limit() - IPV6_HEADER_LENGTH; + assertEquals(0, IpUtils.tcpChecksum(packet, 0, IPV6_HEADER_LENGTH, transportLen)); + + // Check that we can calculate the checksum from scratch. + int sumOffset = IPV6_HEADER_LENGTH + TCP_CHECKSUM_OFFSET; + int sum = getUnsignedByte(packet, sumOffset) * 256 + getUnsignedByte(packet, sumOffset + 1); + assertEquals(0xee71, sum); + + packet.put(sumOffset, (byte) 0); + packet.put(sumOffset + 1, (byte) 0); + assertChecksumEquals(sum, IpUtils.tcpChecksum(packet, 0, IPV6_HEADER_LENGTH, transportLen)); + + // Check that writing the checksum back into the packet results in a valid packet. + packet.putShort( + sumOffset, + IpUtils.tcpChecksum(packet, 0, IPV6_HEADER_LENGTH, transportLen)); + assertEquals(0, IpUtils.tcpChecksum(packet, 0, IPV6_HEADER_LENGTH, transportLen)); + } + + @SmallTest + public void testIpv4UdpChecksum() { + // packet = (scapy.IP(src="192.0.2.1", dst="192.0.2.2", tos=0x40) / + // scapy.UDP(sport=32012, dport=4500) / + // "\xff") + ByteBuffer packet = ByteBuffer.wrap(new byte[] { + (byte) 0x45, (byte) 0x40, (byte) 0x00, (byte) 0x1d, + (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, + (byte) 0x40, (byte) 0x11, (byte) 0xf6, (byte) 0x8b, + (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01, + (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x02, + (byte) 0x7d, (byte) 0x0c, (byte) 0x11, (byte) 0x94, + (byte) 0x00, (byte) 0x09, (byte) 0xee, (byte) 0x36, + (byte) 0xff + }); + + // Check that a valid packet has IP checksum 0 and UDP checksum 0xffff (0 is not a valid + // UDP checksum, so the udpChecksum rewrites 0 to 0xffff). + assertEquals(0, IpUtils.ipChecksum(packet, 0)); + assertEquals((short) 0xffff, IpUtils.udpChecksum(packet, 0, IPV4_HEADER_LENGTH)); + + // Check that we can calculate the checksums from scratch. + final int ipSumOffset = IP_CHECKSUM_OFFSET; + final int ipSum = getChecksum(packet, ipSumOffset); + assertEquals(0xf68b, ipSum); + + packet.put(ipSumOffset, (byte) 0); + packet.put(ipSumOffset + 1, (byte) 0); + assertChecksumEquals(ipSum, IpUtils.ipChecksum(packet, 0)); + + final int udpSumOffset = IPV4_HEADER_LENGTH + UDP_CHECKSUM_OFFSET; + final int udpSum = getChecksum(packet, udpSumOffset); + assertEquals(0xee36, udpSum); + + packet.put(udpSumOffset, (byte) 0); + packet.put(udpSumOffset + 1, (byte) 0); + assertChecksumEquals(udpSum, IpUtils.udpChecksum(packet, 0, IPV4_HEADER_LENGTH)); + + // Check that writing the checksums back into the packet results in a valid packet. + packet.putShort(ipSumOffset, IpUtils.ipChecksum(packet, 0)); + packet.putShort(udpSumOffset, IpUtils.udpChecksum(packet, 0, IPV4_HEADER_LENGTH)); + assertEquals(0, IpUtils.ipChecksum(packet, 0)); + assertEquals((short) 0xffff, IpUtils.udpChecksum(packet, 0, IPV4_HEADER_LENGTH)); + } +} |