summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRemi NGUYEN VAN <reminv@google.com>2019-01-10 19:12:46 +0900
committerRemi NGUYEN VAN <reminv@google.com>2019-01-23 16:44:21 +0900
commit4a2eb87a8e16874938fa1ce7d39bbfa7b59b9890 (patch)
tree45acea97a40f77dce3a557b121efe06254d7858f /src
parent3fab1c749775db02c1409009ff374542bad929cb (diff)
Move IpClient to NetworkStack
Test: atest FrameworksNetTests NetworkStackTests Bug: b/112869080 Change-Id: I7d00848c052382cd1b6ce458868bed6a1e9e8ec5
Diffstat (limited to 'src')
-rw-r--r--src/android/net/apf/ApfFilter.java1591
-rw-r--r--src/android/net/apf/ApfGenerator.java937
-rw-r--r--src/android/net/dhcp/DhcpAckPacket.java88
-rw-r--r--src/android/net/dhcp/DhcpClient.java1052
-rw-r--r--src/android/net/dhcp/DhcpDeclinePacket.java61
-rw-r--r--src/android/net/dhcp/DhcpDiscoverPacket.java67
-rw-r--r--src/android/net/dhcp/DhcpInformPacket.java62
-rw-r--r--src/android/net/dhcp/DhcpNakPacket.java63
-rw-r--r--src/android/net/dhcp/DhcpOfferPacket.java79
-rw-r--r--src/android/net/dhcp/DhcpPacket.java1364
-rw-r--r--src/android/net/dhcp/DhcpReleasePacket.java58
-rw-r--r--src/android/net/dhcp/DhcpRequestPacket.java71
-rw-r--r--src/android/net/ip/ConnectivityPacketTracker.java153
-rw-r--r--src/android/net/ip/IpClient.java1691
-rw-r--r--src/android/net/ip/IpNeighborMonitor.java246
-rw-r--r--src/android/net/ip/IpReachabilityMonitor.java393
-rw-r--r--src/android/net/util/ConnectivityPacketSummary.java435
-rw-r--r--src/android/net/util/FdEventsReader.java254
-rw-r--r--src/android/net/util/PacketReader.java61
-rw-r--r--src/com/android/server/NetworkStackService.java53
-rw-r--r--src/com/android/server/util/NetworkStackConstants.java91
21 files changed, 8870 insertions, 0 deletions
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
new file mode 100644
index 0000000..50c4dfc
--- /dev/null
+++ b/src/android/net/apf/ApfFilter.java
@@ -0,0 +1,1591 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.apf;
+
+import static android.net.util.SocketUtils.makePacketSocketAddress;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ARPHRD_ETHER;
+import static android.system.OsConstants.ETH_P_ARP;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_RAW;
+
+import static com.android.internal.util.BitUtils.bytesToBEInt;
+import static com.android.internal.util.BitUtils.getUint16;
+import static com.android.internal.util.BitUtils.getUint32;
+import static com.android.internal.util.BitUtils.getUint8;
+import static com.android.internal.util.BitUtils.uint32;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
+
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkUtils;
+import android.net.apf.ApfGenerator.IllegalInstructionException;
+import android.net.apf.ApfGenerator.Register;
+import android.net.ip.IpClient.IpClientCallbacksWrapper;
+import android.net.metrics.ApfProgramEvent;
+import android.net.metrics.ApfStats;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.RaEvent;
+import android.net.util.InterfaceParams;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.HexDump;
+import com.android.internal.util.IndentingPrintWriter;
+
+import libcore.io.IoBridge;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * For networks that support packet filtering via APF programs, {@code ApfFilter}
+ * listens for IPv6 ICMPv6 router advertisements (RAs) and generates APF programs to
+ * filter out redundant duplicate ones.
+ *
+ * Threading model:
+ * A collection of RAs we've received is kept in mRas. Generating APF programs uses mRas to
+ * know what RAs to filter for, thus generating APF programs is dependent on mRas.
+ * mRas can be accessed by multiple threads:
+ * - ReceiveThread, which listens for RAs and adds them to mRas, and generates APF programs.
+ * - callers of:
+ * - setMulticastFilter(), which can cause an APF program to be generated.
+ * - dump(), which dumps mRas among other things.
+ * - shutdown(), which clears mRas.
+ * So access to mRas is synchronized.
+ *
+ * @hide
+ */
+public class ApfFilter {
+
+ // Helper class for specifying functional filter parameters.
+ public static class ApfConfiguration {
+ public ApfCapabilities apfCapabilities;
+ public boolean multicastFilter;
+ public boolean ieee802_3Filter;
+ public int[] ethTypeBlackList;
+ }
+
+ // Enums describing the outcome of receiving an RA packet.
+ private static enum ProcessRaResult {
+ MATCH, // Received RA matched a known RA
+ DROPPED, // Received RA ignored due to MAX_RAS
+ PARSE_ERROR, // Received RA could not be parsed
+ ZERO_LIFETIME, // Received RA had 0 lifetime
+ UPDATE_NEW_RA, // APF program updated for new RA
+ UPDATE_EXPIRY // APF program updated for expiry
+ }
+
+ /**
+ * APF packet counters.
+ *
+ * Packet counters are 32bit big-endian values, and allocated near the end of the APF data
+ * buffer, using negative byte offsets, where -4 is equivalent to maximumApfProgramSize - 4,
+ * the last writable 32bit word.
+ */
+ @VisibleForTesting
+ public static enum Counter {
+ RESERVED_OOB, // Points to offset 0 from the end of the buffer (out-of-bounds)
+ TOTAL_PACKETS,
+ PASSED_ARP,
+ PASSED_DHCP,
+ PASSED_IPV4,
+ PASSED_IPV6_NON_ICMP,
+ PASSED_IPV4_UNICAST,
+ PASSED_IPV6_ICMP,
+ PASSED_IPV6_UNICAST_NON_ICMP,
+ PASSED_ARP_NON_IPV4,
+ PASSED_ARP_UNKNOWN,
+ PASSED_ARP_UNICAST_REPLY,
+ PASSED_NON_IP_UNICAST,
+ DROPPED_ETH_BROADCAST,
+ DROPPED_RA,
+ DROPPED_GARP_REPLY,
+ DROPPED_ARP_OTHER_HOST,
+ DROPPED_IPV4_L2_BROADCAST,
+ DROPPED_IPV4_BROADCAST_ADDR,
+ DROPPED_IPV4_BROADCAST_NET,
+ DROPPED_IPV4_MULTICAST,
+ DROPPED_IPV6_ROUTER_SOLICITATION,
+ DROPPED_IPV6_MULTICAST_NA,
+ DROPPED_IPV6_MULTICAST,
+ DROPPED_IPV6_MULTICAST_PING,
+ DROPPED_IPV6_NON_ICMP_MULTICAST,
+ DROPPED_802_3_FRAME,
+ DROPPED_ETHERTYPE_BLACKLISTED,
+ DROPPED_ARP_REPLY_SPA_NO_HOST;
+
+ // Returns the negative byte offset from the end of the APF data segment for
+ // a given counter.
+ public int offset() {
+ return - this.ordinal() * 4; // Currently, all counters are 32bit long.
+ }
+
+ // Returns the total size of the data segment in bytes.
+ public static int totalSize() {
+ return (Counter.class.getEnumConstants().length - 1) * 4;
+ }
+ }
+
+ /**
+ * When APFv4 is supported, loads R1 with the offset of the specified counter.
+ */
+ private void maybeSetupCounter(ApfGenerator gen, Counter c) {
+ if (mApfCapabilities.hasDataAccess()) {
+ gen.addLoadImmediate(Register.R1, c.offset());
+ }
+ }
+
+ // When APFv4 is supported, these point to the trampolines generated by emitEpilogue().
+ // Otherwise, they're just aliases for PASS_LABEL and DROP_LABEL.
+ private final String mCountAndPassLabel;
+ private final String mCountAndDropLabel;
+
+ // Thread to listen for RAs.
+ @VisibleForTesting
+ class ReceiveThread extends Thread {
+ private final byte[] mPacket = new byte[1514];
+ private final FileDescriptor mSocket;
+ private final long mStart = SystemClock.elapsedRealtime();
+
+ private int mReceivedRas = 0;
+ private int mMatchingRas = 0;
+ private int mDroppedRas = 0;
+ private int mParseErrors = 0;
+ private int mZeroLifetimeRas = 0;
+ private int mProgramUpdates = 0;
+
+ private volatile boolean mStopped;
+
+ public ReceiveThread(FileDescriptor socket) {
+ mSocket = socket;
+ }
+
+ public void halt() {
+ mStopped = true;
+ try {
+ // Interrupts the read() call the thread is blocked in.
+ IoBridge.closeAndSignalBlockedThreads(mSocket);
+ } catch (IOException ignored) {}
+ }
+
+ @Override
+ public void run() {
+ log("begin monitoring");
+ while (!mStopped) {
+ try {
+ int length = Os.read(mSocket, mPacket, 0, mPacket.length);
+ updateStats(processRa(mPacket, length));
+ } catch (IOException|ErrnoException e) {
+ if (!mStopped) {
+ Log.e(TAG, "Read error", e);
+ }
+ }
+ }
+ logStats();
+ }
+
+ private void updateStats(ProcessRaResult result) {
+ mReceivedRas++;
+ switch(result) {
+ case MATCH:
+ mMatchingRas++;
+ return;
+ case DROPPED:
+ mDroppedRas++;
+ return;
+ case PARSE_ERROR:
+ mParseErrors++;
+ return;
+ case ZERO_LIFETIME:
+ mZeroLifetimeRas++;
+ return;
+ case UPDATE_EXPIRY:
+ mMatchingRas++;
+ mProgramUpdates++;
+ return;
+ case UPDATE_NEW_RA:
+ mProgramUpdates++;
+ return;
+ }
+ }
+
+ private void logStats() {
+ final long nowMs = SystemClock.elapsedRealtime();
+ synchronized (this) {
+ final ApfStats stats = new ApfStats.Builder()
+ .setReceivedRas(mReceivedRas)
+ .setMatchingRas(mMatchingRas)
+ .setDroppedRas(mDroppedRas)
+ .setParseErrors(mParseErrors)
+ .setZeroLifetimeRas(mZeroLifetimeRas)
+ .setProgramUpdates(mProgramUpdates)
+ .setDurationMs(nowMs - mStart)
+ .setMaxProgramSize(mApfCapabilities.maximumApfProgramSize)
+ .setProgramUpdatesAll(mNumProgramUpdates)
+ .setProgramUpdatesAllowingMulticast(mNumProgramUpdatesAllowingMulticast)
+ .build();
+ mMetricsLog.log(stats);
+ logApfProgramEventLocked(nowMs / DateUtils.SECOND_IN_MILLIS);
+ }
+ }
+ }
+
+ private static final String TAG = "ApfFilter";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ private static final int ETH_HEADER_LEN = 14;
+ private static final int ETH_DEST_ADDR_OFFSET = 0;
+ private static final int ETH_ETHERTYPE_OFFSET = 12;
+ private static final int ETH_TYPE_MIN = 0x0600;
+ private static final int ETH_TYPE_MAX = 0xFFFF;
+ private static final byte[] ETH_BROADCAST_MAC_ADDRESS =
+ {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
+ // TODO: Make these offsets relative to end of link-layer header; don't include ETH_HEADER_LEN.
+ private static final int IPV4_FRAGMENT_OFFSET_OFFSET = ETH_HEADER_LEN + 6;
+ // Endianness is not an issue for this constant because the APF interpreter always operates in
+ // network byte order.
+ private static final int IPV4_FRAGMENT_OFFSET_MASK = 0x1fff;
+ private static final int IPV4_PROTOCOL_OFFSET = ETH_HEADER_LEN + 9;
+ private static final int IPV4_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 16;
+ private static final int IPV4_ANY_HOST_ADDRESS = 0;
+ private static final int IPV4_BROADCAST_ADDRESS = -1; // 255.255.255.255
+
+ // Traffic class and Flow label are not byte aligned. Luckily we
+ // don't care about either value so we'll consider bytes 1-3 of the
+ // IPv6 header as don't care.
+ private static final int IPV6_FLOW_LABEL_OFFSET = ETH_HEADER_LEN + 1;
+ private static final int IPV6_FLOW_LABEL_LEN = 3;
+ private static final int IPV6_NEXT_HEADER_OFFSET = ETH_HEADER_LEN + 6;
+ private static final int IPV6_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 8;
+ private static final int IPV6_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 24;
+ private static final int IPV6_HEADER_LEN = 40;
+ // The IPv6 all nodes address ff02::1
+ private static final byte[] IPV6_ALL_NODES_ADDRESS =
+ { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
+
+ private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
+
+ // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT
+ private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2;
+ private static final int UDP_HEADER_LEN = 8;
+
+ private static final int DHCP_CLIENT_PORT = 68;
+ // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT
+ private static final int DHCP_CLIENT_MAC_OFFSET = ETH_HEADER_LEN + UDP_HEADER_LEN + 28;
+
+ private static final int ARP_HEADER_OFFSET = ETH_HEADER_LEN;
+ private static final byte[] ARP_IPV4_HEADER = {
+ 0, 1, // Hardware type: Ethernet (1)
+ 8, 0, // Protocol type: IP (0x0800)
+ 6, // Hardware size: 6
+ 4, // Protocol size: 4
+ };
+ private static final int ARP_OPCODE_OFFSET = ARP_HEADER_OFFSET + 6;
+ // Opcode: ARP request (0x0001), ARP reply (0x0002)
+ private static final short ARP_OPCODE_REQUEST = 1;
+ private static final short ARP_OPCODE_REPLY = 2;
+ private static final int ARP_SOURCE_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 14;
+ private static final int ARP_TARGET_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 24;
+ // Do not log ApfProgramEvents whose actual lifetimes was less than this.
+ private static final int APF_PROGRAM_EVENT_LIFETIME_THRESHOLD = 2;
+ // Limit on the Black List size to cap on program usage for this
+ // TODO: Select a proper max length
+ private static final int APF_MAX_ETH_TYPE_BLACK_LIST_LEN = 20;
+
+ private final ApfCapabilities mApfCapabilities;
+ private final IpClientCallbacksWrapper mIpClientCallback;
+ private final InterfaceParams mInterfaceParams;
+ private final IpConnectivityLog mMetricsLog;
+
+ @VisibleForTesting
+ byte[] mHardwareAddress;
+ @VisibleForTesting
+ ReceiveThread mReceiveThread;
+ @GuardedBy("this")
+ private long mUniqueCounter;
+ @GuardedBy("this")
+ private boolean mMulticastFilter;
+ @GuardedBy("this")
+ private boolean mInDozeMode;
+ private final boolean mDrop802_3Frames;
+ private final int[] mEthTypeBlackList;
+
+ // Detects doze mode state transitions.
+ private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)) {
+ PowerManager powerManager =
+ (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ final boolean deviceIdle = powerManager.isDeviceIdleMode();
+ setDozeMode(deviceIdle);
+ }
+ }
+ };
+ private final Context mContext;
+
+ // Our IPv4 address, if we have just one, otherwise null.
+ @GuardedBy("this")
+ private byte[] mIPv4Address;
+ // The subnet prefix length of our IPv4 network. Only valid if mIPv4Address is not null.
+ @GuardedBy("this")
+ private int mIPv4PrefixLength;
+
+ @VisibleForTesting
+ ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
+ IpClientCallbacksWrapper ipClientCallback, IpConnectivityLog log) {
+ mApfCapabilities = config.apfCapabilities;
+ mIpClientCallback = ipClientCallback;
+ mInterfaceParams = ifParams;
+ mMulticastFilter = config.multicastFilter;
+ mDrop802_3Frames = config.ieee802_3Filter;
+ mContext = context;
+
+ if (mApfCapabilities.hasDataAccess()) {
+ mCountAndPassLabel = "countAndPass";
+ mCountAndDropLabel = "countAndDrop";
+ } else {
+ // APFv4 unsupported: turn jumps to the counter trampolines to immediately PASS or DROP,
+ // preserving the original pre-APFv4 behavior.
+ mCountAndPassLabel = ApfGenerator.PASS_LABEL;
+ mCountAndDropLabel = ApfGenerator.DROP_LABEL;
+ }
+
+ // Now fill the black list from the passed array
+ mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList);
+
+ mMetricsLog = log;
+
+ // TODO: ApfFilter should not generate programs until IpClient sends provisioning success.
+ maybeStartFilter();
+
+ // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter.
+ mContext.registerReceiver(mDeviceIdleReceiver,
+ new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
+ }
+
+ public synchronized void setDataSnapshot(byte[] data) {
+ mDataSnapshot = data;
+ }
+
+ private void log(String s) {
+ Log.d(TAG, "(" + mInterfaceParams.name + "): " + s);
+ }
+
+ @GuardedBy("this")
+ private long getUniqueNumberLocked() {
+ return mUniqueCounter++;
+ }
+
+ @GuardedBy("this")
+ private static int[] filterEthTypeBlackList(int[] ethTypeBlackList) {
+ ArrayList<Integer> bl = new ArrayList<Integer>();
+
+ for (int p : ethTypeBlackList) {
+ // Check if the protocol is a valid ether type
+ if ((p < ETH_TYPE_MIN) || (p > ETH_TYPE_MAX)) {
+ continue;
+ }
+
+ // Check if the protocol is not repeated in the passed array
+ if (bl.contains(p)) {
+ continue;
+ }
+
+ // Check if list reach its max size
+ if (bl.size() == APF_MAX_ETH_TYPE_BLACK_LIST_LEN) {
+ Log.w(TAG, "Passed EthType Black List size too large (" + bl.size() +
+ ") using top " + APF_MAX_ETH_TYPE_BLACK_LIST_LEN + " protocols");
+ break;
+ }
+
+ // Now add the protocol to the list
+ bl.add(p);
+ }
+
+ return bl.stream().mapToInt(Integer::intValue).toArray();
+ }
+
+ /**
+ * Attempt to start listening for RAs and, if RAs are received, generating and installing
+ * filters to ignore useless RAs.
+ */
+ @VisibleForTesting
+ void maybeStartFilter() {
+ FileDescriptor socket;
+ try {
+ mHardwareAddress = mInterfaceParams.macAddr.toByteArray();
+ synchronized(this) {
+ // Clear the APF memory to reset all counters upon connecting to the first AP
+ // in an SSID. This is limited to APFv4 devices because this large write triggers
+ // a crash on some older devices (b/78905546).
+ if (mApfCapabilities.hasDataAccess()) {
+ byte[] zeroes = new byte[mApfCapabilities.maximumApfProgramSize];
+ mIpClientCallback.installPacketFilter(zeroes);
+ }
+
+ // Install basic filters
+ installNewProgramLocked();
+ }
+ socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6);
+ SocketAddress addr = makePacketSocketAddress(
+ (short) ETH_P_IPV6, mInterfaceParams.index);
+ Os.bind(socket, addr);
+ NetworkUtils.attachRaFilter(socket, mApfCapabilities.apfPacketFormat);
+ } catch(SocketException|ErrnoException e) {
+ Log.e(TAG, "Error starting filter", e);
+ return;
+ }
+ mReceiveThread = new ReceiveThread(socket);
+ mReceiveThread.start();
+ }
+
+ // Returns seconds since device boot.
+ @VisibleForTesting
+ protected long currentTimeSeconds() {
+ return SystemClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS;
+ }
+
+ public static class InvalidRaException extends Exception {
+ public InvalidRaException(String m) {
+ super(m);
+ }
+ }
+
+ // A class to hold information about an RA.
+ @VisibleForTesting
+ class Ra {
+ // From RFC4861:
+ private static final int ICMP6_RA_HEADER_LEN = 16;
+ private static final int ICMP6_RA_CHECKSUM_OFFSET =
+ ETH_HEADER_LEN + IPV6_HEADER_LEN + 2;
+ private static final int ICMP6_RA_CHECKSUM_LEN = 2;
+ private static final int ICMP6_RA_OPTION_OFFSET =
+ ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN;
+ private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET =
+ ETH_HEADER_LEN + IPV6_HEADER_LEN + 6;
+ private static final int ICMP6_RA_ROUTER_LIFETIME_LEN = 2;
+ // Prefix information option.
+ private static final int ICMP6_PREFIX_OPTION_TYPE = 3;
+ private static final int ICMP6_PREFIX_OPTION_LEN = 32;
+ private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET = 4;
+ private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN = 4;
+ private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET = 8;
+ private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4;
+
+ // From RFC6106: Recursive DNS Server option
+ private static final int ICMP6_RDNSS_OPTION_TYPE = 25;
+ // From RFC6106: DNS Search List option
+ private static final int ICMP6_DNSSL_OPTION_TYPE = 31;
+
+ // From RFC4191: Route Information option
+ private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24;
+ // Above three options all have the same format:
+ private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4;
+ private static final int ICMP6_4_BYTE_LIFETIME_LEN = 4;
+
+ // Note: mPacket's position() cannot be assumed to be reset.
+ private final ByteBuffer mPacket;
+ // List of binary ranges that include the whole packet except the lifetimes.
+ // Pairs consist of offset and length.
+ private final ArrayList<Pair<Integer, Integer>> mNonLifetimes =
+ new ArrayList<Pair<Integer, Integer>>();
+ // Minimum lifetime in packet
+ long mMinLifetime;
+ // When the packet was last captured, in seconds since Unix Epoch
+ long mLastSeen;
+
+ // For debugging only. Offsets into the packet where PIOs are.
+ private final ArrayList<Integer> mPrefixOptionOffsets = new ArrayList<>();
+
+ // For debugging only. Offsets into the packet where RDNSS options are.
+ private final ArrayList<Integer> mRdnssOptionOffsets = new ArrayList<>();
+
+ // For debugging only. How many times this RA was seen.
+ int seenCount = 0;
+
+ // For debugging only. Returns the hex representation of the last matching packet.
+ String getLastMatchingPacket() {
+ return HexDump.toHexString(mPacket.array(), 0, mPacket.capacity(),
+ false /* lowercase */);
+ }
+
+ // For debugging only. Returns the string representation of the IPv6 address starting at
+ // position pos in the packet.
+ private String IPv6AddresstoString(int pos) {
+ try {
+ byte[] array = mPacket.array();
+ // Can't just call copyOfRange() and see if it throws, because if it reads past the
+ // end it pads with zeros instead of throwing.
+ if (pos < 0 || pos + 16 > array.length || pos + 16 < pos) {
+ return "???";
+ }
+ byte[] addressBytes = Arrays.copyOfRange(array, pos, pos + 16);
+ InetAddress address = (Inet6Address) InetAddress.getByAddress(addressBytes);
+ return address.getHostAddress();
+ } catch (UnsupportedOperationException e) {
+ // array() failed. Cannot happen, mPacket is array-backed and read-write.
+ return "???";
+ } catch (ClassCastException|UnknownHostException e) {
+ // Cannot happen.
+ return "???";
+ }
+ }
+
+ // Can't be static because it's in a non-static inner class.
+ // TODO: Make this static once RA is its own class.
+ private void prefixOptionToString(StringBuffer sb, int offset) {
+ String prefix = IPv6AddresstoString(offset + 16);
+ int length = getUint8(mPacket, offset + 2);
+ long valid = getUint32(mPacket, offset + 4);
+ long preferred = getUint32(mPacket, offset + 8);
+ sb.append(String.format("%s/%d %ds/%ds ", prefix, length, valid, preferred));
+ }
+
+ private void rdnssOptionToString(StringBuffer sb, int offset) {
+ int optLen = getUint8(mPacket, offset + 1) * 8;
+ if (optLen < 24) return; // Malformed or empty.
+ long lifetime = getUint32(mPacket, offset + 4);
+ int numServers = (optLen - 8) / 16;
+ sb.append("DNS ").append(lifetime).append("s");
+ for (int server = 0; server < numServers; server++) {
+ sb.append(" ").append(IPv6AddresstoString(offset + 8 + 16 * server));
+ }
+ }
+
+ public String toString() {
+ try {
+ StringBuffer sb = new StringBuffer();
+ sb.append(String.format("RA %s -> %s %ds ",
+ IPv6AddresstoString(IPV6_SRC_ADDR_OFFSET),
+ IPv6AddresstoString(IPV6_DEST_ADDR_OFFSET),
+ getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET)));
+ for (int i: mPrefixOptionOffsets) {
+ prefixOptionToString(sb, i);
+ }
+ for (int i: mRdnssOptionOffsets) {
+ rdnssOptionToString(sb, i);
+ }
+ return sb.toString();
+ } catch (BufferUnderflowException|IndexOutOfBoundsException e) {
+ return "<Malformed RA>";
+ }
+ }
+
+ /**
+ * Add a binary range of the packet that does not include a lifetime to mNonLifetimes.
+ * Assumes mPacket.position() is as far as we've parsed the packet.
+ * @param lastNonLifetimeStart offset within packet of where the last binary range of
+ * data not including a lifetime.
+ * @param lifetimeOffset offset from mPacket.position() to the next lifetime data.
+ * @param lifetimeLength length of the next lifetime data.
+ * @return offset within packet of where the next binary range of data not including
+ * a lifetime. This can be passed into the next invocation of this function
+ * via {@code lastNonLifetimeStart}.
+ */
+ private int addNonLifetime(int lastNonLifetimeStart, int lifetimeOffset,
+ int lifetimeLength) {
+ lifetimeOffset += mPacket.position();
+ mNonLifetimes.add(new Pair<Integer, Integer>(lastNonLifetimeStart,
+ lifetimeOffset - lastNonLifetimeStart));
+ return lifetimeOffset + lifetimeLength;
+ }
+
+ private int addNonLifetimeU32(int lastNonLifetimeStart) {
+ return addNonLifetime(lastNonLifetimeStart,
+ ICMP6_4_BYTE_LIFETIME_OFFSET, ICMP6_4_BYTE_LIFETIME_LEN);
+ }
+
+ // Note that this parses RA and may throw InvalidRaException (from
+ // Buffer.position(int) or due to an invalid-length option) or IndexOutOfBoundsException
+ // (from ByteBuffer.get(int) ) if parsing encounters something non-compliant with
+ // specifications.
+ Ra(byte[] packet, int length) throws InvalidRaException {
+ if (length < ICMP6_RA_OPTION_OFFSET) {
+ throw new InvalidRaException("Not an ICMP6 router advertisement");
+ }
+
+ mPacket = ByteBuffer.wrap(Arrays.copyOf(packet, length));
+ mLastSeen = currentTimeSeconds();
+
+ // Sanity check packet in case a packet arrives before we attach RA filter
+ // to our packet socket. b/29586253
+ if (getUint16(mPacket, ETH_ETHERTYPE_OFFSET) != ETH_P_IPV6 ||
+ getUint8(mPacket, IPV6_NEXT_HEADER_OFFSET) != IPPROTO_ICMPV6 ||
+ getUint8(mPacket, ICMP6_TYPE_OFFSET) != ICMPV6_ROUTER_ADVERTISEMENT) {
+ throw new InvalidRaException("Not an ICMP6 router advertisement");
+ }
+
+
+ RaEvent.Builder builder = new RaEvent.Builder();
+
+ // Ignore the flow label and low 4 bits of traffic class.
+ int lastNonLifetimeStart = addNonLifetime(0,
+ IPV6_FLOW_LABEL_OFFSET,
+ IPV6_FLOW_LABEL_LEN);
+
+ // Ignore the checksum.
+ lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
+ ICMP6_RA_CHECKSUM_OFFSET,
+ ICMP6_RA_CHECKSUM_LEN);
+
+ // Parse router lifetime
+ lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
+ ICMP6_RA_ROUTER_LIFETIME_OFFSET,
+ ICMP6_RA_ROUTER_LIFETIME_LEN);
+ builder.updateRouterLifetime(getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET));
+
+ // Ensures that the RA is not truncated.
+ mPacket.position(ICMP6_RA_OPTION_OFFSET);
+ while (mPacket.hasRemaining()) {
+ final int position = mPacket.position();
+ final int optionType = getUint8(mPacket, position);
+ final int optionLength = getUint8(mPacket, position + 1) * 8;
+ long lifetime;
+ switch (optionType) {
+ case ICMP6_PREFIX_OPTION_TYPE:
+ // Parse valid lifetime
+ lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
+ ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET,
+ ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN);
+ lifetime = getUint32(mPacket,
+ position + ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET);
+ builder.updatePrefixValidLifetime(lifetime);
+ // Parse preferred lifetime
+ lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart,
+ ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET,
+ ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN);
+ lifetime = getUint32(mPacket,
+ position + ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET);
+ builder.updatePrefixPreferredLifetime(lifetime);
+ mPrefixOptionOffsets.add(position);
+ break;
+ // These three options have the same lifetime offset and size, and
+ // are processed with the same specialized addNonLifetimeU32:
+ case ICMP6_RDNSS_OPTION_TYPE:
+ mRdnssOptionOffsets.add(position);
+ lastNonLifetimeStart = addNonLifetimeU32(lastNonLifetimeStart);
+ lifetime = getUint32(mPacket, position + ICMP6_4_BYTE_LIFETIME_OFFSET);
+ builder.updateRdnssLifetime(lifetime);
+ break;
+ case ICMP6_ROUTE_INFO_OPTION_TYPE:
+ lastNonLifetimeStart = addNonLifetimeU32(lastNonLifetimeStart);
+ lifetime = getUint32(mPacket, position + ICMP6_4_BYTE_LIFETIME_OFFSET);
+ builder.updateRouteInfoLifetime(lifetime);
+ break;
+ case ICMP6_DNSSL_OPTION_TYPE:
+ lastNonLifetimeStart = addNonLifetimeU32(lastNonLifetimeStart);
+ lifetime = getUint32(mPacket, position + ICMP6_4_BYTE_LIFETIME_OFFSET);
+ builder.updateDnsslLifetime(lifetime);
+ break;
+ default:
+ // RFC4861 section 4.2 dictates we ignore unknown options for fowards
+ // compatibility.
+ break;
+ }
+ if (optionLength <= 0) {
+ throw new InvalidRaException(String.format(
+ "Invalid option length opt=%d len=%d", optionType, optionLength));
+ }
+ mPacket.position(position + optionLength);
+ }
+ // Mark non-lifetime bytes since last lifetime.
+ addNonLifetime(lastNonLifetimeStart, 0, 0);
+ mMinLifetime = minLifetime(packet, length);
+ mMetricsLog.log(builder.build());
+ }
+
+ // Ignoring lifetimes (which may change) does {@code packet} match this RA?
+ boolean matches(byte[] packet, int length) {
+ if (length != mPacket.capacity()) return false;
+ byte[] referencePacket = mPacket.array();
+ for (Pair<Integer, Integer> nonLifetime : mNonLifetimes) {
+ for (int i = nonLifetime.first; i < (nonLifetime.first + nonLifetime.second); i++) {
+ if (packet[i] != referencePacket[i]) return false;
+ }
+ }
+ return true;
+ }
+
+ // What is the minimum of all lifetimes within {@code packet} in seconds?
+ // Precondition: matches(packet, length) already returned true.
+ long minLifetime(byte[] packet, int length) {
+ long minLifetime = Long.MAX_VALUE;
+ // Wrap packet in ByteBuffer so we can read big-endian values easily
+ ByteBuffer byteBuffer = ByteBuffer.wrap(packet);
+ for (int i = 0; (i + 1) < mNonLifetimes.size(); i++) {
+ int offset = mNonLifetimes.get(i).first + mNonLifetimes.get(i).second;
+
+ // The flow label is in mNonLifetimes, but it's not a lifetime.
+ if (offset == IPV6_FLOW_LABEL_OFFSET) {
+ continue;
+ }
+
+ // The checksum is in mNonLifetimes, but it's not a lifetime.
+ if (offset == ICMP6_RA_CHECKSUM_OFFSET) {
+ continue;
+ }
+
+ final int lifetimeLength = mNonLifetimes.get(i+1).first - offset;
+ final long optionLifetime;
+ switch (lifetimeLength) {
+ case 2:
+ optionLifetime = getUint16(byteBuffer, offset);
+ break;
+ case 4:
+ optionLifetime = getUint32(byteBuffer, offset);
+ break;
+ default:
+ throw new IllegalStateException("bogus lifetime size " + lifetimeLength);
+ }
+ minLifetime = Math.min(minLifetime, optionLifetime);
+ }
+ return minLifetime;
+ }
+
+ // How many seconds does this RA's have to live, taking into account the fact
+ // that we might have seen it a while ago.
+ long currentLifetime() {
+ return mMinLifetime - (currentTimeSeconds() - mLastSeen);
+ }
+
+ boolean isExpired() {
+ // TODO: We may want to handle 0 lifetime RAs differently, if they are common. We'll
+ // have to calculte the filter lifetime specially as a fraction of 0 is still 0.
+ return currentLifetime() <= 0;
+ }
+
+ // Append a filter for this RA to {@code gen}. Jump to DROP_LABEL if it should be dropped.
+ // Jump to the next filter if packet doesn't match this RA.
+ @GuardedBy("ApfFilter.this")
+ long generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+ String nextFilterLabel = "Ra" + getUniqueNumberLocked();
+ // Skip if packet is not the right size
+ gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT);
+ gen.addJumpIfR0NotEquals(mPacket.capacity(), nextFilterLabel);
+ int filterLifetime = (int)(currentLifetime() / FRACTION_OF_LIFETIME_TO_FILTER);
+ // Skip filter if expired
+ gen.addLoadFromMemory(Register.R0, gen.FILTER_AGE_MEMORY_SLOT);
+ gen.addJumpIfR0GreaterThan(filterLifetime, nextFilterLabel);
+ for (int i = 0; i < mNonLifetimes.size(); i++) {
+ // Generate code to match the packet bytes
+ Pair<Integer, Integer> nonLifetime = mNonLifetimes.get(i);
+ // Don't generate JNEBS instruction for 0 bytes as it always fails the
+ // ASSERT_FORWARD_IN_PROGRAM(pc + cmp_imm - 1) check where cmp_imm is
+ // the number of bytes to compare. nonLifetime is zero between the
+ // valid and preferred lifetimes in the prefix option.
+ if (nonLifetime.second != 0) {
+ gen.addLoadImmediate(Register.R0, nonLifetime.first);
+ gen.addJumpIfBytesNotEqual(Register.R0,
+ Arrays.copyOfRange(mPacket.array(), nonLifetime.first,
+ nonLifetime.first + nonLifetime.second),
+ nextFilterLabel);
+ }
+ // Generate code to test the lifetimes haven't gone down too far
+ if ((i + 1) < mNonLifetimes.size()) {
+ Pair<Integer, Integer> nextNonLifetime = mNonLifetimes.get(i + 1);
+ int offset = nonLifetime.first + nonLifetime.second;
+
+ // Skip the Flow label.
+ if (offset == IPV6_FLOW_LABEL_OFFSET) {
+ continue;
+ }
+ // Skip the checksum.
+ if (offset == ICMP6_RA_CHECKSUM_OFFSET) {
+ continue;
+ }
+ int length = nextNonLifetime.first - offset;
+ switch (length) {
+ case 4: gen.addLoad32(Register.R0, offset); break;
+ case 2: gen.addLoad16(Register.R0, offset); break;
+ default: throw new IllegalStateException("bogus lifetime size " + length);
+ }
+ gen.addJumpIfR0LessThan(filterLifetime, nextFilterLabel);
+ }
+ }
+ maybeSetupCounter(gen, Counter.DROPPED_RA);
+ gen.addJump(mCountAndDropLabel);
+ gen.defineLabel(nextFilterLabel);
+ return filterLifetime;
+ }
+ }
+
+ // Maximum number of RAs to filter for.
+ private static final int MAX_RAS = 10;
+
+ @GuardedBy("this")
+ private ArrayList<Ra> mRas = new ArrayList<Ra>();
+
+ // There is always some marginal benefit to updating the installed APF program when an RA is
+ // seen because we can extend the program's lifetime slightly, but there is some cost to
+ // updating the program, so don't bother unless the program is going to expire soon. This
+ // constant defines "soon" in seconds.
+ private static final long MAX_PROGRAM_LIFETIME_WORTH_REFRESHING = 30;
+ // We don't want to filter an RA for it's whole lifetime as it'll be expired by the time we ever
+ // see a refresh. Using half the lifetime might be a good idea except for the fact that
+ // packets may be dropped, so let's use 6.
+ private static final int FRACTION_OF_LIFETIME_TO_FILTER = 6;
+
+ // When did we last install a filter program? In seconds since Unix Epoch.
+ @GuardedBy("this")
+ private long mLastTimeInstalledProgram;
+ // How long should the last installed filter program live for? In seconds.
+ @GuardedBy("this")
+ private long mLastInstalledProgramMinLifetime;
+ @GuardedBy("this")
+ private ApfProgramEvent.Builder mLastInstallEvent;
+
+ // For debugging only. The last program installed.
+ @GuardedBy("this")
+ private byte[] mLastInstalledProgram;
+
+ /**
+ * For debugging only. Contains the latest APF buffer snapshot captured from the firmware.
+ *
+ * A typical size for this buffer is 4KB. It is present only if the WiFi HAL supports
+ * IWifiStaIface#readApfPacketFilterData(), and the APF interpreter advertised support for
+ * the opcodes to access the data buffer (LDDW and STDW).
+ */
+ @GuardedBy("this") @Nullable
+ private byte[] mDataSnapshot;
+
+ // How many times the program was updated since we started.
+ @GuardedBy("this")
+ private int mNumProgramUpdates = 0;
+ // How many times the program was updated since we started for allowing multicast traffic.
+ @GuardedBy("this")
+ private int mNumProgramUpdatesAllowingMulticast = 0;
+
+ /**
+ * Generate filter code to process ARP packets. Execution of this code ends in either the
+ * DROP_LABEL or PASS_LABEL and does not fall off the end.
+ * Preconditions:
+ * - Packet being filtered is ARP
+ */
+ @GuardedBy("this")
+ private void generateArpFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+ // Here's a basic summary of what the ARP filter program does:
+ //
+ // if not ARP IPv4
+ // pass
+ // if not ARP IPv4 reply or request
+ // pass
+ // if ARP reply source ip is 0.0.0.0
+ // drop
+ // if unicast ARP reply
+ // pass
+ // if interface has no IPv4 address
+ // if target ip is 0.0.0.0
+ // drop
+ // else
+ // if target ip is not the interface ip
+ // drop
+ // pass
+
+ final String checkTargetIPv4 = "checkTargetIPv4";
+
+ // Pass if not ARP IPv4.
+ gen.addLoadImmediate(Register.R0, ARP_HEADER_OFFSET);
+ maybeSetupCounter(gen, Counter.PASSED_ARP_NON_IPV4);
+ gen.addJumpIfBytesNotEqual(Register.R0, ARP_IPV4_HEADER, mCountAndPassLabel);
+
+ // Pass if unknown ARP opcode.
+ gen.addLoad16(Register.R0, ARP_OPCODE_OFFSET);
+ gen.addJumpIfR0Equals(ARP_OPCODE_REQUEST, checkTargetIPv4); // Skip to unicast check
+ maybeSetupCounter(gen, Counter.PASSED_ARP_UNKNOWN);
+ gen.addJumpIfR0NotEquals(ARP_OPCODE_REPLY, mCountAndPassLabel);
+
+ // Drop if ARP reply source IP is 0.0.0.0
+ gen.addLoad32(Register.R0, ARP_SOURCE_IP_ADDRESS_OFFSET);
+ maybeSetupCounter(gen, Counter.DROPPED_ARP_REPLY_SPA_NO_HOST);
+ gen.addJumpIfR0Equals(IPV4_ANY_HOST_ADDRESS, mCountAndDropLabel);
+
+ // Pass if unicast reply.
+ gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
+ maybeSetupCounter(gen, Counter.PASSED_ARP_UNICAST_REPLY);
+ gen.addJumpIfBytesNotEqual(Register.R0, ETH_BROADCAST_MAC_ADDRESS, mCountAndPassLabel);
+
+ // Either a unicast request, a unicast reply, or a broadcast reply.
+ gen.defineLabel(checkTargetIPv4);
+ if (mIPv4Address == null) {
+ // When there is no IPv4 address, drop GARP replies (b/29404209).
+ gen.addLoad32(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET);
+ maybeSetupCounter(gen, Counter.DROPPED_GARP_REPLY);
+ gen.addJumpIfR0Equals(IPV4_ANY_HOST_ADDRESS, mCountAndDropLabel);
+ } else {
+ // When there is an IPv4 address, drop unicast/broadcast requests
+ // and broadcast replies with a different target IPv4 address.
+ gen.addLoadImmediate(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET);
+ maybeSetupCounter(gen, Counter.DROPPED_ARP_OTHER_HOST);
+ gen.addJumpIfBytesNotEqual(Register.R0, mIPv4Address, mCountAndDropLabel);
+ }
+
+ maybeSetupCounter(gen, Counter.PASSED_ARP);
+ gen.addJump(mCountAndPassLabel);
+ }
+
+ /**
+ * Generate filter code to process IPv4 packets. Execution of this code ends in either the
+ * DROP_LABEL or PASS_LABEL and does not fall off the end.
+ * Preconditions:
+ * - Packet being filtered is IPv4
+ */
+ @GuardedBy("this")
+ private void generateIPv4FilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+ // Here's a basic summary of what the IPv4 filter program does:
+ //
+ // if filtering multicast (i.e. multicast lock not held):
+ // if it's DHCP destined to our MAC:
+ // pass
+ // if it's L2 broadcast:
+ // drop
+ // if it's IPv4 multicast:
+ // drop
+ // if it's IPv4 broadcast:
+ // drop
+ // pass
+
+ if (mMulticastFilter) {
+ final String skipDhcpv4Filter = "skip_dhcp_v4_filter";
+
+ // Pass DHCP addressed to us.
+ // Check it's UDP.
+ gen.addLoad8(Register.R0, IPV4_PROTOCOL_OFFSET);
+ gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipDhcpv4Filter);
+ // Check it's not a fragment. This matches the BPF filter installed by the DHCP client.
+ gen.addLoad16(Register.R0, IPV4_FRAGMENT_OFFSET_OFFSET);
+ gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_OFFSET_MASK, skipDhcpv4Filter);
+ // Check it's addressed to DHCP client port.
+ gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+ gen.addLoad16Indexed(Register.R0, UDP_DESTINATION_PORT_OFFSET);
+ gen.addJumpIfR0NotEquals(DHCP_CLIENT_PORT, skipDhcpv4Filter);
+ // Check it's DHCP to our MAC address.
+ gen.addLoadImmediate(Register.R0, DHCP_CLIENT_MAC_OFFSET);
+ // NOTE: Relies on R1 containing IPv4 header offset.
+ gen.addAddR1();
+ gen.addJumpIfBytesNotEqual(Register.R0, mHardwareAddress, skipDhcpv4Filter);
+ maybeSetupCounter(gen, Counter.PASSED_DHCP);
+ gen.addJump(mCountAndPassLabel);
+
+ // Drop all multicasts/broadcasts.
+ gen.defineLabel(skipDhcpv4Filter);
+
+ // If IPv4 destination address is in multicast range, drop.
+ gen.addLoad8(Register.R0, IPV4_DEST_ADDR_OFFSET);
+ gen.addAnd(0xf0);
+ maybeSetupCounter(gen, Counter.DROPPED_IPV4_MULTICAST);
+ gen.addJumpIfR0Equals(0xe0, mCountAndDropLabel);
+
+ // If IPv4 broadcast packet, drop regardless of L2 (b/30231088).
+ maybeSetupCounter(gen, Counter.DROPPED_IPV4_BROADCAST_ADDR);
+ gen.addLoad32(Register.R0, IPV4_DEST_ADDR_OFFSET);
+ gen.addJumpIfR0Equals(IPV4_BROADCAST_ADDRESS, mCountAndDropLabel);
+ if (mIPv4Address != null && mIPv4PrefixLength < 31) {
+ maybeSetupCounter(gen, Counter.DROPPED_IPV4_BROADCAST_NET);
+ int broadcastAddr = ipv4BroadcastAddress(mIPv4Address, mIPv4PrefixLength);
+ gen.addJumpIfR0Equals(broadcastAddr, mCountAndDropLabel);
+ }
+
+ // If L2 broadcast packet, drop.
+ // TODO: can we invert this condition to fall through to the common pass case below?
+ maybeSetupCounter(gen, Counter.PASSED_IPV4_UNICAST);
+ gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
+ gen.addJumpIfBytesNotEqual(Register.R0, ETH_BROADCAST_MAC_ADDRESS, mCountAndPassLabel);
+ maybeSetupCounter(gen, Counter.DROPPED_IPV4_L2_BROADCAST);
+ gen.addJump(mCountAndDropLabel);
+ }
+
+ // Otherwise, pass
+ maybeSetupCounter(gen, Counter.PASSED_IPV4);
+ gen.addJump(mCountAndPassLabel);
+ }
+
+
+ /**
+ * Generate filter code to process IPv6 packets. Execution of this code ends in either the
+ * DROP_LABEL or PASS_LABEL, or falls off the end for ICMPv6 packets.
+ * Preconditions:
+ * - Packet being filtered is IPv6
+ */
+ @GuardedBy("this")
+ private void generateIPv6FilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+ // Here's a basic summary of what the IPv6 filter program does:
+ //
+ // if we're dropping multicast
+ // if it's not IPCMv6 or it's ICMPv6 but we're in doze mode:
+ // if it's multicast:
+ // drop
+ // pass
+ // if it's ICMPv6 RS to any:
+ // drop
+ // if it's ICMPv6 NA to ff02::1:
+ // drop
+
+ gen.addLoad8(Register.R0, IPV6_NEXT_HEADER_OFFSET);
+
+ // Drop multicast if the multicast filter is enabled.
+ if (mMulticastFilter) {
+ final String skipIPv6MulticastFilterLabel = "skipIPv6MulticastFilter";
+ final String dropAllIPv6MulticastsLabel = "dropAllIPv6Multicast";
+
+ // While in doze mode, drop ICMPv6 multicast pings, let the others pass.
+ // While awake, let all ICMPv6 multicasts through.
+ if (mInDozeMode) {
+ // Not ICMPv6? -> Proceed to multicast filtering
+ gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, dropAllIPv6MulticastsLabel);
+
+ // ICMPv6 but not ECHO? -> Skip the multicast filter.
+ // (ICMPv6 ECHO requests will go through the multicast filter below).
+ gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET);
+ gen.addJumpIfR0NotEquals(ICMPV6_ECHO_REQUEST_TYPE, skipIPv6MulticastFilterLabel);
+ } else {
+ gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIPv6MulticastFilterLabel);
+ }
+
+ // Drop all other packets sent to ff00::/8 (multicast prefix).
+ gen.defineLabel(dropAllIPv6MulticastsLabel);
+ maybeSetupCounter(gen, Counter.DROPPED_IPV6_NON_ICMP_MULTICAST);
+ gen.addLoad8(Register.R0, IPV6_DEST_ADDR_OFFSET);
+ gen.addJumpIfR0Equals(0xff, mCountAndDropLabel);
+ // Not multicast. Pass.
+ maybeSetupCounter(gen, Counter.PASSED_IPV6_UNICAST_NON_ICMP);
+ gen.addJump(mCountAndPassLabel);
+ gen.defineLabel(skipIPv6MulticastFilterLabel);
+ } else {
+ // If not ICMPv6, pass.
+ maybeSetupCounter(gen, Counter.PASSED_IPV6_NON_ICMP);
+ gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, mCountAndPassLabel);
+ }
+
+ // If we got this far, the packet is ICMPv6. Drop some specific types.
+
+ // Add unsolicited multicast neighbor announcements filter
+ String skipUnsolicitedMulticastNALabel = "skipUnsolicitedMulticastNA";
+ gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET);
+ // Drop all router solicitations (b/32833400)
+ maybeSetupCounter(gen, Counter.DROPPED_IPV6_ROUTER_SOLICITATION);
+ gen.addJumpIfR0Equals(ICMPV6_ROUTER_SOLICITATION, mCountAndDropLabel);
+ // If not neighbor announcements, skip filter.
+ gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, skipUnsolicitedMulticastNALabel);
+ // If to ff02::1, drop.
+ // TODO: Drop only if they don't contain the address of on-link neighbours.
+ gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET);
+ gen.addJumpIfBytesNotEqual(Register.R0, IPV6_ALL_NODES_ADDRESS,
+ skipUnsolicitedMulticastNALabel);
+ maybeSetupCounter(gen, Counter.DROPPED_IPV6_MULTICAST_NA);
+ gen.addJump(mCountAndDropLabel);
+ gen.defineLabel(skipUnsolicitedMulticastNALabel);
+ }
+
+ /**
+ * Begin generating an APF program to:
+ * <ul>
+ * <li>Drop/Pass 802.3 frames (based on policy)
+ * <li>Drop packets with EtherType within the Black List
+ * <li>Drop ARP requests not for us, if mIPv4Address is set,
+ * <li>Drop IPv4 broadcast packets, except DHCP destined to our MAC,
+ * <li>Drop IPv4 multicast packets, if mMulticastFilter,
+ * <li>Pass all other IPv4 packets,
+ * <li>Drop all broadcast non-IP non-ARP packets.
+ * <li>Pass all non-ICMPv6 IPv6 packets,
+ * <li>Pass all non-IPv4 and non-IPv6 packets,
+ * <li>Drop IPv6 ICMPv6 NAs to ff02::1.
+ * <li>Drop IPv6 ICMPv6 RSs.
+ * <li>Let execution continue off the end of the program for IPv6 ICMPv6 packets. This allows
+ * insertion of RA filters here, or if there aren't any, just passes the packets.
+ * </ul>
+ */
+ @GuardedBy("this")
+ private ApfGenerator emitPrologueLocked() throws IllegalInstructionException {
+ // This is guaranteed to succeed because of the check in maybeCreate.
+ ApfGenerator gen = new ApfGenerator(mApfCapabilities.apfVersionSupported);
+
+ if (mApfCapabilities.hasDataAccess()) {
+ // Increment TOTAL_PACKETS
+ maybeSetupCounter(gen, Counter.TOTAL_PACKETS);
+ gen.addLoadData(Register.R0, 0); // load counter
+ gen.addAdd(1);
+ gen.addStoreData(Register.R0, 0); // write-back counter
+ }
+
+ // Here's a basic summary of what the initial program does:
+ //
+ // if it's a 802.3 Frame (ethtype < 0x0600):
+ // drop or pass based on configurations
+ // if it has a ether-type that belongs to the black list
+ // drop
+ // if it's ARP:
+ // insert ARP filter to drop or pass these appropriately
+ // if it's IPv4:
+ // insert IPv4 filter to drop or pass these appropriately
+ // if it's not IPv6:
+ // if it's broadcast:
+ // drop
+ // pass
+ // insert IPv6 filter to drop, pass, or fall off the end for ICMPv6 packets
+
+ gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
+
+ if (mDrop802_3Frames) {
+ // drop 802.3 frames (ethtype < 0x0600)
+ maybeSetupCounter(gen, Counter.DROPPED_802_3_FRAME);
+ gen.addJumpIfR0LessThan(ETH_TYPE_MIN, mCountAndDropLabel);
+ }
+
+ // Handle ether-type black list
+ maybeSetupCounter(gen, Counter.DROPPED_ETHERTYPE_BLACKLISTED);
+ for (int p : mEthTypeBlackList) {
+ gen.addJumpIfR0Equals(p, mCountAndDropLabel);
+ }
+
+ // Add ARP filters:
+ String skipArpFiltersLabel = "skipArpFilters";
+ gen.addJumpIfR0NotEquals(ETH_P_ARP, skipArpFiltersLabel);
+ generateArpFilterLocked(gen);
+ gen.defineLabel(skipArpFiltersLabel);
+
+ // Add IPv4 filters:
+ String skipIPv4FiltersLabel = "skipIPv4Filters";
+ // NOTE: Relies on R0 containing ethertype. This is safe because if we got here, we did not
+ // execute the ARP filter, since that filter does not fall through, but either drops or
+ // passes.
+ gen.addJumpIfR0NotEquals(ETH_P_IP, skipIPv4FiltersLabel);
+ generateIPv4FilterLocked(gen);
+ gen.defineLabel(skipIPv4FiltersLabel);
+
+ // Check for IPv6:
+ // NOTE: Relies on R0 containing ethertype. This is safe because if we got here, we did not
+ // execute the ARP or IPv4 filters, since those filters do not fall through, but either
+ // drop or pass.
+ String ipv6FilterLabel = "IPv6Filters";
+ gen.addJumpIfR0Equals(ETH_P_IPV6, ipv6FilterLabel);
+
+ // Drop non-IP non-ARP broadcasts, pass the rest
+ gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
+ maybeSetupCounter(gen, Counter.PASSED_NON_IP_UNICAST);
+ gen.addJumpIfBytesNotEqual(Register.R0, ETH_BROADCAST_MAC_ADDRESS, mCountAndPassLabel);
+ maybeSetupCounter(gen, Counter.DROPPED_ETH_BROADCAST);
+ gen.addJump(mCountAndDropLabel);
+
+ // Add IPv6 filters:
+ gen.defineLabel(ipv6FilterLabel);
+ generateIPv6FilterLocked(gen);
+ return gen;
+ }
+
+ /**
+ * Append packet counting epilogue to the APF program.
+ *
+ * Currently, the epilogue consists of two trampolines which count passed and dropped packets
+ * before jumping to the actual PASS and DROP labels.
+ */
+ @GuardedBy("this")
+ private void emitEpilogue(ApfGenerator gen) throws IllegalInstructionException {
+ // If APFv4 is unsupported, no epilogue is necessary: if execution reached this far, it
+ // will just fall-through to the PASS label.
+ if (!mApfCapabilities.hasDataAccess()) return;
+
+ // Execution will reach the bottom of the program if none of the filters match,
+ // which will pass the packet to the application processor.
+ maybeSetupCounter(gen, Counter.PASSED_IPV6_ICMP);
+
+ // Append the count & pass trampoline, which increments the counter at the data address
+ // pointed to by R1, then jumps to the pass label. This saves a few bytes over inserting
+ // the entire sequence inline for every counter.
+ gen.defineLabel(mCountAndPassLabel);
+ gen.addLoadData(Register.R0, 0); // R0 = *(R1 + 0)
+ gen.addAdd(1); // R0++
+ gen.addStoreData(Register.R0, 0); // *(R1 + 0) = R0
+ gen.addJump(gen.PASS_LABEL);
+
+ // Same as above for the count & drop trampoline.
+ gen.defineLabel(mCountAndDropLabel);
+ gen.addLoadData(Register.R0, 0); // R0 = *(R1 + 0)
+ gen.addAdd(1); // R0++
+ gen.addStoreData(Register.R0, 0); // *(R1 + 0) = R0
+ gen.addJump(gen.DROP_LABEL);
+ }
+
+ /**
+ * Generate and install a new filter program.
+ */
+ @GuardedBy("this")
+ @VisibleForTesting
+ void installNewProgramLocked() {
+ purgeExpiredRasLocked();
+ ArrayList<Ra> rasToFilter = new ArrayList<>();
+ final byte[] program;
+ long programMinLifetime = Long.MAX_VALUE;
+ long maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize;
+ if (mApfCapabilities.hasDataAccess()) {
+ // Reserve space for the counters.
+ maximumApfProgramSize -= Counter.totalSize();
+ }
+
+ try {
+ // Step 1: Determine how many RA filters we can fit in the program.
+ ApfGenerator gen = emitPrologueLocked();
+
+ // The epilogue normally goes after the RA filters, but add it early to include its
+ // length when estimating the total.
+ emitEpilogue(gen);
+
+ // Can't fit the program even without any RA filters?
+ if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
+ Log.e(TAG, "Program exceeds maximum size " + maximumApfProgramSize);
+ return;
+ }
+
+ for (Ra ra : mRas) {
+ ra.generateFilterLocked(gen);
+ // Stop if we get too big.
+ if (gen.programLengthOverEstimate() > maximumApfProgramSize) break;
+ rasToFilter.add(ra);
+ }
+
+ // Step 2: Actually generate the program
+ gen = emitPrologueLocked();
+ for (Ra ra : rasToFilter) {
+ programMinLifetime = Math.min(programMinLifetime, ra.generateFilterLocked(gen));
+ }
+ emitEpilogue(gen);
+ program = gen.generate();
+ } catch (IllegalInstructionException|IllegalStateException e) {
+ Log.e(TAG, "Failed to generate APF program.", e);
+ return;
+ }
+ final long now = currentTimeSeconds();
+ mLastTimeInstalledProgram = now;
+ mLastInstalledProgramMinLifetime = programMinLifetime;
+ mLastInstalledProgram = program;
+ mNumProgramUpdates++;
+
+ if (VDBG) {
+ hexDump("Installing filter: ", program, program.length);
+ }
+ mIpClientCallback.installPacketFilter(program);
+ logApfProgramEventLocked(now);
+ mLastInstallEvent = new ApfProgramEvent.Builder()
+ .setLifetime(programMinLifetime)
+ .setFilteredRas(rasToFilter.size())
+ .setCurrentRas(mRas.size())
+ .setProgramLength(program.length)
+ .setFlags(mIPv4Address != null, mMulticastFilter);
+ }
+
+ @GuardedBy("this")
+ private void logApfProgramEventLocked(long now) {
+ if (mLastInstallEvent == null) {
+ return;
+ }
+ ApfProgramEvent.Builder ev = mLastInstallEvent;
+ mLastInstallEvent = null;
+ final long actualLifetime = now - mLastTimeInstalledProgram;
+ ev.setActualLifetime(actualLifetime);
+ if (actualLifetime < APF_PROGRAM_EVENT_LIFETIME_THRESHOLD) {
+ return;
+ }
+ mMetricsLog.log(ev.build());
+ }
+
+ /**
+ * Returns {@code true} if a new program should be installed because the current one dies soon.
+ */
+ private boolean shouldInstallnewProgram() {
+ long expiry = mLastTimeInstalledProgram + mLastInstalledProgramMinLifetime;
+ return expiry < currentTimeSeconds() + MAX_PROGRAM_LIFETIME_WORTH_REFRESHING;
+ }
+
+ private void hexDump(String msg, byte[] packet, int length) {
+ log(msg + HexDump.toHexString(packet, 0, length, false /* lowercase */));
+ }
+
+ @GuardedBy("this")
+ private void purgeExpiredRasLocked() {
+ for (int i = 0; i < mRas.size();) {
+ if (mRas.get(i).isExpired()) {
+ log("Expiring " + mRas.get(i));
+ mRas.remove(i);
+ } else {
+ i++;
+ }
+ }
+ }
+
+ /**
+ * Process an RA packet, updating the list of known RAs and installing a new APF program
+ * if the current APF program should be updated.
+ * @return a ProcessRaResult enum describing what action was performed.
+ */
+ @VisibleForTesting
+ synchronized ProcessRaResult processRa(byte[] packet, int length) {
+ if (VDBG) hexDump("Read packet = ", packet, length);
+
+ // Have we seen this RA before?
+ for (int i = 0; i < mRas.size(); i++) {
+ Ra ra = mRas.get(i);
+ if (ra.matches(packet, length)) {
+ if (VDBG) log("matched RA " + ra);
+ // Update lifetimes.
+ ra.mLastSeen = currentTimeSeconds();
+ ra.mMinLifetime = ra.minLifetime(packet, length);
+ ra.seenCount++;
+
+ // Keep mRas in LRU order so as to prioritize generating filters for recently seen
+ // RAs. LRU prioritizes this because RA filters are generated in order from mRas
+ // until the filter program exceeds the maximum filter program size allowed by the
+ // chipset, so RAs appearing earlier in mRas are more likely to make it into the
+ // filter program.
+ // TODO: consider sorting the RAs in order of increasing expiry time as well.
+ // Swap to front of array.
+ mRas.add(0, mRas.remove(i));
+
+ // If the current program doesn't expire for a while, don't update.
+ if (shouldInstallnewProgram()) {
+ installNewProgramLocked();
+ return ProcessRaResult.UPDATE_EXPIRY;
+ }
+ return ProcessRaResult.MATCH;
+ }
+ }
+ purgeExpiredRasLocked();
+ // TODO: figure out how to proceed when we've received more then MAX_RAS RAs.
+ if (mRas.size() >= MAX_RAS) {
+ return ProcessRaResult.DROPPED;
+ }
+ final Ra ra;
+ try {
+ ra = new Ra(packet, length);
+ } catch (Exception e) {
+ Log.e(TAG, "Error parsing RA", e);
+ return ProcessRaResult.PARSE_ERROR;
+ }
+ // Ignore 0 lifetime RAs.
+ if (ra.isExpired()) {
+ return ProcessRaResult.ZERO_LIFETIME;
+ }
+ log("Adding " + ra);
+ mRas.add(ra);
+ installNewProgramLocked();
+ return ProcessRaResult.UPDATE_NEW_RA;
+ }
+
+ /**
+ * Create an {@link ApfFilter} if {@code apfCapabilities} indicates support for packet
+ * filtering using APF programs.
+ */
+ public static ApfFilter maybeCreate(Context context, ApfConfiguration config,
+ InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback) {
+ if (context == null || config == null || ifParams == null) return null;
+ ApfCapabilities apfCapabilities = config.apfCapabilities;
+ if (apfCapabilities == null) return null;
+ if (apfCapabilities.apfVersionSupported == 0) return null;
+ if (apfCapabilities.maximumApfProgramSize < 512) {
+ Log.e(TAG, "Unacceptably small APF limit: " + apfCapabilities.maximumApfProgramSize);
+ return null;
+ }
+ // For now only support generating programs for Ethernet frames. If this restriction is
+ // lifted:
+ // 1. the program generator will need its offsets adjusted.
+ // 2. the packet filter attached to our packet socket will need its offset adjusted.
+ if (apfCapabilities.apfPacketFormat != ARPHRD_ETHER) return null;
+ if (!ApfGenerator.supportsVersion(apfCapabilities.apfVersionSupported)) {
+ Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported);
+ return null;
+ }
+
+ return new ApfFilter(context, config, ifParams, ipClientCallback, new IpConnectivityLog());
+ }
+
+ public synchronized void shutdown() {
+ if (mReceiveThread != null) {
+ log("shutting down");
+ mReceiveThread.halt(); // Also closes socket.
+ mReceiveThread = null;
+ }
+ mRas.clear();
+ mContext.unregisterReceiver(mDeviceIdleReceiver);
+ }
+
+ public synchronized void setMulticastFilter(boolean isEnabled) {
+ if (mMulticastFilter == isEnabled) return;
+ mMulticastFilter = isEnabled;
+ if (!isEnabled) {
+ mNumProgramUpdatesAllowingMulticast++;
+ }
+ installNewProgramLocked();
+ }
+
+ @VisibleForTesting
+ public synchronized void setDozeMode(boolean isEnabled) {
+ if (mInDozeMode == isEnabled) return;
+ mInDozeMode = isEnabled;
+ installNewProgramLocked();
+ }
+
+ /** Find the single IPv4 LinkAddress if there is one, otherwise return null. */
+ private static LinkAddress findIPv4LinkAddress(LinkProperties lp) {
+ LinkAddress ipv4Address = null;
+ for (LinkAddress address : lp.getLinkAddresses()) {
+ if (!(address.getAddress() instanceof Inet4Address)) {
+ continue;
+ }
+ if (ipv4Address != null && !ipv4Address.isSameAddressAs(address)) {
+ // More than one IPv4 address, abort.
+ return null;
+ }
+ ipv4Address = address;
+ }
+ return ipv4Address;
+ }
+
+ public synchronized void setLinkProperties(LinkProperties lp) {
+ // NOTE: Do not keep a copy of LinkProperties as it would further duplicate state.
+ final LinkAddress ipv4Address = findIPv4LinkAddress(lp);
+ final byte[] addr = (ipv4Address != null) ? ipv4Address.getAddress().getAddress() : null;
+ final int prefix = (ipv4Address != null) ? ipv4Address.getPrefixLength() : 0;
+ if ((prefix == mIPv4PrefixLength) && Arrays.equals(addr, mIPv4Address)) {
+ return;
+ }
+ mIPv4Address = addr;
+ mIPv4PrefixLength = prefix;
+ installNewProgramLocked();
+ }
+
+ static public long counterValue(byte[] data, Counter counter)
+ throws ArrayIndexOutOfBoundsException {
+ // Follow the same wrap-around addressing scheme of the interpreter.
+ int offset = counter.offset();
+ if (offset < 0) {
+ offset = data.length + offset;
+ }
+
+ // Decode 32bit big-endian integer into a long so we can count up beyond 2^31.
+ long value = 0;
+ for (int i = 0; i < 4; i++) {
+ value = value << 8 | (data[offset] & 0xFF);
+ offset++;
+ }
+ return value;
+ }
+
+ public synchronized void dump(IndentingPrintWriter pw) {
+ pw.println("Capabilities: " + mApfCapabilities);
+ pw.println("Receive thread: " + (mReceiveThread != null ? "RUNNING" : "STOPPED"));
+ pw.println("Multicast: " + (mMulticastFilter ? "DROP" : "ALLOW"));
+ try {
+ pw.println("IPv4 address: " + InetAddress.getByAddress(mIPv4Address).getHostAddress());
+ } catch (UnknownHostException|NullPointerException e) {}
+
+ if (mLastTimeInstalledProgram == 0) {
+ pw.println("No program installed.");
+ return;
+ }
+ pw.println("Program updates: " + mNumProgramUpdates);
+ pw.println(String.format(
+ "Last program length %d, installed %ds ago, lifetime %ds",
+ mLastInstalledProgram.length, currentTimeSeconds() - mLastTimeInstalledProgram,
+ mLastInstalledProgramMinLifetime));
+
+ pw.println("RA filters:");
+ pw.increaseIndent();
+ for (Ra ra: mRas) {
+ pw.println(ra);
+ pw.increaseIndent();
+ pw.println(String.format(
+ "Seen: %d, last %ds ago", ra.seenCount, currentTimeSeconds() - ra.mLastSeen));
+ if (DBG) {
+ pw.println("Last match:");
+ pw.increaseIndent();
+ pw.println(ra.getLastMatchingPacket());
+ pw.decreaseIndent();
+ }
+ pw.decreaseIndent();
+ }
+ pw.decreaseIndent();
+
+ if (DBG) {
+ pw.println("Last program:");
+ pw.increaseIndent();
+ pw.println(HexDump.toHexString(mLastInstalledProgram, false /* lowercase */));
+ pw.decreaseIndent();
+ }
+
+ pw.println("APF packet counters: ");
+ pw.increaseIndent();
+ if (!mApfCapabilities.hasDataAccess()) {
+ pw.println("APF counters not supported");
+ } else if (mDataSnapshot == null) {
+ pw.println("No last snapshot.");
+ } else {
+ try {
+ Counter[] counters = Counter.class.getEnumConstants();
+ for (Counter c : Arrays.asList(counters).subList(1, counters.length)) {
+ long value = counterValue(mDataSnapshot, c);
+ // Only print non-zero counters
+ if (value != 0) {
+ pw.println(c.toString() + ": " + value);
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ pw.println("Uh-oh: " + e);
+ }
+ if (VDBG) {
+ pw.println("Raw data dump: ");
+ pw.println(HexDump.dumpHexString(mDataSnapshot));
+ }
+ }
+ pw.decreaseIndent();
+ }
+
+ // TODO: move to android.net.NetworkUtils
+ @VisibleForTesting
+ public static int ipv4BroadcastAddress(byte[] addrBytes, int prefixLength) {
+ return bytesToBEInt(addrBytes) | (int) (uint32(-1) >>> prefixLength);
+ }
+}
diff --git a/src/android/net/apf/ApfGenerator.java b/src/android/net/apf/ApfGenerator.java
new file mode 100644
index 0000000..87a1b5e
--- /dev/null
+++ b/src/android/net/apf/ApfGenerator.java
@@ -0,0 +1,937 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.apf;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * APF assembler/generator. A tool for generating an APF program.
+ *
+ * Call add*() functions to add instructions to the program, then call
+ * {@link generate} to get the APF bytecode for the program.
+ *
+ * @hide
+ */
+public class ApfGenerator {
+ /**
+ * This exception is thrown when an attempt is made to generate an illegal instruction.
+ */
+ public static class IllegalInstructionException extends Exception {
+ IllegalInstructionException(String msg) {
+ super(msg);
+ }
+ }
+ private enum Opcodes {
+ LABEL(-1),
+ LDB(1), // Load 1 byte from immediate offset, e.g. "ldb R0, [5]"
+ LDH(2), // Load 2 bytes from immediate offset, e.g. "ldh R0, [5]"
+ LDW(3), // Load 4 bytes from immediate offset, e.g. "ldw R0, [5]"
+ LDBX(4), // Load 1 byte from immediate offset plus register, e.g. "ldbx R0, [5]R0"
+ LDHX(5), // Load 2 byte from immediate offset plus register, e.g. "ldhx R0, [5]R0"
+ LDWX(6), // Load 4 byte from immediate offset plus register, e.g. "ldwx R0, [5]R0"
+ ADD(7), // Add, e.g. "add R0,5"
+ MUL(8), // Multiply, e.g. "mul R0,5"
+ DIV(9), // Divide, e.g. "div R0,5"
+ AND(10), // And, e.g. "and R0,5"
+ OR(11), // Or, e.g. "or R0,5"
+ SH(12), // Left shift, e.g, "sh R0, 5" or "sh R0, -5" (shifts right)
+ LI(13), // Load immediate, e.g. "li R0,5" (immediate encoded as signed value)
+ JMP(14), // Jump, e.g. "jmp label"
+ JEQ(15), // Compare equal and branch, e.g. "jeq R0,5,label"
+ JNE(16), // Compare not equal and branch, e.g. "jne R0,5,label"
+ JGT(17), // Compare greater than and branch, e.g. "jgt R0,5,label"
+ JLT(18), // Compare less than and branch, e.g. "jlt R0,5,label"
+ JSET(19), // Compare any bits set and branch, e.g. "jset R0,5,label"
+ JNEBS(20), // Compare not equal byte sequence, e.g. "jnebs R0,5,label,0x1122334455"
+ EXT(21), // Followed by immediate indicating ExtendedOpcodes.
+ LDDW(22), // Load 4 bytes from data memory address (register + immediate): "lddw R0, [5]R1"
+ STDW(23); // Store 4 bytes to data memory address (register + immediate): "stdw R0, [5]R1"
+
+ final int value;
+
+ private Opcodes(int value) {
+ this.value = value;
+ }
+ }
+ // Extended opcodes. Primary opcode is Opcodes.EXT. ExtendedOpcodes are encoded in the immediate
+ // field.
+ private enum ExtendedOpcodes {
+ LDM(0), // Load from memory, e.g. "ldm R0,5"
+ STM(16), // Store to memory, e.g. "stm R0,5"
+ NOT(32), // Not, e.g. "not R0"
+ NEG(33), // Negate, e.g. "neg R0"
+ SWAP(34), // Swap, e.g. "swap R0,R1"
+ MOVE(35); // Move, e.g. "move R0,R1"
+
+ final int value;
+
+ private ExtendedOpcodes(int value) {
+ this.value = value;
+ }
+ }
+ public enum Register {
+ R0(0),
+ R1(1);
+
+ final int value;
+
+ private Register(int value) {
+ this.value = value;
+ }
+ }
+ private class Instruction {
+ private final byte mOpcode; // A "Opcode" value.
+ private final byte mRegister; // A "Register" value.
+ private boolean mHasImm;
+ private byte mImmSize;
+ private boolean mImmSigned;
+ private int mImm;
+ // When mOpcode is a jump:
+ private byte mTargetLabelSize;
+ private String mTargetLabel;
+ // When mOpcode == Opcodes.LABEL:
+ private String mLabel;
+ // When mOpcode == Opcodes.JNEBS:
+ private byte[] mCompareBytes;
+ // Offset in bytes from the begining of this program. Set by {@link ApfGenerator#generate}.
+ int offset;
+
+ Instruction(Opcodes opcode, Register register) {
+ mOpcode = (byte)opcode.value;
+ mRegister = (byte)register.value;
+ }
+
+ Instruction(Opcodes opcode) {
+ this(opcode, Register.R0);
+ }
+
+ void setImm(int imm, boolean signed) {
+ mHasImm = true;
+ mImm = imm;
+ mImmSigned = signed;
+ mImmSize = calculateImmSize(imm, signed);
+ }
+
+ void setUnsignedImm(int imm) {
+ setImm(imm, false);
+ }
+
+ void setSignedImm(int imm) {
+ setImm(imm, true);
+ }
+
+ void setLabel(String label) throws IllegalInstructionException {
+ if (mLabels.containsKey(label)) {
+ throw new IllegalInstructionException("duplicate label " + label);
+ }
+ if (mOpcode != Opcodes.LABEL.value) {
+ throw new IllegalStateException("adding label to non-label instruction");
+ }
+ mLabel = label;
+ mLabels.put(label, this);
+ }
+
+ void setTargetLabel(String label) {
+ mTargetLabel = label;
+ mTargetLabelSize = 4; // May shrink later on in generate().
+ }
+
+ void setCompareBytes(byte[] bytes) {
+ if (mOpcode != Opcodes.JNEBS.value) {
+ throw new IllegalStateException("adding compare bytes to non-JNEBS instruction");
+ }
+ mCompareBytes = bytes;
+ }
+
+ /**
+ * @return size of instruction in bytes.
+ */
+ int size() {
+ if (mOpcode == Opcodes.LABEL.value) {
+ return 0;
+ }
+ int size = 1;
+ if (mHasImm) {
+ size += generatedImmSize();
+ }
+ if (mTargetLabel != null) {
+ size += generatedImmSize();
+ }
+ if (mCompareBytes != null) {
+ size += mCompareBytes.length;
+ }
+ return size;
+ }
+
+ /**
+ * Resize immediate value field so that it's only as big as required to
+ * contain the offset of the jump destination.
+ * @return {@code true} if shrunk.
+ */
+ boolean shrink() throws IllegalInstructionException {
+ if (mTargetLabel == null) {
+ return false;
+ }
+ int oldSize = size();
+ int oldTargetLabelSize = mTargetLabelSize;
+ mTargetLabelSize = calculateImmSize(calculateTargetLabelOffset(), false);
+ if (mTargetLabelSize > oldTargetLabelSize) {
+ throw new IllegalStateException("instruction grew");
+ }
+ return size() < oldSize;
+ }
+
+ /**
+ * Assemble value for instruction size field.
+ */
+ private byte generateImmSizeField() {
+ byte immSize = generatedImmSize();
+ // Encode size field to fit in 2 bits: 0->0, 1->1, 2->2, 3->4.
+ return immSize == 4 ? 3 : immSize;
+ }
+
+ /**
+ * Assemble first byte of generated instruction.
+ */
+ private byte generateInstructionByte() {
+ byte sizeField = generateImmSizeField();
+ return (byte)((mOpcode << 3) | (sizeField << 1) | mRegister);
+ }
+
+ /**
+ * Write {@code value} at offset {@code writingOffset} into {@code bytecode}.
+ * {@link generatedImmSize} bytes are written. {@code value} is truncated to
+ * {@code generatedImmSize} bytes. {@code value} is treated simply as a
+ * 32-bit value, so unsigned values should be zero extended and the truncation
+ * should simply throw away their zero-ed upper bits, and signed values should
+ * be sign extended and the truncation should simply throw away their signed
+ * upper bits.
+ */
+ private int writeValue(int value, byte[] bytecode, int writingOffset) {
+ for (int i = generatedImmSize() - 1; i >= 0; i--) {
+ bytecode[writingOffset++] = (byte)((value >> (i * 8)) & 255);
+ }
+ return writingOffset;
+ }
+
+ /**
+ * Generate bytecode for this instruction at offset {@link offset}.
+ */
+ void generate(byte[] bytecode) throws IllegalInstructionException {
+ if (mOpcode == Opcodes.LABEL.value) {
+ return;
+ }
+ int writingOffset = offset;
+ bytecode[writingOffset++] = generateInstructionByte();
+ if (mTargetLabel != null) {
+ writingOffset = writeValue(calculateTargetLabelOffset(), bytecode, writingOffset);
+ }
+ if (mHasImm) {
+ writingOffset = writeValue(mImm, bytecode, writingOffset);
+ }
+ if (mCompareBytes != null) {
+ System.arraycopy(mCompareBytes, 0, bytecode, writingOffset, mCompareBytes.length);
+ writingOffset += mCompareBytes.length;
+ }
+ if ((writingOffset - offset) != size()) {
+ throw new IllegalStateException("wrote " + (writingOffset - offset) +
+ " but should have written " + size());
+ }
+ }
+
+ /**
+ * Calculate the size of either the immediate field or the target label field, if either is
+ * present. Most instructions have either an immediate or a target label field, but for the
+ * instructions that have both, the size of the target label field must be the same as the
+ * size of the immediate field, because there is only one length field in the instruction
+ * byte, hence why this function simply takes the maximum of the two sizes, so neither is
+ * truncated.
+ */
+ private byte generatedImmSize() {
+ return mImmSize > mTargetLabelSize ? mImmSize : mTargetLabelSize;
+ }
+
+ private int calculateTargetLabelOffset() throws IllegalInstructionException {
+ Instruction targetLabelInstruction;
+ if (mTargetLabel == DROP_LABEL) {
+ targetLabelInstruction = mDropLabel;
+ } else if (mTargetLabel == PASS_LABEL) {
+ targetLabelInstruction = mPassLabel;
+ } else {
+ targetLabelInstruction = mLabels.get(mTargetLabel);
+ }
+ if (targetLabelInstruction == null) {
+ throw new IllegalInstructionException("label not found: " + mTargetLabel);
+ }
+ // Calculate distance from end of this instruction to instruction.offset.
+ final int targetLabelOffset = targetLabelInstruction.offset - (offset + size());
+ if (targetLabelOffset < 0) {
+ throw new IllegalInstructionException("backward branches disallowed; label: " +
+ mTargetLabel);
+ }
+ return targetLabelOffset;
+ }
+
+ private byte calculateImmSize(int imm, boolean signed) {
+ if (imm == 0) {
+ return 0;
+ }
+ if (signed && (imm >= -128 && imm <= 127) ||
+ !signed && (imm >= 0 && imm <= 255)) {
+ return 1;
+ }
+ if (signed && (imm >= -32768 && imm <= 32767) ||
+ !signed && (imm >= 0 && imm <= 65535)) {
+ return 2;
+ }
+ return 4;
+ }
+ }
+
+ /**
+ * Jump to this label to terminate the program and indicate the packet
+ * should be dropped.
+ */
+ public static final String DROP_LABEL = "__DROP__";
+
+ /**
+ * Jump to this label to terminate the program and indicate the packet
+ * should be passed to the AP.
+ */
+ public static final String PASS_LABEL = "__PASS__";
+
+ /**
+ * Number of memory slots available for access via APF stores to memory and loads from memory.
+ * The memory slots are numbered 0 to {@code MEMORY_SLOTS} - 1. This must be kept in sync with
+ * the APF interpreter.
+ */
+ public static final int MEMORY_SLOTS = 16;
+
+ /**
+ * Memory slot number that is prefilled with the IPv4 header length.
+ * Note that this memory slot may be overwritten by a program that
+ * executes stores to this memory slot. This must be kept in sync with
+ * the APF interpreter.
+ */
+ public static final int IPV4_HEADER_SIZE_MEMORY_SLOT = 13;
+
+ /**
+ * Memory slot number that is prefilled with the size of the packet being filtered in bytes.
+ * Note that this memory slot may be overwritten by a program that
+ * executes stores to this memory slot. This must be kept in sync with the APF interpreter.
+ */
+ public static final int PACKET_SIZE_MEMORY_SLOT = 14;
+
+ /**
+ * Memory slot number that is prefilled with the age of the filter in seconds. The age of the
+ * filter is the time since the filter was installed until now.
+ * Note that this memory slot may be overwritten by a program that
+ * executes stores to this memory slot. This must be kept in sync with the APF interpreter.
+ */
+ public static final int FILTER_AGE_MEMORY_SLOT = 15;
+
+ /**
+ * First memory slot containing prefilled values. Can be used in range comparisons to determine
+ * if memory slot index is within prefilled slots.
+ */
+ public static final int FIRST_PREFILLED_MEMORY_SLOT = IPV4_HEADER_SIZE_MEMORY_SLOT;
+
+ /**
+ * Last memory slot containing prefilled values. Can be used in range comparisons to determine
+ * if memory slot index is within prefilled slots.
+ */
+ public static final int LAST_PREFILLED_MEMORY_SLOT = FILTER_AGE_MEMORY_SLOT;
+
+ // This version number syncs up with APF_VERSION in hardware/google/apf/apf_interpreter.h
+ private static final int MIN_APF_VERSION = 2;
+
+ private final ArrayList<Instruction> mInstructions = new ArrayList<Instruction>();
+ private final HashMap<String, Instruction> mLabels = new HashMap<String, Instruction>();
+ private final Instruction mDropLabel = new Instruction(Opcodes.LABEL);
+ private final Instruction mPassLabel = new Instruction(Opcodes.LABEL);
+ private final int mVersion;
+ private boolean mGenerated;
+
+ /**
+ * Creates an ApfGenerator instance which is able to emit instructions for the specified
+ * {@code version} of the APF interpreter. Throws {@code IllegalInstructionException} if
+ * the requested version is unsupported.
+ */
+ ApfGenerator(int version) throws IllegalInstructionException {
+ mVersion = version;
+ requireApfVersion(MIN_APF_VERSION);
+ }
+
+ /**
+ * Returns true if the ApfGenerator supports the specified {@code version}, otherwise false.
+ */
+ public static boolean supportsVersion(int version) {
+ return version >= MIN_APF_VERSION;
+ }
+
+ private void requireApfVersion(int minimumVersion) throws IllegalInstructionException {
+ if (mVersion < minimumVersion) {
+ throw new IllegalInstructionException("Requires APF >= " + minimumVersion);
+ }
+ }
+
+ private void addInstruction(Instruction instruction) {
+ if (mGenerated) {
+ throw new IllegalStateException("Program already generated");
+ }
+ mInstructions.add(instruction);
+ }
+
+ /**
+ * Define a label at the current end of the program. Jumps can jump to this label. Labels are
+ * their own separate instructions, though with size 0. This facilitates having labels with
+ * no corresponding code to execute, for example a label at the end of a program. For example
+ * an {@link ApfGenerator} might be passed to a function that adds a filter like so:
+ * <pre>
+ * load from packet
+ * compare loaded data, jump if not equal to "next_filter"
+ * load from packet
+ * compare loaded data, jump if not equal to "next_filter"
+ * jump to drop label
+ * define "next_filter" here
+ * </pre>
+ * In this case "next_filter" may not have any generated code associated with it.
+ */
+ public ApfGenerator defineLabel(String name) throws IllegalInstructionException {
+ Instruction instruction = new Instruction(Opcodes.LABEL);
+ instruction.setLabel(name);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an unconditional jump instruction to the end of the program.
+ */
+ public ApfGenerator addJump(String target) {
+ Instruction instruction = new Instruction(Opcodes.JMP);
+ instruction.setTargetLabel(target);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to load the byte at offset {@code offset}
+ * bytes from the begining of the packet into {@code register}.
+ */
+ public ApfGenerator addLoad8(Register register, int offset) {
+ Instruction instruction = new Instruction(Opcodes.LDB, register);
+ instruction.setUnsignedImm(offset);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to load 16-bits at offset {@code offset}
+ * bytes from the begining of the packet into {@code register}.
+ */
+ public ApfGenerator addLoad16(Register register, int offset) {
+ Instruction instruction = new Instruction(Opcodes.LDH, register);
+ instruction.setUnsignedImm(offset);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to load 32-bits at offset {@code offset}
+ * bytes from the begining of the packet into {@code register}.
+ */
+ public ApfGenerator addLoad32(Register register, int offset) {
+ Instruction instruction = new Instruction(Opcodes.LDW, register);
+ instruction.setUnsignedImm(offset);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to load a byte from the packet into
+ * {@code register}. The offset of the loaded byte from the begining of the packet is
+ * the sum of {@code offset} and the value in register R1.
+ */
+ public ApfGenerator addLoad8Indexed(Register register, int offset) {
+ Instruction instruction = new Instruction(Opcodes.LDBX, register);
+ instruction.setUnsignedImm(offset);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to load 16-bits from the packet into
+ * {@code register}. The offset of the loaded 16-bits from the begining of the packet is
+ * the sum of {@code offset} and the value in register R1.
+ */
+ public ApfGenerator addLoad16Indexed(Register register, int offset) {
+ Instruction instruction = new Instruction(Opcodes.LDHX, register);
+ instruction.setUnsignedImm(offset);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to load 32-bits from the packet into
+ * {@code register}. The offset of the loaded 32-bits from the begining of the packet is
+ * the sum of {@code offset} and the value in register R1.
+ */
+ public ApfGenerator addLoad32Indexed(Register register, int offset) {
+ Instruction instruction = new Instruction(Opcodes.LDWX, register);
+ instruction.setUnsignedImm(offset);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to add {@code value} to register R0.
+ */
+ public ApfGenerator addAdd(int value) {
+ Instruction instruction = new Instruction(Opcodes.ADD);
+ instruction.setSignedImm(value);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to multiply register R0 by {@code value}.
+ */
+ public ApfGenerator addMul(int value) {
+ Instruction instruction = new Instruction(Opcodes.MUL);
+ instruction.setSignedImm(value);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to divide register R0 by {@code value}.
+ */
+ public ApfGenerator addDiv(int value) {
+ Instruction instruction = new Instruction(Opcodes.DIV);
+ instruction.setSignedImm(value);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to logically and register R0 with {@code value}.
+ */
+ public ApfGenerator addAnd(int value) {
+ Instruction instruction = new Instruction(Opcodes.AND);
+ instruction.setUnsignedImm(value);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to logically or register R0 with {@code value}.
+ */
+ public ApfGenerator addOr(int value) {
+ Instruction instruction = new Instruction(Opcodes.OR);
+ instruction.setUnsignedImm(value);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to shift left register R0 by {@code value} bits.
+ */
+ public ApfGenerator addLeftShift(int value) {
+ Instruction instruction = new Instruction(Opcodes.SH);
+ instruction.setSignedImm(value);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to shift right register R0 by {@code value}
+ * bits.
+ */
+ public ApfGenerator addRightShift(int value) {
+ Instruction instruction = new Instruction(Opcodes.SH);
+ instruction.setSignedImm(-value);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to add register R1 to register R0.
+ */
+ public ApfGenerator addAddR1() {
+ Instruction instruction = new Instruction(Opcodes.ADD, Register.R1);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to multiply register R0 by register R1.
+ */
+ public ApfGenerator addMulR1() {
+ Instruction instruction = new Instruction(Opcodes.MUL, Register.R1);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to divide register R0 by register R1.
+ */
+ public ApfGenerator addDivR1() {
+ Instruction instruction = new Instruction(Opcodes.DIV, Register.R1);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to logically and register R0 with register R1
+ * and store the result back into register R0.
+ */
+ public ApfGenerator addAndR1() {
+ Instruction instruction = new Instruction(Opcodes.AND, Register.R1);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to logically or register R0 with register R1
+ * and store the result back into register R0.
+ */
+ public ApfGenerator addOrR1() {
+ Instruction instruction = new Instruction(Opcodes.OR, Register.R1);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to shift register R0 left by the value in
+ * register R1.
+ */
+ public ApfGenerator addLeftShiftR1() {
+ Instruction instruction = new Instruction(Opcodes.SH, Register.R1);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to move {@code value} into {@code register}.
+ */
+ public ApfGenerator addLoadImmediate(Register register, int value) {
+ Instruction instruction = new Instruction(Opcodes.LI, register);
+ instruction.setSignedImm(value);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to jump to {@code target} if register R0's
+ * value equals {@code value}.
+ */
+ public ApfGenerator addJumpIfR0Equals(int value, String target) {
+ Instruction instruction = new Instruction(Opcodes.JEQ);
+ instruction.setUnsignedImm(value);
+ instruction.setTargetLabel(target);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to jump to {@code target} if register R0's
+ * value does not equal {@code value}.
+ */
+ public ApfGenerator addJumpIfR0NotEquals(int value, String target) {
+ Instruction instruction = new Instruction(Opcodes.JNE);
+ instruction.setUnsignedImm(value);
+ instruction.setTargetLabel(target);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to jump to {@code target} if register R0's
+ * value is greater than {@code value}.
+ */
+ public ApfGenerator addJumpIfR0GreaterThan(int value, String target) {
+ Instruction instruction = new Instruction(Opcodes.JGT);
+ instruction.setUnsignedImm(value);
+ instruction.setTargetLabel(target);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to jump to {@code target} if register R0's
+ * value is less than {@code value}.
+ */
+ public ApfGenerator addJumpIfR0LessThan(int value, String target) {
+ Instruction instruction = new Instruction(Opcodes.JLT);
+ instruction.setUnsignedImm(value);
+ instruction.setTargetLabel(target);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to jump to {@code target} if register R0's
+ * value has any bits set that are also set in {@code value}.
+ */
+ public ApfGenerator addJumpIfR0AnyBitsSet(int value, String target) {
+ Instruction instruction = new Instruction(Opcodes.JSET);
+ instruction.setUnsignedImm(value);
+ instruction.setTargetLabel(target);
+ addInstruction(instruction);
+ return this;
+ }
+ /**
+ * Add an instruction to the end of the program to jump to {@code target} if register R0's
+ * value equals register R1's value.
+ */
+ public ApfGenerator addJumpIfR0EqualsR1(String target) {
+ Instruction instruction = new Instruction(Opcodes.JEQ, Register.R1);
+ instruction.setTargetLabel(target);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to jump to {@code target} if register R0's
+ * value does not equal register R1's value.
+ */
+ public ApfGenerator addJumpIfR0NotEqualsR1(String target) {
+ Instruction instruction = new Instruction(Opcodes.JNE, Register.R1);
+ instruction.setTargetLabel(target);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to jump to {@code target} if register R0's
+ * value is greater than register R1's value.
+ */
+ public ApfGenerator addJumpIfR0GreaterThanR1(String target) {
+ Instruction instruction = new Instruction(Opcodes.JGT, Register.R1);
+ instruction.setTargetLabel(target);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to jump to {@code target} if register R0's
+ * value is less than register R1's value.
+ */
+ public ApfGenerator addJumpIfR0LessThanR1(String target) {
+ Instruction instruction = new Instruction(Opcodes.JLT, Register.R1);
+ instruction.setTargetLabel(target);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to jump to {@code target} if register R0's
+ * value has any bits set that are also set in R1's value.
+ */
+ public ApfGenerator addJumpIfR0AnyBitsSetR1(String target) {
+ Instruction instruction = new Instruction(Opcodes.JSET, Register.R1);
+ instruction.setTargetLabel(target);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to jump to {@code target} if the bytes of the
+ * packet at an offset specified by {@code register} match {@code bytes}.
+ */
+ public ApfGenerator addJumpIfBytesNotEqual(Register register, byte[] bytes, String target)
+ throws IllegalInstructionException {
+ if (register == Register.R1) {
+ throw new IllegalInstructionException("JNEBS fails with R1");
+ }
+ Instruction instruction = new Instruction(Opcodes.JNEBS, register);
+ instruction.setUnsignedImm(bytes.length);
+ instruction.setTargetLabel(target);
+ instruction.setCompareBytes(bytes);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to load memory slot {@code slot} into
+ * {@code register}.
+ */
+ public ApfGenerator addLoadFromMemory(Register register, int slot)
+ throws IllegalInstructionException {
+ if (slot < 0 || slot > (MEMORY_SLOTS - 1)) {
+ throw new IllegalInstructionException("illegal memory slot number: " + slot);
+ }
+ Instruction instruction = new Instruction(Opcodes.EXT, register);
+ instruction.setUnsignedImm(ExtendedOpcodes.LDM.value + slot);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to store {@code register} into memory slot
+ * {@code slot}.
+ */
+ public ApfGenerator addStoreToMemory(Register register, int slot)
+ throws IllegalInstructionException {
+ if (slot < 0 || slot > (MEMORY_SLOTS - 1)) {
+ throw new IllegalInstructionException("illegal memory slot number: " + slot);
+ }
+ Instruction instruction = new Instruction(Opcodes.EXT, register);
+ instruction.setUnsignedImm(ExtendedOpcodes.STM.value + slot);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to logically not {@code register}.
+ */
+ public ApfGenerator addNot(Register register) {
+ Instruction instruction = new Instruction(Opcodes.EXT, register);
+ instruction.setUnsignedImm(ExtendedOpcodes.NOT.value);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to negate {@code register}.
+ */
+ public ApfGenerator addNeg(Register register) {
+ Instruction instruction = new Instruction(Opcodes.EXT, register);
+ instruction.setUnsignedImm(ExtendedOpcodes.NEG.value);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to swap the values in register R0 and register R1.
+ */
+ public ApfGenerator addSwap() {
+ Instruction instruction = new Instruction(Opcodes.EXT);
+ instruction.setUnsignedImm(ExtendedOpcodes.SWAP.value);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to move the value into
+ * {@code register} from the other register.
+ */
+ public ApfGenerator addMove(Register register) {
+ Instruction instruction = new Instruction(Opcodes.EXT, register);
+ instruction.setUnsignedImm(ExtendedOpcodes.MOVE.value);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to load 32 bits from the data memory into
+ * {@code register}. The source address is computed by adding the signed immediate
+ * @{code offset} to the other register.
+ * Requires APF v3 or greater.
+ */
+ public ApfGenerator addLoadData(Register destinationRegister, int offset)
+ throws IllegalInstructionException {
+ requireApfVersion(3);
+ Instruction instruction = new Instruction(Opcodes.LDDW, destinationRegister);
+ instruction.setSignedImm(offset);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Add an instruction to the end of the program to store 32 bits from {@code register} into the
+ * data memory. The destination address is computed by adding the signed immediate
+ * @{code offset} to the other register.
+ * Requires APF v3 or greater.
+ */
+ public ApfGenerator addStoreData(Register sourceRegister, int offset)
+ throws IllegalInstructionException {
+ requireApfVersion(3);
+ Instruction instruction = new Instruction(Opcodes.STDW, sourceRegister);
+ instruction.setSignedImm(offset);
+ addInstruction(instruction);
+ return this;
+ }
+
+ /**
+ * Updates instruction offset fields using latest instruction sizes.
+ * @return current program length in bytes.
+ */
+ private int updateInstructionOffsets() {
+ int offset = 0;
+ for (Instruction instruction : mInstructions) {
+ instruction.offset = offset;
+ offset += instruction.size();
+ }
+ return offset;
+ }
+
+ /**
+ * Returns an overestimate of the size of the generated program. {@link #generate} may return
+ * a program that is smaller.
+ */
+ public int programLengthOverEstimate() {
+ return updateInstructionOffsets();
+ }
+
+ /**
+ * Generate the bytecode for the APF program.
+ * @return the bytecode.
+ * @throws IllegalStateException if a label is referenced but not defined.
+ */
+ public byte[] generate() throws IllegalInstructionException {
+ // Enforce that we can only generate once because we cannot unshrink instructions and
+ // PASS/DROP labels may move further away requiring unshrinking if we add further
+ // instructions.
+ if (mGenerated) {
+ throw new IllegalStateException("Can only generate() once!");
+ }
+ mGenerated = true;
+ int total_size;
+ boolean shrunk;
+ // Shrink the immediate value fields of instructions.
+ // As we shrink the instructions some branch offset
+ // fields may shrink also, thereby shrinking the
+ // instructions further. Loop until we've reached the
+ // minimum size. Rarely will this loop more than a few times.
+ // Limit iterations to avoid O(n^2) behavior.
+ int iterations_remaining = 10;
+ do {
+ total_size = updateInstructionOffsets();
+ // Update drop and pass label offsets.
+ mDropLabel.offset = total_size + 1;
+ mPassLabel.offset = total_size;
+ // Limit run-time in aberant circumstances.
+ if (iterations_remaining-- == 0) break;
+ // Attempt to shrink instructions.
+ shrunk = false;
+ for (Instruction instruction : mInstructions) {
+ if (instruction.shrink()) {
+ shrunk = true;
+ }
+ }
+ } while (shrunk);
+ // Generate bytecode for instructions.
+ byte[] bytecode = new byte[total_size];
+ for (Instruction instruction : mInstructions) {
+ instruction.generate(bytecode);
+ }
+ return bytecode;
+ }
+}
+
diff --git a/src/android/net/dhcp/DhcpAckPacket.java b/src/android/net/dhcp/DhcpAckPacket.java
new file mode 100644
index 0000000..b2eb4e2
--- /dev/null
+++ b/src/android/net/dhcp/DhcpAckPacket.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2010 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.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the DHCP-ACK packet.
+ */
+class DhcpAckPacket extends DhcpPacket {
+
+ /**
+ * The address of the server which sent this packet.
+ */
+ private final Inet4Address mSrcIp;
+
+ DhcpAckPacket(int transId, short secs, boolean broadcast, Inet4Address serverAddress,
+ Inet4Address relayIp, Inet4Address clientIp, Inet4Address yourIp, byte[] clientMac) {
+ super(transId, secs, clientIp, yourIp, serverAddress, relayIp, clientMac, broadcast);
+ mBroadcast = broadcast;
+ mSrcIp = serverAddress;
+ }
+
+ public String toString() {
+ String s = super.toString();
+ String dnsServers = " DNS servers: ";
+
+ for (Inet4Address dnsServer: mDnsServers) {
+ dnsServers += dnsServer.toString() + " ";
+ }
+
+ return s + " ACK: your new IP " + mYourIp +
+ ", netmask " + mSubnetMask +
+ ", gateways " + mGateways + dnsServers +
+ ", lease time " + mLeaseTime;
+ }
+
+ /**
+ * Fills in a packet with the requested ACK parameters.
+ */
+ public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+ ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+ Inet4Address destIp = mBroadcast ? INADDR_BROADCAST : mYourIp;
+ Inet4Address srcIp = mBroadcast ? INADDR_ANY : mSrcIp;
+
+ fillInPacket(encap, destIp, srcIp, destUdp, srcUdp, result,
+ DHCP_BOOTREPLY, mBroadcast);
+ result.flip();
+ return result;
+ }
+
+ /**
+ * Adds the optional parameters to the client-generated ACK packet.
+ */
+ void finishPacket(ByteBuffer buffer) {
+ addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_ACK);
+ addTlv(buffer, DHCP_SERVER_IDENTIFIER, mServerIdentifier);
+
+ addCommonServerTlvs(buffer);
+ addTlvEnd(buffer);
+ }
+
+ /**
+ * Un-boxes an Integer, returning 0 if a null reference is supplied.
+ */
+ private static final int getInt(Integer v) {
+ if (v == null) {
+ return 0;
+ } else {
+ return v.intValue();
+ }
+ }
+}
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java
new file mode 100644
index 0000000..04ac9a3
--- /dev/null
+++ b/src/android/net/dhcp/DhcpClient.java
@@ -0,0 +1,1052 @@
+/*
+ * 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.dhcp;
+
+import static android.net.dhcp.DhcpPacket.DHCP_BROADCAST_ADDRESS;
+import static android.net.dhcp.DhcpPacket.DHCP_DNS_SERVER;
+import static android.net.dhcp.DhcpPacket.DHCP_DOMAIN_NAME;
+import static android.net.dhcp.DhcpPacket.DHCP_LEASE_TIME;
+import static android.net.dhcp.DhcpPacket.DHCP_MTU;
+import static android.net.dhcp.DhcpPacket.DHCP_REBINDING_TIME;
+import static android.net.dhcp.DhcpPacket.DHCP_RENEWAL_TIME;
+import static android.net.dhcp.DhcpPacket.DHCP_ROUTER;
+import static android.net.dhcp.DhcpPacket.DHCP_SUBNET_MASK;
+import static android.net.dhcp.DhcpPacket.DHCP_VENDOR_INFO;
+import static android.net.dhcp.DhcpPacket.INADDR_ANY;
+import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST;
+import static android.net.util.SocketUtils.makePacketSocketAddress;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_RAW;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_BROADCAST;
+import static android.system.OsConstants.SO_RCVBUF;
+import static android.system.OsConstants.SO_REUSEADDR;
+
+import android.content.Context;
+import android.net.DhcpResults;
+import android.net.NetworkUtils;
+import android.net.TrafficStats;
+import android.net.ip.IpClient;
+import android.net.metrics.DhcpClientEvent;
+import android.net.metrics.DhcpErrorEvent;
+import android.net.metrics.IpConnectivityLog;
+import android.net.util.InterfaceParams;
+import android.net.util.SocketUtils;
+import android.os.Message;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.util.HexDump;
+import com.android.internal.util.MessageUtils;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.internal.util.WakeupMessage;
+
+import libcore.io.IoBridge;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * A DHCPv4 client.
+ *
+ * Written to behave similarly to the DhcpStateMachine + dhcpcd 5.5.6 combination used in Android
+ * 5.1 and below, as configured on Nexus 6. The interface is the same as DhcpStateMachine.
+ *
+ * TODO:
+ *
+ * - Exponential backoff when receiving NAKs (not specified by the RFC, but current behaviour).
+ * - Support persisting lease state and support INIT-REBOOT. Android 5.1 does this, but it does not
+ * do so correctly: instead of requesting the lease last obtained on a particular network (e.g., a
+ * given SSID), it requests the last-leased IP address on the same interface, causing a delay if
+ * the server NAKs or a timeout if it doesn't.
+ *
+ * Known differences from current behaviour:
+ *
+ * - Does not request the "static routes" option.
+ * - Does not support BOOTP servers. DHCP has been around since 1993, should be everywhere now.
+ * - Requests the "broadcast" option, but does nothing with it.
+ * - Rejects invalid subnet masks such as 255.255.255.1 (current code treats that as 255.255.255.0).
+ *
+ * @hide
+ */
+public class DhcpClient extends StateMachine {
+
+ private static final String TAG = "DhcpClient";
+ private static final boolean DBG = true;
+ private static final boolean STATE_DBG = false;
+ private static final boolean MSG_DBG = false;
+ private static final boolean PACKET_DBG = false;
+
+ // Timers and timeouts.
+ private static final int SECONDS = 1000;
+ private static final int FIRST_TIMEOUT_MS = 2 * SECONDS;
+ private static final int MAX_TIMEOUT_MS = 128 * SECONDS;
+
+ // This is not strictly needed, since the client is asynchronous and implements exponential
+ // backoff. It's maintained for backwards compatibility with the previous DHCP code, which was
+ // a blocking operation with a 30-second timeout. We pick 36 seconds so we can send packets at
+ // t=0, t=2, t=6, t=14, t=30, allowing for 10% jitter.
+ private static final int DHCP_TIMEOUT_MS = 36 * SECONDS;
+
+ // DhcpClient uses IpClient's handler.
+ private static final int PUBLIC_BASE = IpClient.DHCPCLIENT_CMD_BASE;
+
+ /* Commands from controller to start/stop DHCP */
+ public static final int CMD_START_DHCP = PUBLIC_BASE + 1;
+ public static final int CMD_STOP_DHCP = PUBLIC_BASE + 2;
+
+ /* Notification from DHCP state machine prior to DHCP discovery/renewal */
+ public static final int CMD_PRE_DHCP_ACTION = PUBLIC_BASE + 3;
+ /* Notification from DHCP state machine post DHCP discovery/renewal. Indicates
+ * success/failure */
+ public static final int CMD_POST_DHCP_ACTION = PUBLIC_BASE + 4;
+ /* Notification from DHCP state machine before quitting */
+ public static final int CMD_ON_QUIT = PUBLIC_BASE + 5;
+
+ /* Command from controller to indicate DHCP discovery/renewal can continue
+ * after pre DHCP action is complete */
+ public static final int CMD_PRE_DHCP_ACTION_COMPLETE = PUBLIC_BASE + 6;
+
+ /* Command and event notification to/from IpManager requesting the setting
+ * (or clearing) of an IPv4 LinkAddress.
+ */
+ public static final int CMD_CLEAR_LINKADDRESS = PUBLIC_BASE + 7;
+ public static final int CMD_CONFIGURE_LINKADDRESS = PUBLIC_BASE + 8;
+ public static final int EVENT_LINKADDRESS_CONFIGURED = PUBLIC_BASE + 9;
+
+ /* Message.arg1 arguments to CMD_POST_DHCP_ACTION notification */
+ public static final int DHCP_SUCCESS = 1;
+ public static final int DHCP_FAILURE = 2;
+
+ // Internal messages.
+ private static final int PRIVATE_BASE = IpClient.DHCPCLIENT_CMD_BASE + 100;
+ private static final int CMD_KICK = PRIVATE_BASE + 1;
+ private static final int CMD_RECEIVED_PACKET = PRIVATE_BASE + 2;
+ private static final int CMD_TIMEOUT = PRIVATE_BASE + 3;
+ private static final int CMD_RENEW_DHCP = PRIVATE_BASE + 4;
+ private static final int CMD_REBIND_DHCP = PRIVATE_BASE + 5;
+ private static final int CMD_EXPIRE_DHCP = PRIVATE_BASE + 6;
+
+ // For message logging.
+ private static final Class[] sMessageClasses = { DhcpClient.class };
+ private static final SparseArray<String> sMessageNames =
+ MessageUtils.findMessageNames(sMessageClasses);
+
+ // DHCP parameters that we request.
+ /* package */ static final byte[] REQUESTED_PARAMS = new byte[] {
+ DHCP_SUBNET_MASK,
+ DHCP_ROUTER,
+ DHCP_DNS_SERVER,
+ DHCP_DOMAIN_NAME,
+ DHCP_MTU,
+ DHCP_BROADCAST_ADDRESS, // TODO: currently ignored.
+ DHCP_LEASE_TIME,
+ DHCP_RENEWAL_TIME,
+ DHCP_REBINDING_TIME,
+ DHCP_VENDOR_INFO,
+ };
+
+ // DHCP flag that means "yes, we support unicast."
+ private static final boolean DO_UNICAST = false;
+
+ // System services / libraries we use.
+ private final Context mContext;
+ private final Random mRandom;
+ private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
+
+ // Sockets.
+ // - We use a packet socket to receive, because servers send us packets bound for IP addresses
+ // which we have not yet configured, and the kernel protocol stack drops these.
+ // - We use a UDP socket to send, so the kernel handles ARP and routing for us (DHCP servers can
+ // be off-link as well as on-link).
+ private FileDescriptor mPacketSock;
+ private FileDescriptor mUdpSock;
+ private ReceiveThread mReceiveThread;
+
+ // State variables.
+ private final StateMachine mController;
+ private final WakeupMessage mKickAlarm;
+ private final WakeupMessage mTimeoutAlarm;
+ private final WakeupMessage mRenewAlarm;
+ private final WakeupMessage mRebindAlarm;
+ private final WakeupMessage mExpiryAlarm;
+ private final String mIfaceName;
+
+ private boolean mRegisteredForPreDhcpNotification;
+ private InterfaceParams mIface;
+ // TODO: MacAddress-ify more of this class hierarchy.
+ private byte[] mHwAddr;
+ private SocketAddress mInterfaceBroadcastAddr;
+ private int mTransactionId;
+ private long mTransactionStartMillis;
+ private DhcpResults mDhcpLease;
+ private long mDhcpLeaseExpiry;
+ private DhcpResults mOffer;
+
+ // Milliseconds SystemClock timestamps used to record transition times to DhcpBoundState.
+ private long mLastInitEnterTime;
+ private long mLastBoundExitTime;
+
+ // States.
+ private State mStoppedState = new StoppedState();
+ private State mDhcpState = new DhcpState();
+ private State mDhcpInitState = new DhcpInitState();
+ private State mDhcpSelectingState = new DhcpSelectingState();
+ private State mDhcpRequestingState = new DhcpRequestingState();
+ private State mDhcpHaveLeaseState = new DhcpHaveLeaseState();
+ private State mConfiguringInterfaceState = new ConfiguringInterfaceState();
+ private State mDhcpBoundState = new DhcpBoundState();
+ private State mDhcpRenewingState = new DhcpRenewingState();
+ private State mDhcpRebindingState = new DhcpRebindingState();
+ private State mDhcpInitRebootState = new DhcpInitRebootState();
+ private State mDhcpRebootingState = new DhcpRebootingState();
+ private State mWaitBeforeStartState = new WaitBeforeStartState(mDhcpInitState);
+ private State mWaitBeforeRenewalState = new WaitBeforeRenewalState(mDhcpRenewingState);
+
+ private WakeupMessage makeWakeupMessage(String cmdName, int cmd) {
+ cmdName = DhcpClient.class.getSimpleName() + "." + mIfaceName + "." + cmdName;
+ return new WakeupMessage(mContext, getHandler(), cmdName, cmd);
+ }
+
+ // TODO: Take an InterfaceParams instance instead of an interface name String.
+ private DhcpClient(Context context, StateMachine controller, String iface) {
+ super(TAG, controller.getHandler());
+
+ mContext = context;
+ mController = controller;
+ mIfaceName = iface;
+
+ addState(mStoppedState);
+ addState(mDhcpState);
+ addState(mDhcpInitState, mDhcpState);
+ addState(mWaitBeforeStartState, mDhcpState);
+ addState(mDhcpSelectingState, mDhcpState);
+ addState(mDhcpRequestingState, mDhcpState);
+ addState(mDhcpHaveLeaseState, mDhcpState);
+ addState(mConfiguringInterfaceState, mDhcpHaveLeaseState);
+ addState(mDhcpBoundState, mDhcpHaveLeaseState);
+ addState(mWaitBeforeRenewalState, mDhcpHaveLeaseState);
+ addState(mDhcpRenewingState, mDhcpHaveLeaseState);
+ addState(mDhcpRebindingState, mDhcpHaveLeaseState);
+ addState(mDhcpInitRebootState, mDhcpState);
+ addState(mDhcpRebootingState, mDhcpState);
+
+ setInitialState(mStoppedState);
+
+ mRandom = new Random();
+
+ // Used to schedule packet retransmissions.
+ mKickAlarm = makeWakeupMessage("KICK", CMD_KICK);
+ // Used to time out PacketRetransmittingStates.
+ mTimeoutAlarm = makeWakeupMessage("TIMEOUT", CMD_TIMEOUT);
+ // Used to schedule DHCP reacquisition.
+ mRenewAlarm = makeWakeupMessage("RENEW", CMD_RENEW_DHCP);
+ mRebindAlarm = makeWakeupMessage("REBIND", CMD_REBIND_DHCP);
+ mExpiryAlarm = makeWakeupMessage("EXPIRY", CMD_EXPIRE_DHCP);
+ }
+
+ public void registerForPreDhcpNotification() {
+ mRegisteredForPreDhcpNotification = true;
+ }
+
+ public static DhcpClient makeDhcpClient(
+ Context context, StateMachine controller, InterfaceParams ifParams) {
+ DhcpClient client = new DhcpClient(context, controller, ifParams.name);
+ client.mIface = ifParams;
+ client.start();
+ return client;
+ }
+
+ private boolean initInterface() {
+ if (mIface == null) mIface = InterfaceParams.getByName(mIfaceName);
+ if (mIface == null) {
+ Log.e(TAG, "Can't determine InterfaceParams for " + mIfaceName);
+ return false;
+ }
+
+ mHwAddr = mIface.macAddr.toByteArray();
+ mInterfaceBroadcastAddr = makePacketSocketAddress(mIface.index, DhcpPacket.ETHER_BROADCAST);
+ return true;
+ }
+
+ private void startNewTransaction() {
+ mTransactionId = mRandom.nextInt();
+ mTransactionStartMillis = SystemClock.elapsedRealtime();
+ }
+
+ private boolean initSockets() {
+ return initPacketSocket() && initUdpSocket();
+ }
+
+ private boolean initPacketSocket() {
+ try {
+ mPacketSock = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IP);
+ SocketAddress addr = makePacketSocketAddress((short) ETH_P_IP, mIface.index);
+ Os.bind(mPacketSock, addr);
+ NetworkUtils.attachDhcpFilter(mPacketSock);
+ } catch(SocketException|ErrnoException e) {
+ Log.e(TAG, "Error creating packet socket", e);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean initUdpSocket() {
+ final int oldTag = TrafficStats.getAndSetThreadStatsTag(TrafficStats.TAG_SYSTEM_DHCP);
+ try {
+ mUdpSock = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+ SocketUtils.bindSocketToInterface(mUdpSock, mIfaceName);
+ Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_REUSEADDR, 1);
+ Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_BROADCAST, 1);
+ Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_RCVBUF, 0);
+ Os.bind(mUdpSock, Inet4Address.ANY, DhcpPacket.DHCP_CLIENT);
+ } catch(SocketException|ErrnoException e) {
+ Log.e(TAG, "Error creating UDP socket", e);
+ return false;
+ } finally {
+ TrafficStats.setThreadStatsTag(oldTag);
+ }
+ return true;
+ }
+
+ private boolean connectUdpSock(Inet4Address to) {
+ try {
+ Os.connect(mUdpSock, to, DhcpPacket.DHCP_SERVER);
+ return true;
+ } catch (SocketException|ErrnoException e) {
+ Log.e(TAG, "Error connecting UDP socket", e);
+ return false;
+ }
+ }
+
+ private static void closeQuietly(FileDescriptor fd) {
+ try {
+ IoBridge.closeAndSignalBlockedThreads(fd);
+ } catch (IOException ignored) {}
+ }
+
+ private void closeSockets() {
+ closeQuietly(mUdpSock);
+ closeQuietly(mPacketSock);
+ }
+
+ class ReceiveThread extends Thread {
+
+ private final byte[] mPacket = new byte[DhcpPacket.MAX_LENGTH];
+ private volatile boolean mStopped = false;
+
+ public void halt() {
+ mStopped = true;
+ closeSockets(); // Interrupts the read() call the thread is blocked in.
+ }
+
+ @Override
+ public void run() {
+ if (DBG) Log.d(TAG, "Receive thread started");
+ while (!mStopped) {
+ int length = 0; // Or compiler can't tell it's initialized if a parse error occurs.
+ try {
+ length = Os.read(mPacketSock, mPacket, 0, mPacket.length);
+ DhcpPacket packet = null;
+ packet = DhcpPacket.decodeFullPacket(mPacket, length, DhcpPacket.ENCAP_L2);
+ if (DBG) Log.d(TAG, "Received packet: " + packet);
+ sendMessage(CMD_RECEIVED_PACKET, packet);
+ } catch (IOException|ErrnoException e) {
+ if (!mStopped) {
+ Log.e(TAG, "Read error", e);
+ logError(DhcpErrorEvent.RECEIVE_ERROR);
+ }
+ } catch (DhcpPacket.ParseException e) {
+ Log.e(TAG, "Can't parse packet: " + e.getMessage());
+ if (PACKET_DBG) {
+ Log.d(TAG, HexDump.dumpHexString(mPacket, 0, length));
+ }
+ if (e.errorCode == DhcpErrorEvent.DHCP_NO_COOKIE) {
+ int snetTagId = 0x534e4554;
+ String bugId = "31850211";
+ int uid = -1;
+ String data = DhcpPacket.ParseException.class.getName();
+ EventLog.writeEvent(snetTagId, bugId, uid, data);
+ }
+ logError(e.errorCode);
+ }
+ }
+ if (DBG) Log.d(TAG, "Receive thread stopped");
+ }
+ }
+
+ private short getSecs() {
+ return (short) ((SystemClock.elapsedRealtime() - mTransactionStartMillis) / 1000);
+ }
+
+ private boolean transmitPacket(ByteBuffer buf, String description, int encap, Inet4Address to) {
+ try {
+ if (encap == DhcpPacket.ENCAP_L2) {
+ if (DBG) Log.d(TAG, "Broadcasting " + description);
+ Os.sendto(mPacketSock, buf.array(), 0, buf.limit(), 0, mInterfaceBroadcastAddr);
+ } else if (encap == DhcpPacket.ENCAP_BOOTP && to.equals(INADDR_BROADCAST)) {
+ if (DBG) Log.d(TAG, "Broadcasting " + description);
+ // We only send L3-encapped broadcasts in DhcpRebindingState,
+ // where we have an IP address and an unconnected UDP socket.
+ //
+ // N.B.: We only need this codepath because DhcpRequestPacket
+ // hardcodes the source IP address to 0.0.0.0. We could reuse
+ // the packet socket if this ever changes.
+ Os.sendto(mUdpSock, buf, 0, to, DhcpPacket.DHCP_SERVER);
+ } else {
+ // It's safe to call getpeername here, because we only send unicast packets if we
+ // have an IP address, and we connect the UDP socket in DhcpBoundState#enter.
+ if (DBG) Log.d(TAG, String.format("Unicasting %s to %s",
+ description, Os.getpeername(mUdpSock)));
+ Os.write(mUdpSock, buf);
+ }
+ } catch(ErrnoException|IOException e) {
+ Log.e(TAG, "Can't send packet: ", e);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean sendDiscoverPacket() {
+ ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
+ DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
+ DO_UNICAST, REQUESTED_PARAMS);
+ return transmitPacket(packet, "DHCPDISCOVER", DhcpPacket.ENCAP_L2, INADDR_BROADCAST);
+ }
+
+ private boolean sendRequestPacket(
+ Inet4Address clientAddress, Inet4Address requestedAddress,
+ Inet4Address serverAddress, Inet4Address to) {
+ // TODO: should we use the transaction ID from the server?
+ final int encap = INADDR_ANY.equals(clientAddress)
+ ? DhcpPacket.ENCAP_L2 : DhcpPacket.ENCAP_BOOTP;
+
+ ByteBuffer packet = DhcpPacket.buildRequestPacket(
+ encap, mTransactionId, getSecs(), clientAddress,
+ DO_UNICAST, mHwAddr, requestedAddress,
+ serverAddress, REQUESTED_PARAMS, null);
+ String serverStr = (serverAddress != null) ? serverAddress.getHostAddress() : null;
+ String description = "DHCPREQUEST ciaddr=" + clientAddress.getHostAddress() +
+ " request=" + requestedAddress.getHostAddress() +
+ " serverid=" + serverStr;
+ return transmitPacket(packet, description, encap, to);
+ }
+
+ private void scheduleLeaseTimers() {
+ if (mDhcpLeaseExpiry == 0) {
+ Log.d(TAG, "Infinite lease, no timer scheduling needed");
+ return;
+ }
+
+ final long now = SystemClock.elapsedRealtime();
+
+ // TODO: consider getting the renew and rebind timers from T1 and T2.
+ // See also:
+ // https://tools.ietf.org/html/rfc2131#section-4.4.5
+ // https://tools.ietf.org/html/rfc1533#section-9.9
+ // https://tools.ietf.org/html/rfc1533#section-9.10
+ final long remainingDelay = mDhcpLeaseExpiry - now;
+ final long renewDelay = remainingDelay / 2;
+ final long rebindDelay = remainingDelay * 7 / 8;
+ mRenewAlarm.schedule(now + renewDelay);
+ mRebindAlarm.schedule(now + rebindDelay);
+ mExpiryAlarm.schedule(now + remainingDelay);
+ Log.d(TAG, "Scheduling renewal in " + (renewDelay / 1000) + "s");
+ Log.d(TAG, "Scheduling rebind in " + (rebindDelay / 1000) + "s");
+ Log.d(TAG, "Scheduling expiry in " + (remainingDelay / 1000) + "s");
+ }
+
+ private void notifySuccess() {
+ mController.sendMessage(
+ CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, new DhcpResults(mDhcpLease));
+ }
+
+ private void notifyFailure() {
+ mController.sendMessage(CMD_POST_DHCP_ACTION, DHCP_FAILURE, 0, null);
+ }
+
+ private void acceptDhcpResults(DhcpResults results, String msg) {
+ mDhcpLease = results;
+ mOffer = null;
+ Log.d(TAG, msg + " lease: " + mDhcpLease);
+ notifySuccess();
+ }
+
+ private void clearDhcpState() {
+ mDhcpLease = null;
+ mDhcpLeaseExpiry = 0;
+ mOffer = null;
+ }
+
+ /**
+ * Quit the DhcpStateMachine.
+ *
+ * @hide
+ */
+ public void doQuit() {
+ Log.d(TAG, "doQuit");
+ quit();
+ }
+
+ @Override
+ protected void onQuitting() {
+ Log.d(TAG, "onQuitting");
+ mController.sendMessage(CMD_ON_QUIT);
+ }
+
+ abstract class LoggingState extends State {
+ private long mEnterTimeMs;
+
+ @Override
+ public void enter() {
+ if (STATE_DBG) Log.d(TAG, "Entering state " + getName());
+ mEnterTimeMs = SystemClock.elapsedRealtime();
+ }
+
+ @Override
+ public void exit() {
+ long durationMs = SystemClock.elapsedRealtime() - mEnterTimeMs;
+ logState(getName(), (int) durationMs);
+ }
+
+ private String messageName(int what) {
+ return sMessageNames.get(what, Integer.toString(what));
+ }
+
+ private String messageToString(Message message) {
+ long now = SystemClock.uptimeMillis();
+ return new StringBuilder(" ")
+ .append(message.getWhen() - now)
+ .append(messageName(message.what))
+ .append(" ").append(message.arg1)
+ .append(" ").append(message.arg2)
+ .append(" ").append(message.obj)
+ .toString();
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ if (MSG_DBG) {
+ Log.d(TAG, getName() + messageToString(message));
+ }
+ return NOT_HANDLED;
+ }
+
+ @Override
+ public String getName() {
+ // All DhcpClient's states are inner classes with a well defined name.
+ // Use getSimpleName() and avoid super's getName() creating new String instances.
+ return getClass().getSimpleName();
+ }
+ }
+
+ // Sends CMD_PRE_DHCP_ACTION to the controller, waits for the controller to respond with
+ // CMD_PRE_DHCP_ACTION_COMPLETE, and then transitions to mOtherState.
+ abstract class WaitBeforeOtherState extends LoggingState {
+ protected State mOtherState;
+
+ @Override
+ public void enter() {
+ super.enter();
+ mController.sendMessage(CMD_PRE_DHCP_ACTION);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ super.processMessage(message);
+ switch (message.what) {
+ case CMD_PRE_DHCP_ACTION_COMPLETE:
+ transitionTo(mOtherState);
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class StoppedState extends State {
+ @Override
+ public boolean processMessage(Message message) {
+ switch (message.what) {
+ case CMD_START_DHCP:
+ if (mRegisteredForPreDhcpNotification) {
+ transitionTo(mWaitBeforeStartState);
+ } else {
+ transitionTo(mDhcpInitState);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class WaitBeforeStartState extends WaitBeforeOtherState {
+ public WaitBeforeStartState(State otherState) {
+ super();
+ mOtherState = otherState;
+ }
+ }
+
+ class WaitBeforeRenewalState extends WaitBeforeOtherState {
+ public WaitBeforeRenewalState(State otherState) {
+ super();
+ mOtherState = otherState;
+ }
+ }
+
+ class DhcpState extends State {
+ @Override
+ public void enter() {
+ clearDhcpState();
+ if (initInterface() && initSockets()) {
+ mReceiveThread = new ReceiveThread();
+ mReceiveThread.start();
+ } else {
+ notifyFailure();
+ transitionTo(mStoppedState);
+ }
+ }
+
+ @Override
+ public void exit() {
+ if (mReceiveThread != null) {
+ mReceiveThread.halt(); // Also closes sockets.
+ mReceiveThread = null;
+ }
+ clearDhcpState();
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ super.processMessage(message);
+ switch (message.what) {
+ case CMD_STOP_DHCP:
+ transitionTo(mStoppedState);
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ public boolean isValidPacket(DhcpPacket packet) {
+ // TODO: check checksum.
+ int xid = packet.getTransactionId();
+ if (xid != mTransactionId) {
+ Log.d(TAG, "Unexpected transaction ID " + xid + ", expected " + mTransactionId);
+ return false;
+ }
+ if (!Arrays.equals(packet.getClientMac(), mHwAddr)) {
+ Log.d(TAG, "MAC addr mismatch: got " +
+ HexDump.toHexString(packet.getClientMac()) + ", expected " +
+ HexDump.toHexString(packet.getClientMac()));
+ return false;
+ }
+ return true;
+ }
+
+ public void setDhcpLeaseExpiry(DhcpPacket packet) {
+ long leaseTimeMillis = packet.getLeaseTimeMillis();
+ mDhcpLeaseExpiry =
+ (leaseTimeMillis > 0) ? SystemClock.elapsedRealtime() + leaseTimeMillis : 0;
+ }
+
+ /**
+ * Retransmits packets using jittered exponential backoff with an optional timeout. Packet
+ * transmission is triggered by CMD_KICK, which is sent by an AlarmManager alarm. If a subclass
+ * sets mTimeout to a positive value, then timeout() is called by an AlarmManager alarm mTimeout
+ * milliseconds after entering the state. Kicks and timeouts are cancelled when leaving the
+ * state.
+ *
+ * Concrete subclasses must implement sendPacket, which is called when the alarm fires and a
+ * packet needs to be transmitted, and receivePacket, which is triggered by CMD_RECEIVED_PACKET
+ * sent by the receive thread. They may also set mTimeout and implement timeout.
+ */
+ abstract class PacketRetransmittingState extends LoggingState {
+
+ private int mTimer;
+ protected int mTimeout = 0;
+
+ @Override
+ public void enter() {
+ super.enter();
+ initTimer();
+ maybeInitTimeout();
+ sendMessage(CMD_KICK);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ super.processMessage(message);
+ switch (message.what) {
+ case CMD_KICK:
+ sendPacket();
+ scheduleKick();
+ return HANDLED;
+ case CMD_RECEIVED_PACKET:
+ receivePacket((DhcpPacket) message.obj);
+ return HANDLED;
+ case CMD_TIMEOUT:
+ timeout();
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+
+ @Override
+ public void exit() {
+ super.exit();
+ mKickAlarm.cancel();
+ mTimeoutAlarm.cancel();
+ }
+
+ abstract protected boolean sendPacket();
+ abstract protected void receivePacket(DhcpPacket packet);
+ protected void timeout() {}
+
+ protected void initTimer() {
+ mTimer = FIRST_TIMEOUT_MS;
+ }
+
+ protected int jitterTimer(int baseTimer) {
+ int maxJitter = baseTimer / 10;
+ int jitter = mRandom.nextInt(2 * maxJitter) - maxJitter;
+ return baseTimer + jitter;
+ }
+
+ protected void scheduleKick() {
+ long now = SystemClock.elapsedRealtime();
+ long timeout = jitterTimer(mTimer);
+ long alarmTime = now + timeout;
+ mKickAlarm.schedule(alarmTime);
+ mTimer *= 2;
+ if (mTimer > MAX_TIMEOUT_MS) {
+ mTimer = MAX_TIMEOUT_MS;
+ }
+ }
+
+ protected void maybeInitTimeout() {
+ if (mTimeout > 0) {
+ long alarmTime = SystemClock.elapsedRealtime() + mTimeout;
+ mTimeoutAlarm.schedule(alarmTime);
+ }
+ }
+ }
+
+ class DhcpInitState extends PacketRetransmittingState {
+ public DhcpInitState() {
+ super();
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+ startNewTransaction();
+ mLastInitEnterTime = SystemClock.elapsedRealtime();
+ }
+
+ protected boolean sendPacket() {
+ return sendDiscoverPacket();
+ }
+
+ protected void receivePacket(DhcpPacket packet) {
+ if (!isValidPacket(packet)) return;
+ if (!(packet instanceof DhcpOfferPacket)) return;
+ mOffer = packet.toDhcpResults();
+ if (mOffer != null) {
+ Log.d(TAG, "Got pending lease: " + mOffer);
+ transitionTo(mDhcpRequestingState);
+ }
+ }
+ }
+
+ // Not implemented. We request the first offer we receive.
+ class DhcpSelectingState extends LoggingState {
+ }
+
+ class DhcpRequestingState extends PacketRetransmittingState {
+ public DhcpRequestingState() {
+ mTimeout = DHCP_TIMEOUT_MS / 2;
+ }
+
+ protected boolean sendPacket() {
+ return sendRequestPacket(
+ INADDR_ANY, // ciaddr
+ (Inet4Address) mOffer.ipAddress.getAddress(), // DHCP_REQUESTED_IP
+ (Inet4Address) mOffer.serverAddress, // DHCP_SERVER_IDENTIFIER
+ INADDR_BROADCAST); // packet destination address
+ }
+
+ protected void receivePacket(DhcpPacket packet) {
+ if (!isValidPacket(packet)) return;
+ if ((packet instanceof DhcpAckPacket)) {
+ DhcpResults results = packet.toDhcpResults();
+ if (results != null) {
+ setDhcpLeaseExpiry(packet);
+ acceptDhcpResults(results, "Confirmed");
+ transitionTo(mConfiguringInterfaceState);
+ }
+ } else if (packet instanceof DhcpNakPacket) {
+ // TODO: Wait a while before returning into INIT state.
+ Log.d(TAG, "Received NAK, returning to INIT");
+ mOffer = null;
+ transitionTo(mDhcpInitState);
+ }
+ }
+
+ @Override
+ protected void timeout() {
+ // After sending REQUESTs unsuccessfully for a while, go back to init.
+ transitionTo(mDhcpInitState);
+ }
+ }
+
+ class DhcpHaveLeaseState extends State {
+ @Override
+ public boolean processMessage(Message message) {
+ switch (message.what) {
+ case CMD_EXPIRE_DHCP:
+ Log.d(TAG, "Lease expired!");
+ notifyFailure();
+ transitionTo(mDhcpInitState);
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+
+ @Override
+ public void exit() {
+ // Clear any extant alarms.
+ mRenewAlarm.cancel();
+ mRebindAlarm.cancel();
+ mExpiryAlarm.cancel();
+ clearDhcpState();
+ // Tell IpManager to clear the IPv4 address. There is no need to
+ // wait for confirmation since any subsequent packets are sent from
+ // INADDR_ANY anyway (DISCOVER, REQUEST).
+ mController.sendMessage(CMD_CLEAR_LINKADDRESS);
+ }
+ }
+
+ class ConfiguringInterfaceState extends LoggingState {
+ @Override
+ public void enter() {
+ super.enter();
+ mController.sendMessage(CMD_CONFIGURE_LINKADDRESS, mDhcpLease.ipAddress);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ super.processMessage(message);
+ switch (message.what) {
+ case EVENT_LINKADDRESS_CONFIGURED:
+ transitionTo(mDhcpBoundState);
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+ }
+
+ class DhcpBoundState extends LoggingState {
+ @Override
+ public void enter() {
+ super.enter();
+ if (mDhcpLease.serverAddress != null && !connectUdpSock(mDhcpLease.serverAddress)) {
+ // There's likely no point in going into DhcpInitState here, we'll probably
+ // just repeat the transaction, get the same IP address as before, and fail.
+ //
+ // NOTE: It is observed that connectUdpSock() basically never fails, due to
+ // SO_BINDTODEVICE. Examining the local socket address shows it will happily
+ // return an IPv4 address from another interface, or even return "0.0.0.0".
+ //
+ // TODO: Consider deleting this check, following testing on several kernels.
+ notifyFailure();
+ transitionTo(mStoppedState);
+ }
+
+ scheduleLeaseTimers();
+ logTimeToBoundState();
+ }
+
+ @Override
+ public void exit() {
+ super.exit();
+ mLastBoundExitTime = SystemClock.elapsedRealtime();
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ super.processMessage(message);
+ switch (message.what) {
+ case CMD_RENEW_DHCP:
+ if (mRegisteredForPreDhcpNotification) {
+ transitionTo(mWaitBeforeRenewalState);
+ } else {
+ transitionTo(mDhcpRenewingState);
+ }
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+
+ private void logTimeToBoundState() {
+ long now = SystemClock.elapsedRealtime();
+ if (mLastBoundExitTime > mLastInitEnterTime) {
+ logState(DhcpClientEvent.RENEWING_BOUND, (int)(now - mLastBoundExitTime));
+ } else {
+ logState(DhcpClientEvent.INITIAL_BOUND, (int)(now - mLastInitEnterTime));
+ }
+ }
+ }
+
+ abstract class DhcpReacquiringState extends PacketRetransmittingState {
+ protected String mLeaseMsg;
+
+ @Override
+ public void enter() {
+ super.enter();
+ startNewTransaction();
+ }
+
+ abstract protected Inet4Address packetDestination();
+
+ protected boolean sendPacket() {
+ return sendRequestPacket(
+ (Inet4Address) mDhcpLease.ipAddress.getAddress(), // ciaddr
+ INADDR_ANY, // DHCP_REQUESTED_IP
+ null, // DHCP_SERVER_IDENTIFIER
+ packetDestination()); // packet destination address
+ }
+
+ protected void receivePacket(DhcpPacket packet) {
+ if (!isValidPacket(packet)) return;
+ if ((packet instanceof DhcpAckPacket)) {
+ final DhcpResults results = packet.toDhcpResults();
+ if (results != null) {
+ if (!mDhcpLease.ipAddress.equals(results.ipAddress)) {
+ Log.d(TAG, "Renewed lease not for our current IP address!");
+ notifyFailure();
+ transitionTo(mDhcpInitState);
+ }
+ setDhcpLeaseExpiry(packet);
+ // Updating our notion of DhcpResults here only causes the
+ // DNS servers and routes to be updated in LinkProperties
+ // in IpManager and by any overridden relevant handlers of
+ // the registered IpManager.Callback. IP address changes
+ // are not supported here.
+ acceptDhcpResults(results, mLeaseMsg);
+ transitionTo(mDhcpBoundState);
+ }
+ } else if (packet instanceof DhcpNakPacket) {
+ Log.d(TAG, "Received NAK, returning to INIT");
+ notifyFailure();
+ transitionTo(mDhcpInitState);
+ }
+ }
+ }
+
+ class DhcpRenewingState extends DhcpReacquiringState {
+ public DhcpRenewingState() {
+ mLeaseMsg = "Renewed";
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ if (super.processMessage(message) == HANDLED) {
+ return HANDLED;
+ }
+
+ switch (message.what) {
+ case CMD_REBIND_DHCP:
+ transitionTo(mDhcpRebindingState);
+ return HANDLED;
+ default:
+ return NOT_HANDLED;
+ }
+ }
+
+ @Override
+ protected Inet4Address packetDestination() {
+ // Not specifying a SERVER_IDENTIFIER option is a violation of RFC 2131, but...
+ // http://b/25343517 . Try to make things work anyway by using broadcast renews.
+ return (mDhcpLease.serverAddress != null) ?
+ mDhcpLease.serverAddress : INADDR_BROADCAST;
+ }
+ }
+
+ class DhcpRebindingState extends DhcpReacquiringState {
+ public DhcpRebindingState() {
+ mLeaseMsg = "Rebound";
+ }
+
+ @Override
+ public void enter() {
+ super.enter();
+
+ // We need to broadcast and possibly reconnect the socket to a
+ // completely different server.
+ closeQuietly(mUdpSock);
+ if (!initUdpSocket()) {
+ Log.e(TAG, "Failed to recreate UDP socket");
+ transitionTo(mDhcpInitState);
+ }
+ }
+
+ @Override
+ protected Inet4Address packetDestination() {
+ return INADDR_BROADCAST;
+ }
+ }
+
+ class DhcpInitRebootState extends LoggingState {
+ }
+
+ class DhcpRebootingState extends LoggingState {
+ }
+
+ private void logError(int errorCode) {
+ mMetricsLog.log(mIfaceName, new DhcpErrorEvent(errorCode));
+ }
+
+ private void logState(String name, int durationMs) {
+ final DhcpClientEvent event = new DhcpClientEvent.Builder()
+ .setMsg(name)
+ .setDurationMs(durationMs)
+ .build();
+ mMetricsLog.log(mIfaceName, event);
+ }
+}
diff --git a/src/android/net/dhcp/DhcpDeclinePacket.java b/src/android/net/dhcp/DhcpDeclinePacket.java
new file mode 100644
index 0000000..7ecdea7
--- /dev/null
+++ b/src/android/net/dhcp/DhcpDeclinePacket.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 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.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the DHCP-DECLINE packet.
+ */
+class DhcpDeclinePacket extends DhcpPacket {
+ /**
+ * Generates a DECLINE packet with the specified parameters.
+ */
+ DhcpDeclinePacket(int transId, short secs, Inet4Address clientIp, Inet4Address yourIp,
+ Inet4Address nextIp, Inet4Address relayIp,
+ byte[] clientMac) {
+ super(transId, secs, clientIp, yourIp, nextIp, relayIp, clientMac, false);
+ }
+
+ public String toString() {
+ String s = super.toString();
+ return s + " DECLINE";
+ }
+
+ /**
+ * Fills in a packet with the requested DECLINE attributes.
+ */
+ public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+ ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+
+ fillInPacket(encap, mClientIp, mYourIp, destUdp, srcUdp, result,
+ DHCP_BOOTREQUEST, false);
+ result.flip();
+ return result;
+ }
+
+ /**
+ * Adds optional parameters to the DECLINE packet.
+ */
+ void finishPacket(ByteBuffer buffer) {
+ addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_DECLINE);
+ addTlv(buffer, DHCP_CLIENT_IDENTIFIER, getClientId());
+ // RFC 2131 says we MUST NOT include our common client TLVs or the parameter request list.
+ addTlvEnd(buffer);
+ }
+}
diff --git a/src/android/net/dhcp/DhcpDiscoverPacket.java b/src/android/net/dhcp/DhcpDiscoverPacket.java
new file mode 100644
index 0000000..11f2b61
--- /dev/null
+++ b/src/android/net/dhcp/DhcpDiscoverPacket.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2010 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.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the DHCP-DISCOVER packet.
+ */
+class DhcpDiscoverPacket extends DhcpPacket {
+ /**
+ * The IP address of the client which sent this packet.
+ */
+ final Inet4Address mSrcIp;
+
+ /**
+ * Generates a DISCOVER packet with the specified parameters.
+ */
+ DhcpDiscoverPacket(int transId, short secs, Inet4Address relayIp, byte[] clientMac,
+ boolean broadcast, Inet4Address srcIp) {
+ super(transId, secs, INADDR_ANY, INADDR_ANY, INADDR_ANY, relayIp, clientMac, broadcast);
+ mSrcIp = srcIp;
+ }
+
+ public String toString() {
+ String s = super.toString();
+ return s + " DISCOVER " +
+ (mBroadcast ? "broadcast " : "unicast ");
+ }
+
+ /**
+ * Fills in a packet with the requested DISCOVER parameters.
+ */
+ public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+ ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+ fillInPacket(encap, INADDR_BROADCAST, mSrcIp, destUdp, srcUdp, result, DHCP_BOOTREQUEST,
+ mBroadcast);
+ result.flip();
+ return result;
+ }
+
+ /**
+ * Adds optional parameters to a DISCOVER packet.
+ */
+ void finishPacket(ByteBuffer buffer) {
+ addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_DISCOVER);
+ addTlv(buffer, DHCP_CLIENT_IDENTIFIER, getClientId());
+ addCommonClientTlvs(buffer);
+ addTlv(buffer, DHCP_PARAMETER_LIST, mRequestedParams);
+ addTlvEnd(buffer);
+ }
+}
diff --git a/src/android/net/dhcp/DhcpInformPacket.java b/src/android/net/dhcp/DhcpInformPacket.java
new file mode 100644
index 0000000..7a83466
--- /dev/null
+++ b/src/android/net/dhcp/DhcpInformPacket.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 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.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the (unused) DHCP-INFORM packet.
+ */
+class DhcpInformPacket extends DhcpPacket {
+ /**
+ * Generates an INFORM packet with the specified parameters.
+ */
+ DhcpInformPacket(int transId, short secs, Inet4Address clientIp, Inet4Address yourIp,
+ Inet4Address nextIp, Inet4Address relayIp,
+ byte[] clientMac) {
+ super(transId, secs, clientIp, yourIp, nextIp, relayIp, clientMac, false);
+ }
+
+ public String toString() {
+ String s = super.toString();
+ return s + " INFORM";
+ }
+
+ /**
+ * Builds an INFORM packet.
+ */
+ public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+ ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+
+ fillInPacket(encap, mClientIp, mYourIp, destUdp, srcUdp, result,
+ DHCP_BOOTREQUEST, false);
+ result.flip();
+ return result;
+ }
+
+ /**
+ * Adds additional parameters to the INFORM packet.
+ */
+ void finishPacket(ByteBuffer buffer) {
+ addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_INFORM);
+ addTlv(buffer, DHCP_CLIENT_IDENTIFIER, getClientId());
+ addCommonClientTlvs(buffer);
+ addTlv(buffer, DHCP_PARAMETER_LIST, mRequestedParams);
+ addTlvEnd(buffer);
+ }
+}
diff --git a/src/android/net/dhcp/DhcpNakPacket.java b/src/android/net/dhcp/DhcpNakPacket.java
new file mode 100644
index 0000000..1da0b73
--- /dev/null
+++ b/src/android/net/dhcp/DhcpNakPacket.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2010 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.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the DHCP-NAK packet.
+ */
+class DhcpNakPacket extends DhcpPacket {
+ /**
+ * Generates a NAK packet with the specified parameters.
+ */
+ DhcpNakPacket(int transId, short secs, Inet4Address relayIp, byte[] clientMac,
+ boolean broadcast) {
+ super(transId, secs, INADDR_ANY /* clientIp */, INADDR_ANY /* yourIp */,
+ INADDR_ANY /* nextIp */, relayIp, clientMac, broadcast);
+ }
+
+ public String toString() {
+ String s = super.toString();
+ return s + " NAK, reason " + (mMessage == null ? "(none)" : mMessage);
+ }
+
+ /**
+ * Fills in a packet with the requested NAK attributes.
+ */
+ public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+ ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+ // Constructor does not set values for layers <= 3: use empty values
+ Inet4Address destIp = INADDR_ANY;
+ Inet4Address srcIp = INADDR_ANY;
+
+ fillInPacket(encap, destIp, srcIp, destUdp, srcUdp, result, DHCP_BOOTREPLY, mBroadcast);
+ result.flip();
+ return result;
+ }
+
+ /**
+ * Adds the optional parameters to the client-generated NAK packet.
+ */
+ void finishPacket(ByteBuffer buffer) {
+ addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_NAK);
+ addTlv(buffer, DHCP_SERVER_IDENTIFIER, mServerIdentifier);
+ addTlv(buffer, DHCP_MESSAGE, mMessage);
+ addTlvEnd(buffer);
+ }
+}
diff --git a/src/android/net/dhcp/DhcpOfferPacket.java b/src/android/net/dhcp/DhcpOfferPacket.java
new file mode 100644
index 0000000..0eba77e
--- /dev/null
+++ b/src/android/net/dhcp/DhcpOfferPacket.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2010 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.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the DHCP-OFFER packet.
+ */
+class DhcpOfferPacket extends DhcpPacket {
+ /**
+ * The IP address of the server which sent this packet.
+ */
+ private final Inet4Address mSrcIp;
+
+ /**
+ * Generates a OFFER packet with the specified parameters.
+ */
+ DhcpOfferPacket(int transId, short secs, boolean broadcast, Inet4Address serverAddress,
+ Inet4Address relayIp, Inet4Address clientIp, Inet4Address yourIp, byte[] clientMac) {
+ super(transId, secs, clientIp, yourIp, serverAddress, relayIp, clientMac, broadcast);
+ mSrcIp = serverAddress;
+ }
+
+ public String toString() {
+ String s = super.toString();
+ String dnsServers = ", DNS servers: ";
+
+ if (mDnsServers != null) {
+ for (Inet4Address dnsServer: mDnsServers) {
+ dnsServers += dnsServer + " ";
+ }
+ }
+
+ return s + " OFFER, ip " + mYourIp + ", mask " + mSubnetMask +
+ dnsServers + ", gateways " + mGateways +
+ " lease time " + mLeaseTime + ", domain " + mDomainName;
+ }
+
+ /**
+ * Fills in a packet with the specified OFFER attributes.
+ */
+ public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+ ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+ Inet4Address destIp = mBroadcast ? INADDR_BROADCAST : mYourIp;
+ Inet4Address srcIp = mBroadcast ? INADDR_ANY : mSrcIp;
+
+ fillInPacket(encap, destIp, srcIp, destUdp, srcUdp, result,
+ DHCP_BOOTREPLY, mBroadcast);
+ result.flip();
+ return result;
+ }
+
+ /**
+ * Adds the optional parameters to the server-generated OFFER packet.
+ */
+ void finishPacket(ByteBuffer buffer) {
+ addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_OFFER);
+ addTlv(buffer, DHCP_SERVER_IDENTIFIER, mServerIdentifier);
+
+ addCommonServerTlvs(buffer);
+ addTlvEnd(buffer);
+ }
+}
diff --git a/src/android/net/dhcp/DhcpPacket.java b/src/android/net/dhcp/DhcpPacket.java
new file mode 100644
index 0000000..ce8b7e7
--- /dev/null
+++ b/src/android/net/dhcp/DhcpPacket.java
@@ -0,0 +1,1364 @@
+package android.net.dhcp;
+
+import android.annotation.Nullable;
+import android.net.DhcpResults;
+import android.net.LinkAddress;
+import android.net.NetworkUtils;
+import android.net.metrics.DhcpErrorEvent;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.system.OsConstants;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.UnsupportedEncodingException;
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Defines basic data and operations needed to build and use packets for the
+ * DHCP protocol. Subclasses create the specific packets used at each
+ * stage of the negotiation.
+ *
+ * @hide
+ */
+public abstract class DhcpPacket {
+ protected static final String TAG = "DhcpPacket";
+
+ // TODO: use NetworkStackConstants.IPV4_MIN_MTU once this class is moved to the network stack.
+ private static final int IPV4_MIN_MTU = 68;
+
+ // dhcpcd has a minimum lease of 20 seconds, but DhcpStateMachine would refuse to wake up the
+ // CPU for anything shorter than 5 minutes. For sanity's sake, this must be higher than the
+ // DHCP client timeout.
+ public static final int MINIMUM_LEASE = 60;
+ public static final int INFINITE_LEASE = (int) 0xffffffff;
+
+ public static final Inet4Address INADDR_ANY = (Inet4Address) Inet4Address.ANY;
+ public static final Inet4Address INADDR_BROADCAST = (Inet4Address) Inet4Address.ALL;
+ public static final byte[] ETHER_BROADCAST = new byte[] {
+ (byte) 0xff, (byte) 0xff, (byte) 0xff,
+ (byte) 0xff, (byte) 0xff, (byte) 0xff,
+ };
+
+ /**
+ * Packet encapsulations.
+ */
+ public static final int ENCAP_L2 = 0; // EthernetII header included
+ public static final int ENCAP_L3 = 1; // IP/UDP header included
+ public static final int ENCAP_BOOTP = 2; // BOOTP contents only
+
+ /**
+ * Minimum length of a DHCP packet, excluding options, in the above encapsulations.
+ */
+ public static final int MIN_PACKET_LENGTH_BOOTP = 236; // See diagram in RFC 2131, section 2.
+ public static final int MIN_PACKET_LENGTH_L3 = MIN_PACKET_LENGTH_BOOTP + 20 + 8;
+ public static final int MIN_PACKET_LENGTH_L2 = MIN_PACKET_LENGTH_L3 + 14;
+
+ public static final int HWADDR_LEN = 16;
+ public static final int MAX_OPTION_LEN = 255;
+
+ /**
+ * The minimum and maximum MTU that we are prepared to use. We set the minimum to the minimum
+ * IPv6 MTU because the IPv6 stack enters unusual codepaths when the link MTU drops below 1280,
+ * and does not recover if the MTU is brought above 1280 again. We set the maximum to 1500
+ * because in general it is risky to assume that the hardware is able to send/receive packets
+ * larger than 1500 bytes even if the network supports it.
+ */
+ private static final int MIN_MTU = 1280;
+ private static final int MAX_MTU = 1500;
+
+ /**
+ * IP layer definitions.
+ */
+ private static final byte IP_TYPE_UDP = (byte) 0x11;
+
+ /**
+ * IP: Version 4, Header Length 20 bytes
+ */
+ private static final byte IP_VERSION_HEADER_LEN = (byte) 0x45;
+
+ /**
+ * IP: Flags 0, Fragment Offset 0, Don't Fragment
+ */
+ private static final short IP_FLAGS_OFFSET = (short) 0x4000;
+
+ /**
+ * IP: TOS
+ */
+ private static final byte IP_TOS_LOWDELAY = (byte) 0x10;
+
+ /**
+ * IP: TTL -- use default 64 from RFC1340
+ */
+ private static final byte IP_TTL = (byte) 0x40;
+
+ /**
+ * The client DHCP port.
+ */
+ static final short DHCP_CLIENT = (short) 68;
+
+ /**
+ * The server DHCP port.
+ */
+ static final short DHCP_SERVER = (short) 67;
+
+ /**
+ * The message op code indicating a request from a client.
+ */
+ protected static final byte DHCP_BOOTREQUEST = (byte) 1;
+
+ /**
+ * The message op code indicating a response from the server.
+ */
+ protected static final byte DHCP_BOOTREPLY = (byte) 2;
+
+ /**
+ * The code type used to identify an Ethernet MAC address in the
+ * Client-ID field.
+ */
+ protected static final byte CLIENT_ID_ETHER = (byte) 1;
+
+ /**
+ * The maximum length of a packet that can be constructed.
+ */
+ protected static final int MAX_LENGTH = 1500;
+
+ /**
+ * The magic cookie that identifies this as a DHCP packet instead of BOOTP.
+ */
+ private static final int DHCP_MAGIC_COOKIE = 0x63825363;
+
+ /**
+ * DHCP Optional Type: DHCP Subnet Mask
+ */
+ protected static final byte DHCP_SUBNET_MASK = 1;
+ protected Inet4Address mSubnetMask;
+
+ /**
+ * DHCP Optional Type: DHCP Router
+ */
+ protected static final byte DHCP_ROUTER = 3;
+ protected List <Inet4Address> mGateways;
+
+ /**
+ * DHCP Optional Type: DHCP DNS Server
+ */
+ protected static final byte DHCP_DNS_SERVER = 6;
+ protected List<Inet4Address> mDnsServers;
+
+ /**
+ * DHCP Optional Type: DHCP Host Name
+ */
+ protected static final byte DHCP_HOST_NAME = 12;
+ protected String mHostName;
+
+ /**
+ * DHCP Optional Type: DHCP DOMAIN NAME
+ */
+ protected static final byte DHCP_DOMAIN_NAME = 15;
+ protected String mDomainName;
+
+ /**
+ * DHCP Optional Type: DHCP Interface MTU
+ */
+ protected static final byte DHCP_MTU = 26;
+ protected Short mMtu;
+
+ /**
+ * DHCP Optional Type: DHCP BROADCAST ADDRESS
+ */
+ protected static final byte DHCP_BROADCAST_ADDRESS = 28;
+ protected Inet4Address mBroadcastAddress;
+
+ /**
+ * DHCP Optional Type: Vendor specific information
+ */
+ protected static final byte DHCP_VENDOR_INFO = 43;
+ protected String mVendorInfo;
+
+ /**
+ * Value of the vendor specific option used to indicate that the network is metered
+ */
+ public static final String VENDOR_INFO_ANDROID_METERED = "ANDROID_METERED";
+
+ /**
+ * DHCP Optional Type: DHCP Requested IP Address
+ */
+ protected static final byte DHCP_REQUESTED_IP = 50;
+ protected Inet4Address mRequestedIp;
+
+ /**
+ * DHCP Optional Type: DHCP Lease Time
+ */
+ protected static final byte DHCP_LEASE_TIME = 51;
+ protected Integer mLeaseTime;
+
+ /**
+ * DHCP Optional Type: DHCP Message Type
+ */
+ protected static final byte DHCP_MESSAGE_TYPE = 53;
+ // the actual type values
+ protected static final byte DHCP_MESSAGE_TYPE_DISCOVER = 1;
+ protected static final byte DHCP_MESSAGE_TYPE_OFFER = 2;
+ protected static final byte DHCP_MESSAGE_TYPE_REQUEST = 3;
+ protected static final byte DHCP_MESSAGE_TYPE_DECLINE = 4;
+ protected static final byte DHCP_MESSAGE_TYPE_ACK = 5;
+ protected static final byte DHCP_MESSAGE_TYPE_NAK = 6;
+ protected static final byte DHCP_MESSAGE_TYPE_RELEASE = 7;
+ protected static final byte DHCP_MESSAGE_TYPE_INFORM = 8;
+
+ /**
+ * DHCP Optional Type: DHCP Server Identifier
+ */
+ protected static final byte DHCP_SERVER_IDENTIFIER = 54;
+ protected Inet4Address mServerIdentifier;
+
+ /**
+ * DHCP Optional Type: DHCP Parameter List
+ */
+ protected static final byte DHCP_PARAMETER_LIST = 55;
+ protected byte[] mRequestedParams;
+
+ /**
+ * DHCP Optional Type: DHCP MESSAGE
+ */
+ protected static final byte DHCP_MESSAGE = 56;
+ protected String mMessage;
+
+ /**
+ * DHCP Optional Type: Maximum DHCP Message Size
+ */
+ protected static final byte DHCP_MAX_MESSAGE_SIZE = 57;
+ protected Short mMaxMessageSize;
+
+ /**
+ * DHCP Optional Type: DHCP Renewal Time Value
+ */
+ protected static final byte DHCP_RENEWAL_TIME = 58;
+ protected Integer mT1;
+
+ /**
+ * DHCP Optional Type: Rebinding Time Value
+ */
+ protected static final byte DHCP_REBINDING_TIME = 59;
+ protected Integer mT2;
+
+ /**
+ * DHCP Optional Type: Vendor Class Identifier
+ */
+ protected static final byte DHCP_VENDOR_CLASS_ID = 60;
+ protected String mVendorId;
+
+ /**
+ * DHCP Optional Type: DHCP Client Identifier
+ */
+ protected static final byte DHCP_CLIENT_IDENTIFIER = 61;
+ protected byte[] mClientId;
+
+ /**
+ * DHCP zero-length option code: pad
+ */
+ protected static final byte DHCP_OPTION_PAD = 0x00;
+
+ /**
+ * DHCP zero-length option code: end of options
+ */
+ protected static final byte DHCP_OPTION_END = (byte) 0xff;
+
+ /**
+ * The transaction identifier used in this particular DHCP negotiation
+ */
+ protected final int mTransId;
+
+ /**
+ * The seconds field in the BOOTP header. Per RFC, should be nonzero in client requests only.
+ */
+ protected final short mSecs;
+
+ /**
+ * The IP address of the client host. This address is typically
+ * proposed by the client (from an earlier DHCP negotiation) or
+ * supplied by the server.
+ */
+ protected final Inet4Address mClientIp;
+ protected final Inet4Address mYourIp;
+ private final Inet4Address mNextIp;
+ protected final Inet4Address mRelayIp;
+
+ /**
+ * Does the client request a broadcast response?
+ */
+ protected boolean mBroadcast;
+
+ /**
+ * The six-octet MAC of the client.
+ */
+ protected final byte[] mClientMac;
+
+ /**
+ * Asks the packet object to create a ByteBuffer serialization of
+ * the packet for transmission.
+ */
+ public abstract ByteBuffer buildPacket(int encap, short destUdp,
+ short srcUdp);
+
+ /**
+ * Allows the concrete class to fill in packet-type-specific details,
+ * typically optional parameters at the end of the packet.
+ */
+ abstract void finishPacket(ByteBuffer buffer);
+
+ // Set in unit tests, to ensure that the test does not break when run on different devices and
+ // on different releases.
+ static String testOverrideVendorId = null;
+ static String testOverrideHostname = null;
+
+ protected DhcpPacket(int transId, short secs, Inet4Address clientIp, Inet4Address yourIp,
+ Inet4Address nextIp, Inet4Address relayIp,
+ byte[] clientMac, boolean broadcast) {
+ mTransId = transId;
+ mSecs = secs;
+ mClientIp = clientIp;
+ mYourIp = yourIp;
+ mNextIp = nextIp;
+ mRelayIp = relayIp;
+ mClientMac = clientMac;
+ mBroadcast = broadcast;
+ }
+
+ /**
+ * Returns the transaction ID.
+ */
+ public int getTransactionId() {
+ return mTransId;
+ }
+
+ /**
+ * Returns the client MAC.
+ */
+ public byte[] getClientMac() {
+ return mClientMac;
+ }
+
+ // TODO: refactor DhcpClient to set clientId when constructing packets and remove
+ // hasExplicitClientId logic
+ /**
+ * Returns whether a client ID was set in the options for this packet.
+ */
+ public boolean hasExplicitClientId() {
+ return mClientId != null;
+ }
+
+ /**
+ * Convenience method to return the client ID if it was set explicitly, or null otherwise.
+ */
+ @Nullable
+ public byte[] getExplicitClientIdOrNull() {
+ return hasExplicitClientId() ? getClientId() : null;
+ }
+
+ /**
+ * Returns the client ID. If not set explicitly, this follows RFC 2132 and creates a client ID
+ * based on the hardware address.
+ */
+ public byte[] getClientId() {
+ final byte[] clientId;
+ if (hasExplicitClientId()) {
+ clientId = Arrays.copyOf(mClientId, mClientId.length);
+ } else {
+ clientId = new byte[mClientMac.length + 1];
+ clientId[0] = CLIENT_ID_ETHER;
+ System.arraycopy(mClientMac, 0, clientId, 1, mClientMac.length);
+ }
+ return clientId;
+ }
+
+ /**
+ * Returns whether a parameter is included in the parameter request list option of this packet.
+ *
+ * <p>If there is no parameter request list option in the packet, false is returned.
+ *
+ * @param paramId ID of the parameter, such as {@link #DHCP_MTU} or {@link #DHCP_HOST_NAME}.
+ */
+ public boolean hasRequestedParam(byte paramId) {
+ if (mRequestedParams == null) {
+ return false;
+ }
+
+ for (byte reqParam : mRequestedParams) {
+ if (reqParam == paramId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Creates a new L3 packet (including IP header) containing the
+ * DHCP udp packet. This method relies upon the delegated method
+ * finishPacket() to insert the per-packet contents.
+ */
+ protected void fillInPacket(int encap, Inet4Address destIp,
+ Inet4Address srcIp, short destUdp, short srcUdp, ByteBuffer buf,
+ byte requestCode, boolean broadcast) {
+ byte[] destIpArray = destIp.getAddress();
+ byte[] srcIpArray = srcIp.getAddress();
+ int ipHeaderOffset = 0;
+ int ipLengthOffset = 0;
+ int ipChecksumOffset = 0;
+ int endIpHeader = 0;
+ int udpHeaderOffset = 0;
+ int udpLengthOffset = 0;
+ int udpChecksumOffset = 0;
+
+ buf.clear();
+ buf.order(ByteOrder.BIG_ENDIAN);
+
+ if (encap == ENCAP_L2) {
+ buf.put(ETHER_BROADCAST);
+ buf.put(mClientMac);
+ buf.putShort((short) OsConstants.ETH_P_IP);
+ }
+
+ // if a full IP packet needs to be generated, put the IP & UDP
+ // headers in place, and pre-populate with artificial values
+ // needed to seed the IP checksum.
+ if (encap <= ENCAP_L3) {
+ ipHeaderOffset = buf.position();
+ buf.put(IP_VERSION_HEADER_LEN);
+ buf.put(IP_TOS_LOWDELAY); // tos: IPTOS_LOWDELAY
+ ipLengthOffset = buf.position();
+ buf.putShort((short)0); // length
+ buf.putShort((short)0); // id
+ buf.putShort(IP_FLAGS_OFFSET); // ip offset: don't fragment
+ buf.put(IP_TTL); // TTL: use default 64 from RFC1340
+ buf.put(IP_TYPE_UDP);
+ ipChecksumOffset = buf.position();
+ buf.putShort((short) 0); // checksum
+
+ buf.put(srcIpArray);
+ buf.put(destIpArray);
+ endIpHeader = buf.position();
+
+ // UDP header
+ udpHeaderOffset = buf.position();
+ buf.putShort(srcUdp);
+ buf.putShort(destUdp);
+ udpLengthOffset = buf.position();
+ buf.putShort((short) 0); // length
+ udpChecksumOffset = buf.position();
+ buf.putShort((short) 0); // UDP checksum -- initially zero
+ }
+
+ // DHCP payload
+ buf.put(requestCode);
+ buf.put((byte) 1); // Hardware Type: Ethernet
+ buf.put((byte) mClientMac.length); // Hardware Address Length
+ buf.put((byte) 0); // Hop Count
+ buf.putInt(mTransId); // Transaction ID
+ buf.putShort(mSecs); // Elapsed Seconds
+
+ if (broadcast) {
+ buf.putShort((short) 0x8000); // Flags
+ } else {
+ buf.putShort((short) 0x0000); // Flags
+ }
+
+ buf.put(mClientIp.getAddress());
+ buf.put(mYourIp.getAddress());
+ buf.put(mNextIp.getAddress());
+ buf.put(mRelayIp.getAddress());
+ buf.put(mClientMac);
+ buf.position(buf.position() +
+ (HWADDR_LEN - mClientMac.length) // pad addr to 16 bytes
+ + 64 // empty server host name (64 bytes)
+ + 128); // empty boot file name (128 bytes)
+ buf.putInt(DHCP_MAGIC_COOKIE); // magic number
+ finishPacket(buf);
+
+ // round up to an even number of octets
+ if ((buf.position() & 1) == 1) {
+ buf.put((byte) 0);
+ }
+
+ // If an IP packet is being built, the IP & UDP checksums must be
+ // computed.
+ if (encap <= ENCAP_L3) {
+ // fix UDP header: insert length
+ short udpLen = (short)(buf.position() - udpHeaderOffset);
+ buf.putShort(udpLengthOffset, udpLen);
+ // fix UDP header: checksum
+ // checksum for UDP at udpChecksumOffset
+ int udpSeed = 0;
+
+ // apply IPv4 pseudo-header. Read IP address src and destination
+ // values from the IP header and accumulate checksum.
+ udpSeed += intAbs(buf.getShort(ipChecksumOffset + 2));
+ udpSeed += intAbs(buf.getShort(ipChecksumOffset + 4));
+ udpSeed += intAbs(buf.getShort(ipChecksumOffset + 6));
+ udpSeed += intAbs(buf.getShort(ipChecksumOffset + 8));
+
+ // accumulate extra data for the pseudo-header
+ udpSeed += IP_TYPE_UDP;
+ udpSeed += udpLen;
+ // and compute UDP checksum
+ buf.putShort(udpChecksumOffset, (short) checksum(buf, udpSeed,
+ udpHeaderOffset,
+ buf.position()));
+ // fix IP header: insert length
+ buf.putShort(ipLengthOffset, (short)(buf.position() - ipHeaderOffset));
+ // fixup IP-header checksum
+ buf.putShort(ipChecksumOffset,
+ (short) checksum(buf, 0, ipHeaderOffset, endIpHeader));
+ }
+ }
+
+ /**
+ * 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 int checksum(ByteBuffer buf, int seed, int start, int end) {
+ int sum = seed;
+ 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);
+
+ short[] shortArray = new short[(end - start) / 2];
+ shortBuf.get(shortArray);
+
+ for (short s : shortArray) {
+ sum += intAbs(s);
+ }
+
+ start += shortArray.length * 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);
+ }
+
+ /**
+ * Adds an optional parameter containing a single byte value.
+ */
+ protected static void addTlv(ByteBuffer buf, byte type, byte value) {
+ buf.put(type);
+ buf.put((byte) 1);
+ buf.put(value);
+ }
+
+ /**
+ * Adds an optional parameter containing an array of bytes.
+ *
+ * <p>This method is a no-op if the payload argument is null.
+ */
+ protected static void addTlv(ByteBuffer buf, byte type, @Nullable byte[] payload) {
+ if (payload != null) {
+ if (payload.length > MAX_OPTION_LEN) {
+ throw new IllegalArgumentException("DHCP option too long: "
+ + payload.length + " vs. " + MAX_OPTION_LEN);
+ }
+ buf.put(type);
+ buf.put((byte) payload.length);
+ buf.put(payload);
+ }
+ }
+
+ /**
+ * Adds an optional parameter containing an IP address.
+ *
+ * <p>This method is a no-op if the address argument is null.
+ */
+ protected static void addTlv(ByteBuffer buf, byte type, @Nullable Inet4Address addr) {
+ if (addr != null) {
+ addTlv(buf, type, addr.getAddress());
+ }
+ }
+
+ /**
+ * Adds an optional parameter containing a list of IP addresses.
+ *
+ * <p>This method is a no-op if the addresses argument is null or empty.
+ */
+ protected static void addTlv(ByteBuffer buf, byte type, @Nullable List<Inet4Address> addrs) {
+ if (addrs == null || addrs.size() == 0) return;
+
+ int optionLen = 4 * addrs.size();
+ if (optionLen > MAX_OPTION_LEN) {
+ throw new IllegalArgumentException("DHCP option too long: "
+ + optionLen + " vs. " + MAX_OPTION_LEN);
+ }
+
+ buf.put(type);
+ buf.put((byte)(optionLen));
+
+ for (Inet4Address addr : addrs) {
+ buf.put(addr.getAddress());
+ }
+ }
+
+ /**
+ * Adds an optional parameter containing a short integer.
+ *
+ * <p>This method is a no-op if the value argument is null.
+ */
+ protected static void addTlv(ByteBuffer buf, byte type, @Nullable Short value) {
+ if (value != null) {
+ buf.put(type);
+ buf.put((byte) 2);
+ buf.putShort(value.shortValue());
+ }
+ }
+
+ /**
+ * Adds an optional parameter containing a simple integer.
+ *
+ * <p>This method is a no-op if the value argument is null.
+ */
+ protected static void addTlv(ByteBuffer buf, byte type, @Nullable Integer value) {
+ if (value != null) {
+ buf.put(type);
+ buf.put((byte) 4);
+ buf.putInt(value.intValue());
+ }
+ }
+
+ /**
+ * Adds an optional parameter containing an ASCII string.
+ *
+ * <p>This method is a no-op if the string argument is null.
+ */
+ protected static void addTlv(ByteBuffer buf, byte type, @Nullable String str) {
+ if (str != null) {
+ try {
+ addTlv(buf, type, str.getBytes("US-ASCII"));
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException("String is not US-ASCII: " + str);
+ }
+ }
+ }
+
+ /**
+ * Adds the special end-of-optional-parameters indicator.
+ */
+ protected static void addTlvEnd(ByteBuffer buf) {
+ buf.put((byte) 0xFF);
+ }
+
+ private String getVendorId() {
+ if (testOverrideVendorId != null) return testOverrideVendorId;
+ return "android-dhcp-" + Build.VERSION.RELEASE;
+ }
+
+ private String getHostname() {
+ if (testOverrideHostname != null) return testOverrideHostname;
+ return SystemProperties.get("net.hostname");
+ }
+
+ /**
+ * Adds common client TLVs.
+ *
+ * TODO: Does this belong here? The alternative would be to modify all the buildXyzPacket
+ * methods to take them.
+ */
+ protected void addCommonClientTlvs(ByteBuffer buf) {
+ addTlv(buf, DHCP_MAX_MESSAGE_SIZE, (short) MAX_LENGTH);
+ addTlv(buf, DHCP_VENDOR_CLASS_ID, getVendorId());
+ final String hn = getHostname();
+ if (!TextUtils.isEmpty(hn)) addTlv(buf, DHCP_HOST_NAME, hn);
+ }
+
+ protected void addCommonServerTlvs(ByteBuffer buf) {
+ addTlv(buf, DHCP_LEASE_TIME, mLeaseTime);
+ if (mLeaseTime != null && mLeaseTime != INFINITE_LEASE) {
+ // The client should renew at 1/2 the lease-expiry interval
+ addTlv(buf, DHCP_RENEWAL_TIME, (int) (Integer.toUnsignedLong(mLeaseTime) / 2));
+ // Default rebinding time is set as below by RFC2131
+ addTlv(buf, DHCP_REBINDING_TIME,
+ (int) (Integer.toUnsignedLong(mLeaseTime) * 875L / 1000L));
+ }
+ addTlv(buf, DHCP_SUBNET_MASK, mSubnetMask);
+ addTlv(buf, DHCP_BROADCAST_ADDRESS, mBroadcastAddress);
+ addTlv(buf, DHCP_ROUTER, mGateways);
+ addTlv(buf, DHCP_DNS_SERVER, mDnsServers);
+ addTlv(buf, DHCP_DOMAIN_NAME, mDomainName);
+ addTlv(buf, DHCP_HOST_NAME, mHostName);
+ addTlv(buf, DHCP_VENDOR_INFO, mVendorInfo);
+ if (mMtu != null && Short.toUnsignedInt(mMtu) >= IPV4_MIN_MTU) {
+ addTlv(buf, DHCP_MTU, mMtu);
+ }
+ }
+
+ /**
+ * Converts a MAC from an array of octets to an ASCII string.
+ */
+ public static String macToString(byte[] mac) {
+ String macAddr = "";
+
+ for (int i = 0; i < mac.length; i++) {
+ String hexString = "0" + Integer.toHexString(mac[i]);
+
+ // substring operation grabs the last 2 digits: this
+ // allows signed bytes to be converted correctly.
+ macAddr += hexString.substring(hexString.length() - 2);
+
+ if (i != (mac.length - 1)) {
+ macAddr += ":";
+ }
+ }
+
+ return macAddr;
+ }
+
+ public String toString() {
+ String macAddr = macToString(mClientMac);
+
+ return macAddr;
+ }
+
+ /**
+ * Reads a four-octet value from a ByteBuffer and construct
+ * an IPv4 address from that value.
+ */
+ private static Inet4Address readIpAddress(ByteBuffer packet) {
+ Inet4Address result = null;
+ byte[] ipAddr = new byte[4];
+ packet.get(ipAddr);
+
+ try {
+ result = (Inet4Address) Inet4Address.getByAddress(ipAddr);
+ } catch (UnknownHostException ex) {
+ // ipAddr is numeric, so this should not be
+ // triggered. However, if it is, just nullify
+ result = null;
+ }
+
+ return result;
+ }
+
+ /**
+ * Reads a string of specified length from the buffer.
+ */
+ private static String readAsciiString(ByteBuffer buf, int byteCount, boolean nullOk) {
+ byte[] bytes = new byte[byteCount];
+ buf.get(bytes);
+ int length = bytes.length;
+ if (!nullOk) {
+ // Stop at the first null byte. This is because some DHCP options (e.g., the domain
+ // name) are passed to netd via FrameworkListener, which refuses arguments containing
+ // null bytes. We don't do this by default because vendorInfo is an opaque string which
+ // could in theory contain null bytes.
+ for (length = 0; length < bytes.length; length++) {
+ if (bytes[length] == 0) {
+ break;
+ }
+ }
+ }
+ return new String(bytes, 0, length, StandardCharsets.US_ASCII);
+ }
+
+ private static boolean isPacketToOrFromClient(short udpSrcPort, short udpDstPort) {
+ return (udpSrcPort == DHCP_CLIENT) || (udpDstPort == DHCP_CLIENT);
+ }
+
+ private static boolean isPacketServerToServer(short udpSrcPort, short udpDstPort) {
+ return (udpSrcPort == DHCP_SERVER) && (udpDstPort == DHCP_SERVER);
+ }
+
+ public static class ParseException extends Exception {
+ public final int errorCode;
+ public ParseException(int errorCode, String msg, Object... args) {
+ super(String.format(msg, args));
+ this.errorCode = errorCode;
+ }
+ }
+
+ /**
+ * Creates a concrete DhcpPacket from the supplied ByteBuffer. The
+ * buffer may have an L2 encapsulation (which is the full EthernetII
+ * format starting with the source-address MAC) or an L3 encapsulation
+ * (which starts with the IP header).
+ * <br>
+ * A subset of the optional parameters are parsed and are stored
+ * in object fields.
+ */
+ @VisibleForTesting
+ static DhcpPacket decodeFullPacket(ByteBuffer packet, int pktType) throws ParseException
+ {
+ // bootp parameters
+ int transactionId;
+ short secs;
+ Inet4Address clientIp;
+ Inet4Address yourIp;
+ Inet4Address nextIp;
+ Inet4Address relayIp;
+ byte[] clientMac;
+ byte[] clientId = null;
+ List<Inet4Address> dnsServers = new ArrayList<>();
+ List<Inet4Address> gateways = new ArrayList<>(); // aka router
+ Inet4Address serverIdentifier = null;
+ Inet4Address netMask = null;
+ String message = null;
+ String vendorId = null;
+ String vendorInfo = null;
+ byte[] expectedParams = null;
+ String hostName = null;
+ String domainName = null;
+ Inet4Address ipSrc = null;
+ Inet4Address ipDst = null;
+ Inet4Address bcAddr = null;
+ Inet4Address requestedIp = null;
+
+ // The following are all unsigned integers. Internally we store them as signed integers of
+ // the same length because that way we're guaranteed that they can't be out of the range of
+ // the unsigned field in the packet. Callers wanting to pass in an unsigned value will need
+ // to cast it.
+ Short mtu = null;
+ Short maxMessageSize = null;
+ Integer leaseTime = null;
+ Integer T1 = null;
+ Integer T2 = null;
+
+ // dhcp options
+ byte dhcpType = (byte) 0xFF;
+
+ packet.order(ByteOrder.BIG_ENDIAN);
+
+ // check to see if we need to parse L2, IP, and UDP encaps
+ if (pktType == ENCAP_L2) {
+ if (packet.remaining() < MIN_PACKET_LENGTH_L2) {
+ throw new ParseException(DhcpErrorEvent.L2_TOO_SHORT,
+ "L2 packet too short, %d < %d", packet.remaining(), MIN_PACKET_LENGTH_L2);
+ }
+
+ byte[] l2dst = new byte[6];
+ byte[] l2src = new byte[6];
+
+ packet.get(l2dst);
+ packet.get(l2src);
+
+ short l2type = packet.getShort();
+
+ if (l2type != OsConstants.ETH_P_IP) {
+ throw new ParseException(DhcpErrorEvent.L2_WRONG_ETH_TYPE,
+ "Unexpected L2 type 0x%04x, expected 0x%04x", l2type, OsConstants.ETH_P_IP);
+ }
+ }
+
+ if (pktType <= ENCAP_L3) {
+ if (packet.remaining() < MIN_PACKET_LENGTH_L3) {
+ throw new ParseException(DhcpErrorEvent.L3_TOO_SHORT,
+ "L3 packet too short, %d < %d", packet.remaining(), MIN_PACKET_LENGTH_L3);
+ }
+
+ byte ipTypeAndLength = packet.get();
+ int ipVersion = (ipTypeAndLength & 0xf0) >> 4;
+ if (ipVersion != 4) {
+ throw new ParseException(
+ DhcpErrorEvent.L3_NOT_IPV4, "Invalid IP version %d", ipVersion);
+ }
+
+ // System.out.println("ipType is " + ipType);
+ byte ipDiffServicesField = packet.get();
+ short ipTotalLength = packet.getShort();
+ short ipIdentification = packet.getShort();
+ byte ipFlags = packet.get();
+ byte ipFragOffset = packet.get();
+ byte ipTTL = packet.get();
+ byte ipProto = packet.get();
+ short ipChksm = packet.getShort();
+
+ ipSrc = readIpAddress(packet);
+ ipDst = readIpAddress(packet);
+
+ if (ipProto != IP_TYPE_UDP) {
+ throw new ParseException(
+ DhcpErrorEvent.L4_NOT_UDP, "Protocol not UDP: %d", ipProto);
+ }
+
+ // Skip options. This cannot cause us to read beyond the end of the buffer because the
+ // IPv4 header cannot be more than (0x0f * 4) = 60 bytes long, and that is less than
+ // MIN_PACKET_LENGTH_L3.
+ int optionWords = ((ipTypeAndLength & 0x0f) - 5);
+ for (int i = 0; i < optionWords; i++) {
+ packet.getInt();
+ }
+
+ // assume UDP
+ short udpSrcPort = packet.getShort();
+ short udpDstPort = packet.getShort();
+ short udpLen = packet.getShort();
+ short udpChkSum = packet.getShort();
+
+ // Only accept packets to or from the well-known client port (expressly permitting
+ // packets from ports other than the well-known server port; http://b/24687559), and
+ // server-to-server packets, e.g. for relays.
+ if (!isPacketToOrFromClient(udpSrcPort, udpDstPort) &&
+ !isPacketServerToServer(udpSrcPort, udpDstPort)) {
+ // This should almost never happen because we use SO_ATTACH_FILTER on the packet
+ // socket to drop packets that don't have the right source ports. However, it's
+ // possible that a packet arrives between when the socket is bound and when the
+ // filter is set. http://b/26696823 .
+ throw new ParseException(DhcpErrorEvent.L4_WRONG_PORT,
+ "Unexpected UDP ports %d->%d", udpSrcPort, udpDstPort);
+ }
+ }
+
+ // We need to check the length even for ENCAP_L3 because the IPv4 header is variable-length.
+ if (pktType > ENCAP_BOOTP || packet.remaining() < MIN_PACKET_LENGTH_BOOTP) {
+ throw new ParseException(DhcpErrorEvent.BOOTP_TOO_SHORT,
+ "Invalid type or BOOTP packet too short, %d < %d",
+ packet.remaining(), MIN_PACKET_LENGTH_BOOTP);
+ }
+
+ byte type = packet.get();
+ byte hwType = packet.get();
+ int addrLen = packet.get() & 0xff;
+ byte hops = packet.get();
+ transactionId = packet.getInt();
+ secs = packet.getShort();
+ short bootpFlags = packet.getShort();
+ boolean broadcast = (bootpFlags & 0x8000) != 0;
+ byte[] ipv4addr = new byte[4];
+
+ try {
+ packet.get(ipv4addr);
+ clientIp = (Inet4Address) Inet4Address.getByAddress(ipv4addr);
+ packet.get(ipv4addr);
+ yourIp = (Inet4Address) Inet4Address.getByAddress(ipv4addr);
+ packet.get(ipv4addr);
+ nextIp = (Inet4Address) Inet4Address.getByAddress(ipv4addr);
+ packet.get(ipv4addr);
+ relayIp = (Inet4Address) Inet4Address.getByAddress(ipv4addr);
+ } catch (UnknownHostException ex) {
+ throw new ParseException(DhcpErrorEvent.L3_INVALID_IP,
+ "Invalid IPv4 address: %s", Arrays.toString(ipv4addr));
+ }
+
+ // Some DHCP servers have been known to announce invalid client hardware address values such
+ // as 0xff. The legacy DHCP client accepted these becuause it does not check the length at
+ // all but only checks that the interface MAC address matches the first bytes of the address
+ // in the packets. We're a bit stricter: if the length is obviously invalid (i.e., bigger
+ // than the size of the field), we fudge it to 6 (Ethernet). http://b/23725795
+ // TODO: evaluate whether to make this test more liberal.
+ if (addrLen > HWADDR_LEN) {
+ addrLen = ETHER_BROADCAST.length;
+ }
+
+ clientMac = new byte[addrLen];
+ packet.get(clientMac);
+
+ // skip over address padding (16 octets allocated)
+ packet.position(packet.position() + (16 - addrLen)
+ + 64 // skip server host name (64 chars)
+ + 128); // skip boot file name (128 chars)
+
+ // Ensure this is a DHCP packet with a magic cookie, and not BOOTP. http://b/31850211
+ if (packet.remaining() < 4) {
+ throw new ParseException(DhcpErrorEvent.DHCP_NO_COOKIE, "not a DHCP message");
+ }
+
+ int dhcpMagicCookie = packet.getInt();
+ if (dhcpMagicCookie != DHCP_MAGIC_COOKIE) {
+ throw new ParseException(DhcpErrorEvent.DHCP_BAD_MAGIC_COOKIE,
+ "Bad magic cookie 0x%08x, should be 0x%08x",
+ dhcpMagicCookie, DHCP_MAGIC_COOKIE);
+ }
+
+ // parse options
+ boolean notFinishedOptions = true;
+
+ while ((packet.position() < packet.limit()) && notFinishedOptions) {
+ final byte optionType = packet.get(); // cannot underflow because position < limit
+ try {
+ if (optionType == DHCP_OPTION_END) {
+ notFinishedOptions = false;
+ } else if (optionType == DHCP_OPTION_PAD) {
+ // The pad option doesn't have a length field. Nothing to do.
+ } else {
+ int optionLen = packet.get() & 0xFF;
+ int expectedLen = 0;
+
+ switch(optionType) {
+ case DHCP_SUBNET_MASK:
+ netMask = readIpAddress(packet);
+ expectedLen = 4;
+ break;
+ case DHCP_ROUTER:
+ for (expectedLen = 0; expectedLen < optionLen; expectedLen += 4) {
+ gateways.add(readIpAddress(packet));
+ }
+ break;
+ case DHCP_DNS_SERVER:
+ for (expectedLen = 0; expectedLen < optionLen; expectedLen += 4) {
+ dnsServers.add(readIpAddress(packet));
+ }
+ break;
+ case DHCP_HOST_NAME:
+ expectedLen = optionLen;
+ hostName = readAsciiString(packet, optionLen, false);
+ break;
+ case DHCP_MTU:
+ expectedLen = 2;
+ mtu = packet.getShort();
+ break;
+ case DHCP_DOMAIN_NAME:
+ expectedLen = optionLen;
+ domainName = readAsciiString(packet, optionLen, false);
+ break;
+ case DHCP_BROADCAST_ADDRESS:
+ bcAddr = readIpAddress(packet);
+ expectedLen = 4;
+ break;
+ case DHCP_REQUESTED_IP:
+ requestedIp = readIpAddress(packet);
+ expectedLen = 4;
+ break;
+ case DHCP_LEASE_TIME:
+ leaseTime = Integer.valueOf(packet.getInt());
+ expectedLen = 4;
+ break;
+ case DHCP_MESSAGE_TYPE:
+ dhcpType = packet.get();
+ expectedLen = 1;
+ break;
+ case DHCP_SERVER_IDENTIFIER:
+ serverIdentifier = readIpAddress(packet);
+ expectedLen = 4;
+ break;
+ case DHCP_PARAMETER_LIST:
+ expectedParams = new byte[optionLen];
+ packet.get(expectedParams);
+ expectedLen = optionLen;
+ break;
+ case DHCP_MESSAGE:
+ expectedLen = optionLen;
+ message = readAsciiString(packet, optionLen, false);
+ break;
+ case DHCP_MAX_MESSAGE_SIZE:
+ expectedLen = 2;
+ maxMessageSize = Short.valueOf(packet.getShort());
+ break;
+ case DHCP_RENEWAL_TIME:
+ expectedLen = 4;
+ T1 = Integer.valueOf(packet.getInt());
+ break;
+ case DHCP_REBINDING_TIME:
+ expectedLen = 4;
+ T2 = Integer.valueOf(packet.getInt());
+ break;
+ case DHCP_VENDOR_CLASS_ID:
+ expectedLen = optionLen;
+ // Embedded nulls are safe as this does not get passed to netd.
+ vendorId = readAsciiString(packet, optionLen, true);
+ break;
+ case DHCP_CLIENT_IDENTIFIER: { // Client identifier
+ byte[] id = new byte[optionLen];
+ packet.get(id);
+ expectedLen = optionLen;
+ } break;
+ case DHCP_VENDOR_INFO:
+ expectedLen = optionLen;
+ // Embedded nulls are safe as this does not get passed to netd.
+ vendorInfo = readAsciiString(packet, optionLen, true);
+ break;
+ default:
+ // ignore any other parameters
+ for (int i = 0; i < optionLen; i++) {
+ expectedLen++;
+ byte throwaway = packet.get();
+ }
+ }
+
+ if (expectedLen != optionLen) {
+ final int errorCode = DhcpErrorEvent.errorCodeWithOption(
+ DhcpErrorEvent.DHCP_INVALID_OPTION_LENGTH, optionType);
+ throw new ParseException(errorCode,
+ "Invalid length %d for option %d, expected %d",
+ optionLen, optionType, expectedLen);
+ }
+ }
+ } catch (BufferUnderflowException e) {
+ final int errorCode = DhcpErrorEvent.errorCodeWithOption(
+ DhcpErrorEvent.BUFFER_UNDERFLOW, optionType);
+ throw new ParseException(errorCode, "BufferUnderflowException");
+ }
+ }
+
+ DhcpPacket newPacket;
+
+ switch(dhcpType) {
+ case (byte) 0xFF:
+ throw new ParseException(DhcpErrorEvent.DHCP_NO_MSG_TYPE,
+ "No DHCP message type option");
+ case DHCP_MESSAGE_TYPE_DISCOVER:
+ newPacket = new DhcpDiscoverPacket(transactionId, secs, relayIp, clientMac,
+ broadcast, ipSrc);
+ break;
+ case DHCP_MESSAGE_TYPE_OFFER:
+ newPacket = new DhcpOfferPacket(
+ transactionId, secs, broadcast, ipSrc, relayIp, clientIp, yourIp, clientMac);
+ break;
+ case DHCP_MESSAGE_TYPE_REQUEST:
+ newPacket = new DhcpRequestPacket(
+ transactionId, secs, clientIp, relayIp, clientMac, broadcast);
+ break;
+ case DHCP_MESSAGE_TYPE_DECLINE:
+ newPacket = new DhcpDeclinePacket(
+ transactionId, secs, clientIp, yourIp, nextIp, relayIp,
+ clientMac);
+ break;
+ case DHCP_MESSAGE_TYPE_ACK:
+ newPacket = new DhcpAckPacket(
+ transactionId, secs, broadcast, ipSrc, relayIp, clientIp, yourIp, clientMac);
+ break;
+ case DHCP_MESSAGE_TYPE_NAK:
+ newPacket = new DhcpNakPacket(
+ transactionId, secs, relayIp, clientMac, broadcast);
+ break;
+ case DHCP_MESSAGE_TYPE_RELEASE:
+ if (serverIdentifier == null) {
+ throw new ParseException(DhcpErrorEvent.MISC_ERROR,
+ "DHCPRELEASE without server identifier");
+ }
+ newPacket = new DhcpReleasePacket(
+ transactionId, serverIdentifier, clientIp, relayIp, clientMac);
+ break;
+ case DHCP_MESSAGE_TYPE_INFORM:
+ newPacket = new DhcpInformPacket(
+ transactionId, secs, clientIp, yourIp, nextIp, relayIp,
+ clientMac);
+ break;
+ default:
+ throw new ParseException(DhcpErrorEvent.DHCP_UNKNOWN_MSG_TYPE,
+ "Unimplemented DHCP type %d", dhcpType);
+ }
+
+ newPacket.mBroadcastAddress = bcAddr;
+ newPacket.mClientId = clientId;
+ newPacket.mDnsServers = dnsServers;
+ newPacket.mDomainName = domainName;
+ newPacket.mGateways = gateways;
+ newPacket.mHostName = hostName;
+ newPacket.mLeaseTime = leaseTime;
+ newPacket.mMessage = message;
+ newPacket.mMtu = mtu;
+ newPacket.mRequestedIp = requestedIp;
+ newPacket.mRequestedParams = expectedParams;
+ newPacket.mServerIdentifier = serverIdentifier;
+ newPacket.mSubnetMask = netMask;
+ newPacket.mMaxMessageSize = maxMessageSize;
+ newPacket.mT1 = T1;
+ newPacket.mT2 = T2;
+ newPacket.mVendorId = vendorId;
+ newPacket.mVendorInfo = vendorInfo;
+ return newPacket;
+ }
+
+ /**
+ * Parse a packet from an array of bytes, stopping at the given length.
+ */
+ public static DhcpPacket decodeFullPacket(byte[] packet, int length, int pktType)
+ throws ParseException {
+ ByteBuffer buffer = ByteBuffer.wrap(packet, 0, length).order(ByteOrder.BIG_ENDIAN);
+ try {
+ return decodeFullPacket(buffer, pktType);
+ } catch (ParseException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ParseException(DhcpErrorEvent.PARSING_ERROR, e.getMessage());
+ }
+ }
+
+ /**
+ * Construct a DhcpResults object from a DHCP reply packet.
+ */
+ public DhcpResults toDhcpResults() {
+ Inet4Address ipAddress = mYourIp;
+ if (ipAddress.equals(Inet4Address.ANY)) {
+ ipAddress = mClientIp;
+ if (ipAddress.equals(Inet4Address.ANY)) {
+ return null;
+ }
+ }
+
+ int prefixLength;
+ if (mSubnetMask != null) {
+ try {
+ prefixLength = NetworkUtils.netmaskToPrefixLength(mSubnetMask);
+ } catch (IllegalArgumentException e) {
+ // Non-contiguous netmask.
+ return null;
+ }
+ } else {
+ prefixLength = NetworkUtils.getImplicitNetmask(ipAddress);
+ }
+
+ DhcpResults results = new DhcpResults();
+ try {
+ results.ipAddress = new LinkAddress(ipAddress, prefixLength);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+
+ if (mGateways.size() > 0) {
+ results.gateway = mGateways.get(0);
+ }
+
+ results.dnsServers.addAll(mDnsServers);
+ results.domains = mDomainName;
+ results.serverAddress = mServerIdentifier;
+ results.vendorInfo = mVendorInfo;
+ results.leaseDuration = (mLeaseTime != null) ? mLeaseTime : INFINITE_LEASE;
+ results.mtu = (mMtu != null && MIN_MTU <= mMtu && mMtu <= MAX_MTU) ? mMtu : 0;
+
+ return results;
+ }
+
+ /**
+ * Returns the parsed lease time, in milliseconds, or 0 for infinite.
+ */
+ public long getLeaseTimeMillis() {
+ // dhcpcd treats the lack of a lease time option as an infinite lease.
+ if (mLeaseTime == null || mLeaseTime == INFINITE_LEASE) {
+ return 0;
+ } else if (0 <= mLeaseTime && mLeaseTime < MINIMUM_LEASE) {
+ return MINIMUM_LEASE * 1000;
+ } else {
+ return (mLeaseTime & 0xffffffffL) * 1000;
+ }
+ }
+
+ /**
+ * Builds a DHCP-DISCOVER packet from the required specified
+ * parameters.
+ */
+ public static ByteBuffer buildDiscoverPacket(int encap, int transactionId,
+ short secs, byte[] clientMac, boolean broadcast, byte[] expectedParams) {
+ DhcpPacket pkt = new DhcpDiscoverPacket(transactionId, secs, INADDR_ANY /* relayIp */,
+ clientMac, broadcast, INADDR_ANY /* srcIp */);
+ pkt.mRequestedParams = expectedParams;
+ return pkt.buildPacket(encap, DHCP_SERVER, DHCP_CLIENT);
+ }
+
+ /**
+ * Builds a DHCP-OFFER packet from the required specified
+ * parameters.
+ */
+ public static ByteBuffer buildOfferPacket(int encap, int transactionId,
+ boolean broadcast, Inet4Address serverIpAddr, Inet4Address relayIp,
+ Inet4Address yourIp, byte[] mac, Integer timeout, Inet4Address netMask,
+ Inet4Address bcAddr, List<Inet4Address> gateways, List<Inet4Address> dnsServers,
+ Inet4Address dhcpServerIdentifier, String domainName, String hostname, boolean metered,
+ short mtu) {
+ DhcpPacket pkt = new DhcpOfferPacket(
+ transactionId, (short) 0, broadcast, serverIpAddr, relayIp,
+ INADDR_ANY /* clientIp */, yourIp, mac);
+ pkt.mGateways = gateways;
+ pkt.mDnsServers = dnsServers;
+ pkt.mLeaseTime = timeout;
+ pkt.mDomainName = domainName;
+ pkt.mHostName = hostname;
+ pkt.mServerIdentifier = dhcpServerIdentifier;
+ pkt.mSubnetMask = netMask;
+ pkt.mBroadcastAddress = bcAddr;
+ pkt.mMtu = mtu;
+ if (metered) {
+ pkt.mVendorInfo = VENDOR_INFO_ANDROID_METERED;
+ }
+ return pkt.buildPacket(encap, DHCP_CLIENT, DHCP_SERVER);
+ }
+
+ /**
+ * Builds a DHCP-ACK packet from the required specified parameters.
+ */
+ public static ByteBuffer buildAckPacket(int encap, int transactionId,
+ boolean broadcast, Inet4Address serverIpAddr, Inet4Address relayIp, Inet4Address yourIp,
+ Inet4Address requestClientIp, byte[] mac, Integer timeout, Inet4Address netMask,
+ Inet4Address bcAddr, List<Inet4Address> gateways, List<Inet4Address> dnsServers,
+ Inet4Address dhcpServerIdentifier, String domainName, String hostname, boolean metered,
+ short mtu) {
+ DhcpPacket pkt = new DhcpAckPacket(
+ transactionId, (short) 0, broadcast, serverIpAddr, relayIp, requestClientIp, yourIp,
+ mac);
+ pkt.mGateways = gateways;
+ pkt.mDnsServers = dnsServers;
+ pkt.mLeaseTime = timeout;
+ pkt.mDomainName = domainName;
+ pkt.mHostName = hostname;
+ pkt.mSubnetMask = netMask;
+ pkt.mServerIdentifier = dhcpServerIdentifier;
+ pkt.mBroadcastAddress = bcAddr;
+ pkt.mMtu = mtu;
+ if (metered) {
+ pkt.mVendorInfo = VENDOR_INFO_ANDROID_METERED;
+ }
+ return pkt.buildPacket(encap, DHCP_CLIENT, DHCP_SERVER);
+ }
+
+ /**
+ * Builds a DHCP-NAK packet from the required specified parameters.
+ */
+ public static ByteBuffer buildNakPacket(int encap, int transactionId, Inet4Address serverIpAddr,
+ Inet4Address relayIp, byte[] mac, boolean broadcast, String message) {
+ DhcpPacket pkt = new DhcpNakPacket(
+ transactionId, (short) 0, relayIp, mac, broadcast);
+ pkt.mMessage = message;
+ pkt.mServerIdentifier = serverIpAddr;
+ return pkt.buildPacket(encap, DHCP_CLIENT, DHCP_SERVER);
+ }
+
+ /**
+ * Builds a DHCP-REQUEST packet from the required specified parameters.
+ */
+ public static ByteBuffer buildRequestPacket(int encap,
+ int transactionId, short secs, Inet4Address clientIp, boolean broadcast,
+ byte[] clientMac, Inet4Address requestedIpAddress,
+ Inet4Address serverIdentifier, byte[] requestedParams, String hostName) {
+ DhcpPacket pkt = new DhcpRequestPacket(transactionId, secs, clientIp,
+ INADDR_ANY /* relayIp */, clientMac, broadcast);
+ pkt.mRequestedIp = requestedIpAddress;
+ pkt.mServerIdentifier = serverIdentifier;
+ pkt.mHostName = hostName;
+ pkt.mRequestedParams = requestedParams;
+ ByteBuffer result = pkt.buildPacket(encap, DHCP_SERVER, DHCP_CLIENT);
+ return result;
+ }
+}
diff --git a/src/android/net/dhcp/DhcpReleasePacket.java b/src/android/net/dhcp/DhcpReleasePacket.java
new file mode 100644
index 0000000..3958303
--- /dev/null
+++ b/src/android/net/dhcp/DhcpReleasePacket.java
@@ -0,0 +1,58 @@
+/*
+ * 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.dhcp;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * Implements DHCP-RELEASE
+ */
+class DhcpReleasePacket extends DhcpPacket {
+
+ final Inet4Address mClientAddr;
+
+ /**
+ * Generates a RELEASE packet with the specified parameters.
+ */
+ public DhcpReleasePacket(int transId, Inet4Address serverId, Inet4Address clientAddr,
+ Inet4Address relayIp, byte[] clientMac) {
+ super(transId, (short)0, clientAddr, INADDR_ANY /* yourIp */, INADDR_ANY /* nextIp */,
+ relayIp, clientMac, false /* broadcast */);
+ mServerIdentifier = serverId;
+ mClientAddr = clientAddr;
+ }
+
+
+ @Override
+ public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+ ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+ fillInPacket(encap, mServerIdentifier /* destIp */, mClientIp /* srcIp */, destUdp, srcUdp,
+ result, DHCP_BOOTREPLY, mBroadcast);
+ result.flip();
+ return result;
+ }
+
+ @Override
+ void finishPacket(ByteBuffer buffer) {
+ addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_RELEASE);
+ addTlv(buffer, DHCP_CLIENT_IDENTIFIER, getClientId());
+ addTlv(buffer, DHCP_SERVER_IDENTIFIER, mServerIdentifier);
+ addCommonClientTlvs(buffer);
+ addTlvEnd(buffer);
+ }
+}
diff --git a/src/android/net/dhcp/DhcpRequestPacket.java b/src/android/net/dhcp/DhcpRequestPacket.java
new file mode 100644
index 0000000..231d045
--- /dev/null
+++ b/src/android/net/dhcp/DhcpRequestPacket.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 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.dhcp;
+
+import android.util.Log;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+
+/**
+ * This class implements the DHCP-REQUEST packet.
+ */
+class DhcpRequestPacket extends DhcpPacket {
+ /**
+ * Generates a REQUEST packet with the specified parameters.
+ */
+ DhcpRequestPacket(int transId, short secs, Inet4Address clientIp, Inet4Address relayIp,
+ byte[] clientMac, boolean broadcast) {
+ super(transId, secs, clientIp, INADDR_ANY, INADDR_ANY, relayIp, clientMac, broadcast);
+ }
+
+ public String toString() {
+ String s = super.toString();
+ return s + " REQUEST, desired IP " + mRequestedIp + " from host '"
+ + mHostName + "', param list length "
+ + (mRequestedParams == null ? 0 : mRequestedParams.length);
+ }
+
+ /**
+ * Fills in a packet with the requested REQUEST attributes.
+ */
+ public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) {
+ ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH);
+
+ fillInPacket(encap, INADDR_BROADCAST, INADDR_ANY, destUdp, srcUdp,
+ result, DHCP_BOOTREQUEST, mBroadcast);
+ result.flip();
+ return result;
+ }
+
+ /**
+ * Adds the optional parameters to the client-generated REQUEST packet.
+ */
+ void finishPacket(ByteBuffer buffer) {
+ addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_REQUEST);
+ addTlv(buffer, DHCP_CLIENT_IDENTIFIER, getClientId());
+ if (!INADDR_ANY.equals(mRequestedIp)) {
+ addTlv(buffer, DHCP_REQUESTED_IP, mRequestedIp);
+ }
+ if (!INADDR_ANY.equals(mServerIdentifier)) {
+ addTlv(buffer, DHCP_SERVER_IDENTIFIER, mServerIdentifier);
+ }
+ addCommonClientTlvs(buffer);
+ addTlv(buffer, DHCP_PARAMETER_LIST, mRequestedParams);
+ addTlvEnd(buffer);
+ }
+}
diff --git a/src/android/net/ip/ConnectivityPacketTracker.java b/src/android/net/ip/ConnectivityPacketTracker.java
new file mode 100644
index 0000000..385dd52
--- /dev/null
+++ b/src/android/net/ip/ConnectivityPacketTracker.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.ip;
+
+import static android.net.util.SocketUtils.makePacketSocketAddress;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ARPHRD_ETHER;
+import static android.system.OsConstants.ETH_P_ALL;
+import static android.system.OsConstants.SOCK_RAW;
+
+import android.net.NetworkUtils;
+import android.net.util.ConnectivityPacketSummary;
+import android.net.util.InterfaceParams;
+import android.net.util.PacketReader;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.text.TextUtils;
+import android.util.LocalLog;
+import android.util.Log;
+
+import libcore.util.HexEncoding;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+
+/**
+ * Critical connectivity packet tracking daemon.
+ *
+ * Tracks ARP, DHCPv4, and IPv6 RS/RA/NS/NA packets.
+ *
+ * This class's constructor, start() and stop() methods must only be called
+ * from the same thread on which the passed in |log| is accessed.
+ *
+ * Log lines include a hexdump of the packet, which can be decoded via:
+ *
+ * echo -n H3XSTR1NG | sed -e 's/\([0-9A-F][0-9A-F]\)/\1 /g' -e 's/^/000000 /'
+ * | text2pcap - -
+ * | tcpdump -n -vv -e -r -
+ *
+ * @hide
+ */
+public class ConnectivityPacketTracker {
+ private static final String TAG = ConnectivityPacketTracker.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final String MARK_START = "--- START ---";
+ private static final String MARK_STOP = "--- STOP ---";
+ private static final String MARK_NAMED_START = "--- START (%s) ---";
+ private static final String MARK_NAMED_STOP = "--- STOP (%s) ---";
+
+ private final String mTag;
+ private final LocalLog mLog;
+ private final PacketReader mPacketListener;
+ private boolean mRunning;
+ private String mDisplayName;
+
+ public ConnectivityPacketTracker(Handler h, InterfaceParams ifParams, LocalLog log) {
+ if (ifParams == null) throw new IllegalArgumentException("null InterfaceParams");
+
+ mTag = TAG + "." + ifParams.name;
+ mLog = log;
+ mPacketListener = new PacketListener(h, ifParams);
+ }
+
+ public void start(String displayName) {
+ mRunning = true;
+ mDisplayName = displayName;
+ mPacketListener.start();
+ }
+
+ public void stop() {
+ mPacketListener.stop();
+ mRunning = false;
+ mDisplayName = null;
+ }
+
+ private final class PacketListener extends PacketReader {
+ private final InterfaceParams mInterface;
+
+ PacketListener(Handler h, InterfaceParams ifParams) {
+ super(h, ifParams.defaultMtu);
+ mInterface = ifParams;
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ FileDescriptor s = null;
+ try {
+ s = Os.socket(AF_PACKET, SOCK_RAW, 0);
+ NetworkUtils.attachControlPacketFilter(s, ARPHRD_ETHER);
+ Os.bind(s, makePacketSocketAddress((short) ETH_P_ALL, mInterface.index));
+ } catch (ErrnoException | IOException e) {
+ logError("Failed to create packet tracking socket: ", e);
+ closeFd(s);
+ return null;
+ }
+ return s;
+ }
+
+ @Override
+ protected void handlePacket(byte[] recvbuf, int length) {
+ final String summary = ConnectivityPacketSummary.summarize(
+ mInterface.macAddr, recvbuf, length);
+ if (summary == null) return;
+
+ if (DBG) Log.d(mTag, summary);
+ addLogEntry(summary +
+ "\n[" + new String(HexEncoding.encode(recvbuf, 0, length)) + "]");
+ }
+
+ @Override
+ protected void onStart() {
+ final String msg = TextUtils.isEmpty(mDisplayName)
+ ? MARK_START
+ : String.format(MARK_NAMED_START, mDisplayName);
+ mLog.log(msg);
+ }
+
+ @Override
+ protected void onStop() {
+ String msg = TextUtils.isEmpty(mDisplayName)
+ ? MARK_STOP
+ : String.format(MARK_NAMED_STOP, mDisplayName);
+ if (!mRunning) msg += " (packet listener stopped unexpectedly)";
+ mLog.log(msg);
+ }
+
+ @Override
+ protected void logError(String msg, Exception e) {
+ Log.e(mTag, msg, e);
+ addLogEntry(msg + e);
+ }
+
+ private void addLogEntry(String entry) {
+ mLog.log(entry);
+ }
+ }
+}
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
new file mode 100644
index 0000000..ad7f85d
--- /dev/null
+++ b/src/android/net/ip/IpClient.java
@@ -0,0 +1,1691 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.ip;
+
+import static android.net.shared.IpConfigurationParcelableUtil.toStableParcelable;
+import static android.net.shared.LinkPropertiesParcelableUtil.fromStableParcelable;
+import static android.net.shared.LinkPropertiesParcelableUtil.toStableParcelable;
+
+import static com.android.server.util.PermissionUtil.checkNetworkStackCallingPermission;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.DhcpResults;
+import android.net.INetd;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.ProvisioningConfigurationParcelable;
+import android.net.ProxyInfo;
+import android.net.ProxyInfoParcelable;
+import android.net.RouteInfo;
+import android.net.apf.ApfCapabilities;
+import android.net.apf.ApfFilter;
+import android.net.dhcp.DhcpClient;
+import android.net.ip.IIpClientCallbacks;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.IpManagerEvent;
+import android.net.shared.InitialConfiguration;
+import android.net.shared.NetdService;
+import android.net.shared.ProvisioningConfiguration;
+import android.net.util.InterfaceParams;
+import android.net.util.SharedLog;
+import android.os.ConditionVariable;
+import android.os.INetworkManagementService;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.LocalLog;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IState;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.MessageUtils;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.internal.util.WakeupMessage;
+import com.android.server.net.NetlinkTracker;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.net.InetAddress;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+
+/**
+ * IpClient
+ *
+ * This class provides the interface to IP-layer provisioning and maintenance
+ * functionality that can be used by transport layers like Wi-Fi, Ethernet,
+ * et cetera.
+ *
+ * [ Lifetime ]
+ * IpClient is designed to be instantiated as soon as the interface name is
+ * known and can be as long-lived as the class containing it (i.e. declaring
+ * it "private final" is okay).
+ *
+ * @hide
+ */
+public class IpClient extends StateMachine {
+ private static final boolean DBG = false;
+
+ // For message logging.
+ private static final Class[] sMessageClasses = { IpClient.class, DhcpClient.class };
+ private static final SparseArray<String> sWhatToString =
+ MessageUtils.findMessageNames(sMessageClasses);
+ // Two static concurrent hashmaps of interface name to logging classes.
+ // One holds StateMachine logs and the other connectivity packet logs.
+ private static final ConcurrentHashMap<String, SharedLog> sSmLogs = new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap<String, LocalLog> sPktLogs = new ConcurrentHashMap<>();
+
+ /**
+ * Dump all state machine and connectivity packet logs to the specified writer.
+ * @param skippedIfaces Interfaces for which logs should not be dumped.
+ */
+ public static void dumpAllLogs(PrintWriter writer, Set<String> skippedIfaces) {
+ for (String ifname : sSmLogs.keySet()) {
+ if (skippedIfaces.contains(ifname)) continue;
+
+ writer.println(String.format("--- BEGIN %s ---", ifname));
+
+ final SharedLog smLog = sSmLogs.get(ifname);
+ if (smLog != null) {
+ writer.println("State machine log:");
+ smLog.dump(null, writer, null);
+ }
+
+ writer.println("");
+
+ final LocalLog pktLog = sPktLogs.get(ifname);
+ if (pktLog != null) {
+ writer.println("Connectivity packet log:");
+ pktLog.readOnlyLocalLog().dump(null, writer, null);
+ }
+
+ writer.println(String.format("--- END %s ---", ifname));
+ }
+ }
+
+ // Use a wrapper class to log in order to ensure complete and detailed
+ // logging. This method is lighter weight than annotations/reflection
+ // and has the following benefits:
+ //
+ // - No invoked method can be forgotten.
+ // Any new method added to IpClient.Callback must be overridden
+ // here or it will never be called.
+ //
+ // - No invoking call site can be forgotten.
+ // Centralized logging in this way means call sites don't need to
+ // remember to log, and therefore no call site can be forgotten.
+ //
+ // - No variation in log format among call sites.
+ // Encourages logging of any available arguments, and all call sites
+ // are necessarily logged identically.
+ //
+ // NOTE: Log first because passed objects may or may not be thread-safe and
+ // once passed on to the callback they may be modified by another thread.
+ //
+ // TODO: Find an lighter weight approach.
+ public static class IpClientCallbacksWrapper {
+ private static final String PREFIX = "INVOKE ";
+ private final IIpClientCallbacks mCallback;
+ private final SharedLog mLog;
+
+ @VisibleForTesting
+ protected IpClientCallbacksWrapper(IIpClientCallbacks callback, SharedLog log) {
+ mCallback = callback;
+ mLog = log;
+ }
+
+ private void log(String msg) {
+ mLog.log(PREFIX + msg);
+ }
+
+ private void log(String msg, Throwable e) {
+ mLog.e(PREFIX + msg, e);
+ }
+
+ public void onPreDhcpAction() {
+ log("onPreDhcpAction()");
+ try {
+ mCallback.onPreDhcpAction();
+ } catch (RemoteException e) {
+ log("Failed to call onPreDhcpAction", e);
+ }
+ }
+
+ public void onPostDhcpAction() {
+ log("onPostDhcpAction()");
+ try {
+ mCallback.onPostDhcpAction();
+ } catch (RemoteException e) {
+ log("Failed to call onPostDhcpAction", e);
+ }
+ }
+
+ public void onNewDhcpResults(DhcpResults dhcpResults) {
+ log("onNewDhcpResults({" + dhcpResults + "})");
+ try {
+ mCallback.onNewDhcpResults(toStableParcelable(dhcpResults));
+ } catch (RemoteException e) {
+ log("Failed to call onNewDhcpResults", e);
+ }
+ }
+
+ public void onProvisioningSuccess(LinkProperties newLp) {
+ log("onProvisioningSuccess({" + newLp + "})");
+ try {
+ mCallback.onProvisioningSuccess(toStableParcelable(newLp));
+ } catch (RemoteException e) {
+ log("Failed to call onProvisioningSuccess", e);
+ }
+ }
+
+ public void onProvisioningFailure(LinkProperties newLp) {
+ log("onProvisioningFailure({" + newLp + "})");
+ try {
+ mCallback.onProvisioningFailure(toStableParcelable(newLp));
+ } catch (RemoteException e) {
+ log("Failed to call onProvisioningFailure", e);
+ }
+ }
+
+ public void onLinkPropertiesChange(LinkProperties newLp) {
+ log("onLinkPropertiesChange({" + newLp + "})");
+ try {
+ mCallback.onLinkPropertiesChange(toStableParcelable(newLp));
+ } catch (RemoteException e) {
+ log("Failed to call onLinkPropertiesChange", e);
+ }
+ }
+
+ public void onReachabilityLost(String logMsg) {
+ log("onReachabilityLost(" + logMsg + ")");
+ try {
+ mCallback.onReachabilityLost(logMsg);
+ } catch (RemoteException e) {
+ log("Failed to call onReachabilityLost", e);
+ }
+ }
+
+ public void onQuit() {
+ log("onQuit()");
+ try {
+ mCallback.onQuit();
+ } catch (RemoteException e) {
+ log("Failed to call onQuit", e);
+ }
+ }
+
+ public void installPacketFilter(byte[] filter) {
+ log("installPacketFilter(byte[" + filter.length + "])");
+ try {
+ mCallback.installPacketFilter(filter);
+ } catch (RemoteException e) {
+ log("Failed to call installPacketFilter", e);
+ }
+ }
+
+ public void startReadPacketFilter() {
+ log("startReadPacketFilter()");
+ try {
+ mCallback.startReadPacketFilter();
+ } catch (RemoteException e) {
+ log("Failed to call startReadPacketFilter", e);
+ }
+ }
+
+ public void setFallbackMulticastFilter(boolean enabled) {
+ log("setFallbackMulticastFilter(" + enabled + ")");
+ try {
+ mCallback.setFallbackMulticastFilter(enabled);
+ } catch (RemoteException e) {
+ log("Failed to call setFallbackMulticastFilter", e);
+ }
+ }
+
+ public void setNeighborDiscoveryOffload(boolean enable) {
+ log("setNeighborDiscoveryOffload(" + enable + ")");
+ try {
+ mCallback.setNeighborDiscoveryOffload(enable);
+ } catch (RemoteException e) {
+ log("Failed to call setNeighborDiscoveryOffload", e);
+ }
+ }
+ }
+
+ public static final String DUMP_ARG_CONFIRM = "confirm";
+
+ private static final int CMD_TERMINATE_AFTER_STOP = 1;
+ private static final int CMD_STOP = 2;
+ private static final int CMD_START = 3;
+ private static final int CMD_CONFIRM = 4;
+ private static final int EVENT_PRE_DHCP_ACTION_COMPLETE = 5;
+ // Triggered by NetlinkTracker to communicate netlink events.
+ private static final int EVENT_NETLINK_LINKPROPERTIES_CHANGED = 6;
+ private static final int CMD_UPDATE_TCP_BUFFER_SIZES = 7;
+ private static final int CMD_UPDATE_HTTP_PROXY = 8;
+ private static final int CMD_SET_MULTICAST_FILTER = 9;
+ private static final int EVENT_PROVISIONING_TIMEOUT = 10;
+ private static final int EVENT_DHCPACTION_TIMEOUT = 11;
+ private static final int EVENT_READ_PACKET_FILTER_COMPLETE = 12;
+
+ // Internal commands to use instead of trying to call transitionTo() inside
+ // a given State's enter() method. Calling transitionTo() from enter/exit
+ // encounters a Log.wtf() that can cause trouble on eng builds.
+ private static final int CMD_JUMP_STARTED_TO_RUNNING = 100;
+ private static final int CMD_JUMP_RUNNING_TO_STOPPING = 101;
+ private static final int CMD_JUMP_STOPPING_TO_STOPPED = 102;
+
+ // IpClient shares a handler with DhcpClient: commands must not overlap
+ public static final int DHCPCLIENT_CMD_BASE = 1000;
+
+ private static final int MAX_LOG_RECORDS = 500;
+ private static final int MAX_PACKET_RECORDS = 100;
+
+ private static final boolean NO_CALLBACKS = false;
+ private static final boolean SEND_CALLBACKS = true;
+
+ // This must match the interface prefix in clatd.c.
+ // TODO: Revert this hack once IpClient and Nat464Xlat work in concert.
+ private static final String CLAT_PREFIX = "v4-";
+
+ private static final int IMMEDIATE_FAILURE_DURATION = 0;
+
+ private static final int PROV_CHANGE_STILL_NOT_PROVISIONED = 1;
+ private static final int PROV_CHANGE_LOST_PROVISIONING = 2;
+ private static final int PROV_CHANGE_GAINED_PROVISIONING = 3;
+ private static final int PROV_CHANGE_STILL_PROVISIONED = 4;
+
+ private final State mStoppedState = new StoppedState();
+ private final State mStoppingState = new StoppingState();
+ private final State mStartedState = new StartedState();
+ private final State mRunningState = new RunningState();
+
+ private final String mTag;
+ private final Context mContext;
+ private final String mInterfaceName;
+ private final String mClatInterfaceName;
+ @VisibleForTesting
+ protected final IpClientCallbacksWrapper mCallback;
+ private final Dependencies mDependencies;
+ private final CountDownLatch mShutdownLatch;
+ private final ConnectivityManager mCm;
+ private final INetworkManagementService mNwService;
+ private final NetlinkTracker mNetlinkTracker;
+ private final WakeupMessage mProvisioningTimeoutAlarm;
+ private final WakeupMessage mDhcpActionTimeoutAlarm;
+ private final SharedLog mLog;
+ private final LocalLog mConnectivityPacketLog;
+ private final MessageHandlingLogger mMsgStateLogger;
+ private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
+ private final InterfaceController mInterfaceCtrl;
+
+ private InterfaceParams mInterfaceParams;
+
+ /**
+ * Non-final member variables accessed only from within our StateMachine.
+ */
+ private LinkProperties mLinkProperties;
+ private android.net.shared.ProvisioningConfiguration mConfiguration;
+ private IpReachabilityMonitor mIpReachabilityMonitor;
+ private DhcpClient mDhcpClient;
+ private DhcpResults mDhcpResults;
+ private String mTcpBufferSizes;
+ private ProxyInfo mHttpProxy;
+ private ApfFilter mApfFilter;
+ private boolean mMulticastFiltering;
+ private long mStartTimeMillis;
+
+ /**
+ * Reading the snapshot is an asynchronous operation initiated by invoking
+ * Callback.startReadPacketFilter() and completed when the WiFi Service responds with an
+ * EVENT_READ_PACKET_FILTER_COMPLETE message. The mApfDataSnapshotComplete condition variable
+ * signals when a new snapshot is ready.
+ */
+ private final ConditionVariable mApfDataSnapshotComplete = new ConditionVariable();
+
+ public static class Dependencies {
+ public INetworkManagementService getNMS() {
+ return INetworkManagementService.Stub.asInterface(
+ ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));
+ }
+
+ public INetd getNetd() {
+ return NetdService.getInstance();
+ }
+
+ /**
+ * Get interface parameters for the specified interface.
+ */
+ public InterfaceParams getInterfaceParams(String ifname) {
+ return InterfaceParams.getByName(ifname);
+ }
+ }
+
+ public IpClient(Context context, String ifName, IIpClientCallbacks callback) {
+ this(context, ifName, callback, new Dependencies());
+ }
+
+ /**
+ * An expanded constructor, useful for dependency injection.
+ * TODO: migrate all test users to mock IpClient directly and remove this ctor.
+ */
+ public IpClient(Context context, String ifName, IIpClientCallbacks callback,
+ INetworkManagementService nwService) {
+ this(context, ifName, callback, new Dependencies() {
+ @Override
+ public INetworkManagementService getNMS() {
+ return nwService;
+ }
+ });
+ }
+
+ @VisibleForTesting
+ IpClient(Context context, String ifName, IIpClientCallbacks callback, Dependencies deps) {
+ super(IpClient.class.getSimpleName() + "." + ifName);
+ Preconditions.checkNotNull(ifName);
+ Preconditions.checkNotNull(callback);
+
+ mTag = getName();
+
+ mContext = context;
+ mInterfaceName = ifName;
+ mClatInterfaceName = CLAT_PREFIX + ifName;
+ mDependencies = deps;
+ mShutdownLatch = new CountDownLatch(1);
+ mCm = mContext.getSystemService(ConnectivityManager.class);
+ mNwService = deps.getNMS();
+
+ sSmLogs.putIfAbsent(mInterfaceName, new SharedLog(MAX_LOG_RECORDS, mTag));
+ mLog = sSmLogs.get(mInterfaceName);
+ sPktLogs.putIfAbsent(mInterfaceName, new LocalLog(MAX_PACKET_RECORDS));
+ mConnectivityPacketLog = sPktLogs.get(mInterfaceName);
+ mMsgStateLogger = new MessageHandlingLogger();
+ mCallback = new IpClientCallbacksWrapper(callback, mLog);
+
+ // TODO: Consider creating, constructing, and passing in some kind of
+ // InterfaceController.Dependencies class.
+ mInterfaceCtrl = new InterfaceController(mInterfaceName, deps.getNetd(), mLog);
+
+ mNetlinkTracker = new NetlinkTracker(
+ mInterfaceName,
+ new NetlinkTracker.Callback() {
+ @Override
+ public void update() {
+ sendMessage(EVENT_NETLINK_LINKPROPERTIES_CHANGED);
+ }
+ }) {
+ @Override
+ public void interfaceAdded(String iface) {
+ super.interfaceAdded(iface);
+ if (mClatInterfaceName.equals(iface)) {
+ mCallback.setNeighborDiscoveryOffload(false);
+ } else if (!mInterfaceName.equals(iface)) {
+ return;
+ }
+
+ final String msg = "interfaceAdded(" + iface + ")";
+ logMsg(msg);
+ }
+
+ @Override
+ public void interfaceRemoved(String iface) {
+ super.interfaceRemoved(iface);
+ // TODO: Also observe mInterfaceName going down and take some
+ // kind of appropriate action.
+ if (mClatInterfaceName.equals(iface)) {
+ // TODO: consider sending a message to the IpClient main
+ // StateMachine thread, in case "NDO enabled" state becomes
+ // tied to more things that 464xlat operation.
+ mCallback.setNeighborDiscoveryOffload(true);
+ } else if (!mInterfaceName.equals(iface)) {
+ return;
+ }
+
+ final String msg = "interfaceRemoved(" + iface + ")";
+ logMsg(msg);
+ }
+
+ private void logMsg(String msg) {
+ Log.d(mTag, msg);
+ getHandler().post(() -> mLog.log("OBSERVED " + msg));
+ }
+ };
+
+ mLinkProperties = new LinkProperties();
+ mLinkProperties.setInterfaceName(mInterfaceName);
+
+ mProvisioningTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
+ mTag + ".EVENT_PROVISIONING_TIMEOUT", EVENT_PROVISIONING_TIMEOUT);
+ mDhcpActionTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
+ mTag + ".EVENT_DHCPACTION_TIMEOUT", EVENT_DHCPACTION_TIMEOUT);
+
+ // Anything the StateMachine may access must have been instantiated
+ // before this point.
+ configureAndStartStateMachine();
+
+ // Anything that may send messages to the StateMachine must only be
+ // configured to do so after the StateMachine has started (above).
+ startStateMachineUpdaters();
+ }
+
+ /**
+ * Make a IIpClient connector to communicate with this IpClient.
+ */
+ public IIpClient makeConnector() {
+ return new IpClientConnector();
+ }
+
+ class IpClientConnector extends IIpClient.Stub {
+ @Override
+ public void completedPreDhcpAction() {
+ checkNetworkStackCallingPermission();
+ IpClient.this.completedPreDhcpAction();
+ }
+ @Override
+ public void confirmConfiguration() {
+ checkNetworkStackCallingPermission();
+ IpClient.this.confirmConfiguration();
+ }
+ @Override
+ public void readPacketFilterComplete(byte[] data) {
+ checkNetworkStackCallingPermission();
+ IpClient.this.readPacketFilterComplete(data);
+ }
+ @Override
+ public void shutdown() {
+ checkNetworkStackCallingPermission();
+ IpClient.this.shutdown();
+ }
+ @Override
+ public void startProvisioning(ProvisioningConfigurationParcelable req) {
+ checkNetworkStackCallingPermission();
+ IpClient.this.startProvisioning(ProvisioningConfiguration.fromStableParcelable(req));
+ }
+ @Override
+ public void stop() {
+ checkNetworkStackCallingPermission();
+ IpClient.this.stop();
+ }
+ @Override
+ public void setTcpBufferSizes(String tcpBufferSizes) {
+ checkNetworkStackCallingPermission();
+ IpClient.this.setTcpBufferSizes(tcpBufferSizes);
+ }
+ @Override
+ public void setHttpProxy(ProxyInfoParcelable proxyInfo) {
+ checkNetworkStackCallingPermission();
+ IpClient.this.setHttpProxy(fromStableParcelable(proxyInfo));
+ }
+ @Override
+ public void setMulticastFilter(boolean enabled) {
+ checkNetworkStackCallingPermission();
+ IpClient.this.setMulticastFilter(enabled);
+ }
+ }
+
+ public String getInterfaceName() {
+ return mInterfaceName;
+ }
+
+ private void configureAndStartStateMachine() {
+ // CHECKSTYLE:OFF IndentationCheck
+ addState(mStoppedState);
+ addState(mStartedState);
+ addState(mRunningState, mStartedState);
+ addState(mStoppingState);
+ // CHECKSTYLE:ON IndentationCheck
+
+ setInitialState(mStoppedState);
+
+ super.start();
+ }
+
+ private void startStateMachineUpdaters() {
+ try {
+ mNwService.registerObserver(mNetlinkTracker);
+ } catch (RemoteException e) {
+ logError("Couldn't register NetlinkTracker: %s", e);
+ }
+ }
+
+ private void stopStateMachineUpdaters() {
+ try {
+ mNwService.unregisterObserver(mNetlinkTracker);
+ } catch (RemoteException e) {
+ logError("Couldn't unregister NetlinkTracker: %s", e);
+ }
+ }
+
+ @Override
+ protected void onQuitting() {
+ mCallback.onQuit();
+ mShutdownLatch.countDown();
+ }
+
+ /**
+ * Shut down this IpClient instance altogether.
+ */
+ public void shutdown() {
+ stop();
+ sendMessage(CMD_TERMINATE_AFTER_STOP);
+ }
+
+ /**
+ * Start provisioning with the provided parameters.
+ */
+ public void startProvisioning(ProvisioningConfiguration req) {
+ if (!req.isValid()) {
+ doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
+ return;
+ }
+
+ mInterfaceParams = mDependencies.getInterfaceParams(mInterfaceName);
+ if (mInterfaceParams == null) {
+ logError("Failed to find InterfaceParams for " + mInterfaceName);
+ doImmediateProvisioningFailure(IpManagerEvent.ERROR_INTERFACE_NOT_FOUND);
+ return;
+ }
+
+ mCallback.setNeighborDiscoveryOffload(true);
+ sendMessage(CMD_START, new android.net.shared.ProvisioningConfiguration(req));
+ }
+
+ /**
+ * Stop this IpClient.
+ *
+ * <p>This does not shut down the StateMachine itself, which is handled by {@link #shutdown()}.
+ */
+ public void stop() {
+ sendMessage(CMD_STOP);
+ }
+
+ /**
+ * Confirm the provisioning configuration.
+ */
+ public void confirmConfiguration() {
+ sendMessage(CMD_CONFIRM);
+ }
+
+ /**
+ * For clients using {@link ProvisioningConfiguration.Builder#withPreDhcpAction()}, must be
+ * called after {@link IIpClientCallbacks#onPreDhcpAction} to indicate that DHCP is clear to
+ * proceed.
+ */
+ public void completedPreDhcpAction() {
+ sendMessage(EVENT_PRE_DHCP_ACTION_COMPLETE);
+ }
+
+ /**
+ * Indicate that packet filter read is complete.
+ */
+ public void readPacketFilterComplete(byte[] data) {
+ sendMessage(EVENT_READ_PACKET_FILTER_COMPLETE, data);
+ }
+
+ /**
+ * Set the TCP buffer sizes to use.
+ *
+ * This may be called, repeatedly, at any time before or after a call to
+ * #startProvisioning(). The setting is cleared upon calling #stop().
+ */
+ public void setTcpBufferSizes(String tcpBufferSizes) {
+ sendMessage(CMD_UPDATE_TCP_BUFFER_SIZES, tcpBufferSizes);
+ }
+
+ /**
+ * Set the HTTP Proxy configuration to use.
+ *
+ * This may be called, repeatedly, at any time before or after a call to
+ * #startProvisioning(). The setting is cleared upon calling #stop().
+ */
+ public void setHttpProxy(ProxyInfo proxyInfo) {
+ sendMessage(CMD_UPDATE_HTTP_PROXY, proxyInfo);
+ }
+
+ /**
+ * Enable or disable the multicast filter. Attempts to use APF to accomplish the filtering,
+ * if not, Callback.setFallbackMulticastFilter() is called.
+ */
+ public void setMulticastFilter(boolean enabled) {
+ sendMessage(CMD_SET_MULTICAST_FILTER, enabled);
+ }
+
+ /**
+ * Dump logs of this IpClient.
+ */
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ if (args != null && args.length > 0 && DUMP_ARG_CONFIRM.equals(args[0])) {
+ // Execute confirmConfiguration() and take no further action.
+ confirmConfiguration();
+ return;
+ }
+
+ // Thread-unsafe access to mApfFilter but just used for debugging.
+ final ApfFilter apfFilter = mApfFilter;
+ final android.net.shared.ProvisioningConfiguration provisioningConfig = mConfiguration;
+ final ApfCapabilities apfCapabilities = (provisioningConfig != null)
+ ? provisioningConfig.mApfCapabilities : null;
+
+ IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
+ pw.println(mTag + " APF dump:");
+ pw.increaseIndent();
+ if (apfFilter != null) {
+ if (apfCapabilities.hasDataAccess()) {
+ // Request a new snapshot, then wait for it.
+ mApfDataSnapshotComplete.close();
+ mCallback.startReadPacketFilter();
+ if (!mApfDataSnapshotComplete.block(1000)) {
+ pw.print("TIMEOUT: DUMPING STALE APF SNAPSHOT");
+ }
+ }
+ apfFilter.dump(pw);
+
+ } else {
+ pw.print("No active ApfFilter; ");
+ if (provisioningConfig == null) {
+ pw.println("IpClient not yet started.");
+ } else if (apfCapabilities == null || apfCapabilities.apfVersionSupported == 0) {
+ pw.println("Hardware does not support APF.");
+ } else {
+ pw.println("ApfFilter not yet started, APF capabilities: " + apfCapabilities);
+ }
+ }
+ pw.decreaseIndent();
+ pw.println();
+ pw.println(mTag + " current ProvisioningConfiguration:");
+ pw.increaseIndent();
+ pw.println(Objects.toString(provisioningConfig, "N/A"));
+ pw.decreaseIndent();
+
+ final IpReachabilityMonitor iprm = mIpReachabilityMonitor;
+ if (iprm != null) {
+ pw.println();
+ pw.println(mTag + " current IpReachabilityMonitor state:");
+ pw.increaseIndent();
+ iprm.dump(pw);
+ pw.decreaseIndent();
+ }
+
+ pw.println();
+ pw.println(mTag + " StateMachine dump:");
+ pw.increaseIndent();
+ mLog.dump(fd, pw, args);
+ pw.decreaseIndent();
+
+ pw.println();
+ pw.println(mTag + " connectivity packet log:");
+ pw.println();
+ pw.println("Debug with python and scapy via:");
+ pw.println("shell$ python");
+ pw.println(">>> from scapy import all as scapy");
+ pw.println(">>> scapy.Ether(\"<paste_hex_string>\".decode(\"hex\")).show2()");
+ pw.println();
+
+ pw.increaseIndent();
+ mConnectivityPacketLog.readOnlyLocalLog().dump(fd, pw, args);
+ pw.decreaseIndent();
+ }
+
+
+ /**
+ * Internals.
+ */
+
+ @Override
+ protected String getWhatToString(int what) {
+ return sWhatToString.get(what, "UNKNOWN: " + Integer.toString(what));
+ }
+
+ @Override
+ protected String getLogRecString(Message msg) {
+ final String logLine = String.format(
+ "%s/%d %d %d %s [%s]",
+ mInterfaceName, (mInterfaceParams == null) ? -1 : mInterfaceParams.index,
+ msg.arg1, msg.arg2, Objects.toString(msg.obj), mMsgStateLogger);
+
+ final String richerLogLine = getWhatToString(msg.what) + " " + logLine;
+ mLog.log(richerLogLine);
+ if (DBG) {
+ Log.d(mTag, richerLogLine);
+ }
+
+ mMsgStateLogger.reset();
+ return logLine;
+ }
+
+ @Override
+ protected boolean recordLogRec(Message msg) {
+ // Don't log EVENT_NETLINK_LINKPROPERTIES_CHANGED. They can be noisy,
+ // and we already log any LinkProperties change that results in an
+ // invocation of IpClient.Callback#onLinkPropertiesChange().
+ final boolean shouldLog = (msg.what != EVENT_NETLINK_LINKPROPERTIES_CHANGED);
+ if (!shouldLog) {
+ mMsgStateLogger.reset();
+ }
+ return shouldLog;
+ }
+
+ private void logError(String fmt, Object... args) {
+ final String msg = "ERROR " + String.format(fmt, args);
+ Log.e(mTag, msg);
+ mLog.log(msg);
+ }
+
+ // This needs to be called with care to ensure that our LinkProperties
+ // are in sync with the actual LinkProperties of the interface. For example,
+ // we should only call this if we know for sure that there are no IP addresses
+ // assigned to the interface, etc.
+ private void resetLinkProperties() {
+ mNetlinkTracker.clearLinkProperties();
+ mConfiguration = null;
+ mDhcpResults = null;
+ mTcpBufferSizes = "";
+ mHttpProxy = null;
+
+ mLinkProperties = new LinkProperties();
+ mLinkProperties.setInterfaceName(mInterfaceName);
+ }
+
+ private void recordMetric(final int type) {
+ // We may record error metrics prior to starting.
+ // Map this to IMMEDIATE_FAILURE_DURATION.
+ final long duration = (mStartTimeMillis > 0)
+ ? (SystemClock.elapsedRealtime() - mStartTimeMillis)
+ : IMMEDIATE_FAILURE_DURATION;
+ mMetricsLog.log(mInterfaceName, new IpManagerEvent(type, duration));
+ }
+
+ // For now: use WifiStateMachine's historical notion of provisioned.
+ @VisibleForTesting
+ static boolean isProvisioned(LinkProperties lp, InitialConfiguration config) {
+ // For historical reasons, we should connect even if all we have is
+ // an IPv4 address and nothing else.
+ if (lp.hasIPv4Address() || lp.isProvisioned()) {
+ return true;
+ }
+ if (config == null) {
+ return false;
+ }
+
+ // When an InitialConfiguration is specified, ignore any difference with previous
+ // properties and instead check if properties observed match the desired properties.
+ return config.isProvisionedBy(lp.getLinkAddresses(), lp.getRoutes());
+ }
+
+ // TODO: Investigate folding all this into the existing static function
+ // LinkProperties.compareProvisioning() or some other single function that
+ // takes two LinkProperties objects and returns a ProvisioningChange
+ // object that is a correct and complete assessment of what changed, taking
+ // account of the asymmetries described in the comments in this function.
+ // Then switch to using it everywhere (IpReachabilityMonitor, etc.).
+ private int compareProvisioning(LinkProperties oldLp, LinkProperties newLp) {
+ int delta;
+ InitialConfiguration config = mConfiguration != null ? mConfiguration.mInitialConfig : null;
+ final boolean wasProvisioned = isProvisioned(oldLp, config);
+ final boolean isProvisioned = isProvisioned(newLp, config);
+
+ if (!wasProvisioned && isProvisioned) {
+ delta = PROV_CHANGE_GAINED_PROVISIONING;
+ } else if (wasProvisioned && isProvisioned) {
+ delta = PROV_CHANGE_STILL_PROVISIONED;
+ } else if (!wasProvisioned && !isProvisioned) {
+ delta = PROV_CHANGE_STILL_NOT_PROVISIONED;
+ } else {
+ // (wasProvisioned && !isProvisioned)
+ //
+ // Note that this is true even if we lose a configuration element
+ // (e.g., a default gateway) that would not be required to advance
+ // into provisioned state. This is intended: if we have a default
+ // router and we lose it, that's a sure sign of a problem, but if
+ // we connect to a network with no IPv4 DNS servers, we consider
+ // that to be a network without DNS servers and connect anyway.
+ //
+ // See the comment below.
+ delta = PROV_CHANGE_LOST_PROVISIONING;
+ }
+
+ final boolean lostIPv6 = oldLp.isIPv6Provisioned() && !newLp.isIPv6Provisioned();
+ final boolean lostIPv4Address = oldLp.hasIPv4Address() && !newLp.hasIPv4Address();
+ final boolean lostIPv6Router = oldLp.hasIPv6DefaultRoute() && !newLp.hasIPv6DefaultRoute();
+
+ // If bad wifi avoidance is disabled, then ignore IPv6 loss of
+ // provisioning. Otherwise, when a hotspot that loses Internet
+ // access sends out a 0-lifetime RA to its clients, the clients
+ // will disconnect and then reconnect, avoiding the bad hotspot,
+ // instead of getting stuck on the bad hotspot. http://b/31827713 .
+ //
+ // This is incorrect because if the hotspot then regains Internet
+ // access with a different prefix, TCP connections on the
+ // deprecated addresses will remain stuck.
+ //
+ // Note that we can still be disconnected by IpReachabilityMonitor
+ // if the IPv6 default gateway (but not the IPv6 DNS servers; see
+ // accompanying code in IpReachabilityMonitor) is unreachable.
+ final boolean ignoreIPv6ProvisioningLoss =
+ mConfiguration != null && mConfiguration.mUsingMultinetworkPolicyTracker
+ && mCm.getAvoidBadWifi();
+
+ // Additionally:
+ //
+ // Partial configurations (e.g., only an IPv4 address with no DNS
+ // servers and no default route) are accepted as long as DHCPv4
+ // succeeds. On such a network, isProvisioned() will always return
+ // false, because the configuration is not complete, but we want to
+ // connect anyway. It might be a disconnected network such as a
+ // Chromecast or a wireless printer, for example.
+ //
+ // Because on such a network isProvisioned() will always return false,
+ // delta will never be LOST_PROVISIONING. So check for loss of
+ // provisioning here too.
+ if (lostIPv4Address || (lostIPv6 && !ignoreIPv6ProvisioningLoss)) {
+ delta = PROV_CHANGE_LOST_PROVISIONING;
+ }
+
+ // Additionally:
+ //
+ // If the previous link properties had a global IPv6 address and an
+ // IPv6 default route then also consider the loss of that default route
+ // to be a loss of provisioning. See b/27962810.
+ if (oldLp.hasGlobalIPv6Address() && (lostIPv6Router && !ignoreIPv6ProvisioningLoss)) {
+ delta = PROV_CHANGE_LOST_PROVISIONING;
+ }
+
+ return delta;
+ }
+
+ private void dispatchCallback(int delta, LinkProperties newLp) {
+ switch (delta) {
+ case PROV_CHANGE_GAINED_PROVISIONING:
+ if (DBG) {
+ Log.d(mTag, "onProvisioningSuccess()");
+ }
+ recordMetric(IpManagerEvent.PROVISIONING_OK);
+ mCallback.onProvisioningSuccess(newLp);
+ break;
+
+ case PROV_CHANGE_LOST_PROVISIONING:
+ if (DBG) {
+ Log.d(mTag, "onProvisioningFailure()");
+ }
+ recordMetric(IpManagerEvent.PROVISIONING_FAIL);
+ mCallback.onProvisioningFailure(newLp);
+ break;
+
+ default:
+ if (DBG) {
+ Log.d(mTag, "onLinkPropertiesChange()");
+ }
+ mCallback.onLinkPropertiesChange(newLp);
+ break;
+ }
+ }
+
+ // Updates all IpClient-related state concerned with LinkProperties.
+ // Returns a ProvisioningChange for possibly notifying other interested
+ // parties that are not fronted by IpClient.
+ private int setLinkProperties(LinkProperties newLp) {
+ if (mApfFilter != null) {
+ mApfFilter.setLinkProperties(newLp);
+ }
+ if (mIpReachabilityMonitor != null) {
+ mIpReachabilityMonitor.updateLinkProperties(newLp);
+ }
+
+ int delta = compareProvisioning(mLinkProperties, newLp);
+ mLinkProperties = new LinkProperties(newLp);
+
+ if (delta == PROV_CHANGE_GAINED_PROVISIONING) {
+ // TODO: Add a proper ProvisionedState and cancel the alarm in
+ // its enter() method.
+ mProvisioningTimeoutAlarm.cancel();
+ }
+
+ return delta;
+ }
+
+ private LinkProperties assembleLinkProperties() {
+ // [1] Create a new LinkProperties object to populate.
+ LinkProperties newLp = new LinkProperties();
+ newLp.setInterfaceName(mInterfaceName);
+
+ // [2] Pull in data from netlink:
+ // - IPv4 addresses
+ // - IPv6 addresses
+ // - IPv6 routes
+ // - IPv6 DNS servers
+ //
+ // N.B.: this is fundamentally race-prone and should be fixed by
+ // changing NetlinkTracker from a hybrid edge/level model to an
+ // edge-only model, or by giving IpClient its own netlink socket(s)
+ // so as to track all required information directly.
+ LinkProperties netlinkLinkProperties = mNetlinkTracker.getLinkProperties();
+ newLp.setLinkAddresses(netlinkLinkProperties.getLinkAddresses());
+ for (RouteInfo route : netlinkLinkProperties.getRoutes()) {
+ newLp.addRoute(route);
+ }
+ addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers());
+
+ // [3] Add in data from DHCPv4, if available.
+ //
+ // mDhcpResults is never shared with any other owner so we don't have
+ // to worry about concurrent modification.
+ if (mDhcpResults != null) {
+ for (RouteInfo route : mDhcpResults.getRoutes(mInterfaceName)) {
+ newLp.addRoute(route);
+ }
+ addAllReachableDnsServers(newLp, mDhcpResults.dnsServers);
+ newLp.setDomains(mDhcpResults.domains);
+
+ if (mDhcpResults.mtu != 0) {
+ newLp.setMtu(mDhcpResults.mtu);
+ }
+ }
+
+ // [4] Add in TCP buffer sizes and HTTP Proxy config, if available.
+ if (!TextUtils.isEmpty(mTcpBufferSizes)) {
+ newLp.setTcpBufferSizes(mTcpBufferSizes);
+ }
+ if (mHttpProxy != null) {
+ newLp.setHttpProxy(mHttpProxy);
+ }
+
+ // [5] Add data from InitialConfiguration
+ if (mConfiguration != null && mConfiguration.mInitialConfig != null) {
+ InitialConfiguration config = mConfiguration.mInitialConfig;
+ // Add InitialConfiguration routes and dns server addresses once all addresses
+ // specified in the InitialConfiguration have been observed with Netlink.
+ if (config.isProvisionedBy(newLp.getLinkAddresses(), null)) {
+ for (IpPrefix prefix : config.directlyConnectedRoutes) {
+ newLp.addRoute(new RouteInfo(prefix, null, mInterfaceName));
+ }
+ }
+ addAllReachableDnsServers(newLp, config.dnsServers);
+ }
+ final LinkProperties oldLp = mLinkProperties;
+ if (DBG) {
+ Log.d(mTag, String.format("Netlink-seen LPs: %s, new LPs: %s; old LPs: %s",
+ netlinkLinkProperties, newLp, oldLp));
+ }
+
+ // TODO: also learn via netlink routes specified by an InitialConfiguration and specified
+ // from a static IP v4 config instead of manually patching them in in steps [3] and [5].
+ return newLp;
+ }
+
+ private static void addAllReachableDnsServers(
+ LinkProperties lp, Iterable<InetAddress> dnses) {
+ // TODO: Investigate deleting this reachability check. We should be
+ // able to pass everything down to netd and let netd do evaluation
+ // and RFC6724-style sorting.
+ for (InetAddress dns : dnses) {
+ if (!dns.isAnyLocalAddress() && lp.isReachable(dns)) {
+ lp.addDnsServer(dns);
+ }
+ }
+ }
+
+ // Returns false if we have lost provisioning, true otherwise.
+ private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
+ final LinkProperties newLp = assembleLinkProperties();
+ if (Objects.equals(newLp, mLinkProperties)) {
+ return true;
+ }
+ final int delta = setLinkProperties(newLp);
+ if (sendCallbacks) {
+ dispatchCallback(delta, newLp);
+ }
+ return (delta != PROV_CHANGE_LOST_PROVISIONING);
+ }
+
+ private void handleIPv4Success(DhcpResults dhcpResults) {
+ mDhcpResults = new DhcpResults(dhcpResults);
+ final LinkProperties newLp = assembleLinkProperties();
+ final int delta = setLinkProperties(newLp);
+
+ if (DBG) {
+ Log.d(mTag, "onNewDhcpResults(" + Objects.toString(dhcpResults) + ")");
+ }
+ mCallback.onNewDhcpResults(dhcpResults);
+ dispatchCallback(delta, newLp);
+ }
+
+ private void handleIPv4Failure() {
+ // TODO: Investigate deleting this clearIPv4Address() call.
+ //
+ // DhcpClient will send us CMD_CLEAR_LINKADDRESS in all circumstances
+ // that could trigger a call to this function. If we missed handling
+ // that message in StartedState for some reason we would still clear
+ // any addresses upon entry to StoppedState.
+ mInterfaceCtrl.clearIPv4Address();
+ mDhcpResults = null;
+ if (DBG) {
+ Log.d(mTag, "onNewDhcpResults(null)");
+ }
+ mCallback.onNewDhcpResults(null);
+
+ handleProvisioningFailure();
+ }
+
+ private void handleProvisioningFailure() {
+ final LinkProperties newLp = assembleLinkProperties();
+ int delta = setLinkProperties(newLp);
+ // If we've gotten here and we're still not provisioned treat that as
+ // a total loss of provisioning.
+ //
+ // Either (a) static IP configuration failed or (b) DHCPv4 failed AND
+ // there was no usable IPv6 obtained before a non-zero provisioning
+ // timeout expired.
+ //
+ // Regardless: GAME OVER.
+ if (delta == PROV_CHANGE_STILL_NOT_PROVISIONED) {
+ delta = PROV_CHANGE_LOST_PROVISIONING;
+ }
+
+ dispatchCallback(delta, newLp);
+ if (delta == PROV_CHANGE_LOST_PROVISIONING) {
+ transitionTo(mStoppingState);
+ }
+ }
+
+ private void doImmediateProvisioningFailure(int failureType) {
+ logError("onProvisioningFailure(): %s", failureType);
+ recordMetric(failureType);
+ mCallback.onProvisioningFailure(new LinkProperties(mLinkProperties));
+ }
+
+ private boolean startIPv4() {
+ // If we have a StaticIpConfiguration attempt to apply it and
+ // handle the result accordingly.
+ if (mConfiguration.mStaticIpConfig != null) {
+ if (mInterfaceCtrl.setIPv4Address(mConfiguration.mStaticIpConfig.ipAddress)) {
+ handleIPv4Success(new DhcpResults(mConfiguration.mStaticIpConfig));
+ } else {
+ return false;
+ }
+ } else {
+ // Start DHCPv4.
+ mDhcpClient = DhcpClient.makeDhcpClient(mContext, IpClient.this, mInterfaceParams);
+ mDhcpClient.registerForPreDhcpNotification();
+ mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP);
+ }
+
+ return true;
+ }
+
+ private boolean startIPv6() {
+ return mInterfaceCtrl.setIPv6PrivacyExtensions(true)
+ && mInterfaceCtrl.setIPv6AddrGenModeIfSupported(mConfiguration.mIPv6AddrGenMode)
+ && mInterfaceCtrl.enableIPv6();
+ }
+
+ private boolean applyInitialConfig(InitialConfiguration config) {
+ // TODO: also support specifying a static IPv4 configuration in InitialConfiguration.
+ for (LinkAddress addr : findAll(config.ipAddresses, LinkAddress::isIPv6)) {
+ if (!mInterfaceCtrl.addAddress(addr)) return false;
+ }
+
+ return true;
+ }
+
+ private boolean startIpReachabilityMonitor() {
+ try {
+ // TODO: Fetch these parameters from settings, and install a
+ // settings observer to watch for update and re-program these
+ // parameters (Q: is this level of dynamic updatability really
+ // necessary or does reading from settings at startup suffice?).
+ final int numSolicits = 5;
+ final int interSolicitIntervalMs = 750;
+ setNeighborParameters(mDependencies.getNetd(), mInterfaceName,
+ numSolicits, interSolicitIntervalMs);
+ } catch (Exception e) {
+ mLog.e("Failed to adjust neighbor parameters", e);
+ // Carry on using the system defaults (currently: 3, 1000);
+ }
+
+ try {
+ mIpReachabilityMonitor = new IpReachabilityMonitor(
+ mContext,
+ mInterfaceParams,
+ getHandler(),
+ mLog,
+ new IpReachabilityMonitor.Callback() {
+ @Override
+ public void notifyLost(InetAddress ip, String logMsg) {
+ mCallback.onReachabilityLost(logMsg);
+ }
+ },
+ mConfiguration.mUsingMultinetworkPolicyTracker);
+ } catch (IllegalArgumentException iae) {
+ // Failed to start IpReachabilityMonitor. Log it and call
+ // onProvisioningFailure() immediately.
+ //
+ // See http://b/31038971.
+ logError("IpReachabilityMonitor failure: %s", iae);
+ mIpReachabilityMonitor = null;
+ }
+
+ return (mIpReachabilityMonitor != null);
+ }
+
+ private void stopAllIP() {
+ // We don't need to worry about routes, just addresses, because:
+ // - disableIpv6() will clear autoconf IPv6 routes as well, and
+ // - we don't get IPv4 routes from netlink
+ // so we neither react to nor need to wait for changes in either.
+
+ mInterfaceCtrl.disableIPv6();
+ mInterfaceCtrl.clearAllAddresses();
+ }
+
+ class StoppedState extends State {
+ @Override
+ public void enter() {
+ stopAllIP();
+
+ resetLinkProperties();
+ if (mStartTimeMillis > 0) {
+ // Completed a life-cycle; send a final empty LinkProperties
+ // (cleared in resetLinkProperties() above) and record an event.
+ mCallback.onLinkPropertiesChange(new LinkProperties(mLinkProperties));
+ recordMetric(IpManagerEvent.COMPLETE_LIFECYCLE);
+ mStartTimeMillis = 0;
+ }
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_TERMINATE_AFTER_STOP:
+ stopStateMachineUpdaters();
+ quit();
+ break;
+
+ case CMD_STOP:
+ break;
+
+ case CMD_START:
+ mConfiguration = (android.net.shared.ProvisioningConfiguration) msg.obj;
+ transitionTo(mStartedState);
+ break;
+
+ case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
+ handleLinkPropertiesUpdate(NO_CALLBACKS);
+ break;
+
+ case CMD_UPDATE_TCP_BUFFER_SIZES:
+ mTcpBufferSizes = (String) msg.obj;
+ handleLinkPropertiesUpdate(NO_CALLBACKS);
+ break;
+
+ case CMD_UPDATE_HTTP_PROXY:
+ mHttpProxy = (ProxyInfo) msg.obj;
+ handleLinkPropertiesUpdate(NO_CALLBACKS);
+ break;
+
+ case CMD_SET_MULTICAST_FILTER:
+ mMulticastFiltering = (boolean) msg.obj;
+ break;
+
+ case DhcpClient.CMD_ON_QUIT:
+ // Everything is already stopped.
+ logError("Unexpected CMD_ON_QUIT (already stopped).");
+ break;
+
+ default:
+ return NOT_HANDLED;
+ }
+
+ mMsgStateLogger.handled(this, getCurrentState());
+ return HANDLED;
+ }
+ }
+
+ class StoppingState extends State {
+ @Override
+ public void enter() {
+ if (mDhcpClient == null) {
+ // There's no DHCPv4 for which to wait; proceed to stopped.
+ deferMessage(obtainMessage(CMD_JUMP_STOPPING_TO_STOPPED));
+ }
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_JUMP_STOPPING_TO_STOPPED:
+ transitionTo(mStoppedState);
+ break;
+
+ case CMD_STOP:
+ break;
+
+ case DhcpClient.CMD_CLEAR_LINKADDRESS:
+ mInterfaceCtrl.clearIPv4Address();
+ break;
+
+ case DhcpClient.CMD_ON_QUIT:
+ mDhcpClient = null;
+ transitionTo(mStoppedState);
+ break;
+
+ default:
+ deferMessage(msg);
+ }
+
+ mMsgStateLogger.handled(this, getCurrentState());
+ return HANDLED;
+ }
+ }
+
+ class StartedState extends State {
+ @Override
+ public void enter() {
+ mStartTimeMillis = SystemClock.elapsedRealtime();
+
+ if (mConfiguration.mProvisioningTimeoutMs > 0) {
+ final long alarmTime = SystemClock.elapsedRealtime()
+ + mConfiguration.mProvisioningTimeoutMs;
+ mProvisioningTimeoutAlarm.schedule(alarmTime);
+ }
+
+ if (readyToProceed()) {
+ deferMessage(obtainMessage(CMD_JUMP_STARTED_TO_RUNNING));
+ } else {
+ // Clear all IPv4 and IPv6 before proceeding to RunningState.
+ // Clean up any leftover state from an abnormal exit from
+ // tethering or during an IpClient restart.
+ stopAllIP();
+ }
+ }
+
+ @Override
+ public void exit() {
+ mProvisioningTimeoutAlarm.cancel();
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_JUMP_STARTED_TO_RUNNING:
+ transitionTo(mRunningState);
+ break;
+
+ case CMD_STOP:
+ transitionTo(mStoppingState);
+ break;
+
+ case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
+ handleLinkPropertiesUpdate(NO_CALLBACKS);
+ if (readyToProceed()) {
+ transitionTo(mRunningState);
+ }
+ break;
+
+ case EVENT_PROVISIONING_TIMEOUT:
+ handleProvisioningFailure();
+ break;
+
+ default:
+ // It's safe to process messages out of order because the
+ // only message that can both
+ // a) be received at this time and
+ // b) affect provisioning state
+ // is EVENT_NETLINK_LINKPROPERTIES_CHANGED (handled above).
+ deferMessage(msg);
+ }
+
+ mMsgStateLogger.handled(this, getCurrentState());
+ return HANDLED;
+ }
+
+ private boolean readyToProceed() {
+ return (!mLinkProperties.hasIPv4Address() && !mLinkProperties.hasGlobalIPv6Address());
+ }
+ }
+
+ class RunningState extends State {
+ private ConnectivityPacketTracker mPacketTracker;
+ private boolean mDhcpActionInFlight;
+
+ @Override
+ public void enter() {
+ ApfFilter.ApfConfiguration apfConfig = new ApfFilter.ApfConfiguration();
+ apfConfig.apfCapabilities = mConfiguration.mApfCapabilities;
+ apfConfig.multicastFilter = mMulticastFiltering;
+ // Get the Configuration for ApfFilter from Context
+ apfConfig.ieee802_3Filter =
+ mContext.getResources().getBoolean(R.bool.config_apfDrop802_3Frames);
+ apfConfig.ethTypeBlackList =
+ mContext.getResources().getIntArray(R.array.config_apfEthTypeBlackList);
+ mApfFilter = ApfFilter.maybeCreate(mContext, apfConfig, mInterfaceParams, mCallback);
+ // TODO: investigate the effects of any multicast filtering racing/interfering with the
+ // rest of this IP configuration startup.
+ if (mApfFilter == null) {
+ mCallback.setFallbackMulticastFilter(mMulticastFiltering);
+ }
+
+ mPacketTracker = createPacketTracker();
+ if (mPacketTracker != null) mPacketTracker.start(mConfiguration.mDisplayName);
+
+ if (mConfiguration.mEnableIPv6 && !startIPv6()) {
+ doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV6);
+ enqueueJumpToStoppingState();
+ return;
+ }
+
+ if (mConfiguration.mEnableIPv4 && !startIPv4()) {
+ doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV4);
+ enqueueJumpToStoppingState();
+ return;
+ }
+
+ final InitialConfiguration config = mConfiguration.mInitialConfig;
+ if ((config != null) && !applyInitialConfig(config)) {
+ // TODO introduce a new IpManagerEvent constant to distinguish this error case.
+ doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
+ enqueueJumpToStoppingState();
+ return;
+ }
+
+ if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) {
+ doImmediateProvisioningFailure(
+ IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR);
+ enqueueJumpToStoppingState();
+ return;
+ }
+ }
+
+ @Override
+ public void exit() {
+ stopDhcpAction();
+
+ if (mIpReachabilityMonitor != null) {
+ mIpReachabilityMonitor.stop();
+ mIpReachabilityMonitor = null;
+ }
+
+ if (mDhcpClient != null) {
+ mDhcpClient.sendMessage(DhcpClient.CMD_STOP_DHCP);
+ mDhcpClient.doQuit();
+ }
+
+ if (mPacketTracker != null) {
+ mPacketTracker.stop();
+ mPacketTracker = null;
+ }
+
+ if (mApfFilter != null) {
+ mApfFilter.shutdown();
+ mApfFilter = null;
+ }
+
+ resetLinkProperties();
+ }
+
+ private void enqueueJumpToStoppingState() {
+ deferMessage(obtainMessage(CMD_JUMP_RUNNING_TO_STOPPING));
+ }
+
+ private ConnectivityPacketTracker createPacketTracker() {
+ try {
+ return new ConnectivityPacketTracker(
+ getHandler(), mInterfaceParams, mConnectivityPacketLog);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ private void ensureDhcpAction() {
+ if (!mDhcpActionInFlight) {
+ mCallback.onPreDhcpAction();
+ mDhcpActionInFlight = true;
+ final long alarmTime = SystemClock.elapsedRealtime()
+ + mConfiguration.mRequestedPreDhcpActionMs;
+ mDhcpActionTimeoutAlarm.schedule(alarmTime);
+ }
+ }
+
+ private void stopDhcpAction() {
+ mDhcpActionTimeoutAlarm.cancel();
+ if (mDhcpActionInFlight) {
+ mCallback.onPostDhcpAction();
+ mDhcpActionInFlight = false;
+ }
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_JUMP_RUNNING_TO_STOPPING:
+ case CMD_STOP:
+ transitionTo(mStoppingState);
+ break;
+
+ case CMD_START:
+ logError("ALERT: START received in StartedState. Please fix caller.");
+ break;
+
+ case CMD_CONFIRM:
+ // TODO: Possibly introduce a second type of confirmation
+ // that both probes (a) on-link neighbors and (b) does
+ // a DHCPv4 RENEW. We used to do this on Wi-Fi framework
+ // roams.
+ if (mIpReachabilityMonitor != null) {
+ mIpReachabilityMonitor.probeAll();
+ }
+ break;
+
+ case EVENT_PRE_DHCP_ACTION_COMPLETE:
+ // It's possible to reach here if, for example, someone
+ // calls completedPreDhcpAction() after provisioning with
+ // a static IP configuration.
+ if (mDhcpClient != null) {
+ mDhcpClient.sendMessage(DhcpClient.CMD_PRE_DHCP_ACTION_COMPLETE);
+ }
+ break;
+
+ case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
+ if (!handleLinkPropertiesUpdate(SEND_CALLBACKS)) {
+ transitionTo(mStoppingState);
+ }
+ break;
+
+ case CMD_UPDATE_TCP_BUFFER_SIZES:
+ mTcpBufferSizes = (String) msg.obj;
+ // This cannot possibly change provisioning state.
+ handleLinkPropertiesUpdate(SEND_CALLBACKS);
+ break;
+
+ case CMD_UPDATE_HTTP_PROXY:
+ mHttpProxy = (ProxyInfo) msg.obj;
+ // This cannot possibly change provisioning state.
+ handleLinkPropertiesUpdate(SEND_CALLBACKS);
+ break;
+
+ case CMD_SET_MULTICAST_FILTER: {
+ mMulticastFiltering = (boolean) msg.obj;
+ if (mApfFilter != null) {
+ mApfFilter.setMulticastFilter(mMulticastFiltering);
+ } else {
+ mCallback.setFallbackMulticastFilter(mMulticastFiltering);
+ }
+ break;
+ }
+
+ case EVENT_READ_PACKET_FILTER_COMPLETE: {
+ if (mApfFilter != null) {
+ mApfFilter.setDataSnapshot((byte[]) msg.obj);
+ }
+ mApfDataSnapshotComplete.open();
+ break;
+ }
+
+ case EVENT_DHCPACTION_TIMEOUT:
+ stopDhcpAction();
+ break;
+
+ case DhcpClient.CMD_PRE_DHCP_ACTION:
+ if (mConfiguration.mRequestedPreDhcpActionMs > 0) {
+ ensureDhcpAction();
+ } else {
+ sendMessage(EVENT_PRE_DHCP_ACTION_COMPLETE);
+ }
+ break;
+
+ case DhcpClient.CMD_CLEAR_LINKADDRESS:
+ mInterfaceCtrl.clearIPv4Address();
+ break;
+
+ case DhcpClient.CMD_CONFIGURE_LINKADDRESS: {
+ final LinkAddress ipAddress = (LinkAddress) msg.obj;
+ if (mInterfaceCtrl.setIPv4Address(ipAddress)) {
+ mDhcpClient.sendMessage(DhcpClient.EVENT_LINKADDRESS_CONFIGURED);
+ } else {
+ logError("Failed to set IPv4 address.");
+ dispatchCallback(PROV_CHANGE_LOST_PROVISIONING,
+ new LinkProperties(mLinkProperties));
+ transitionTo(mStoppingState);
+ }
+ break;
+ }
+
+ // This message is only received when:
+ //
+ // a) initial address acquisition succeeds,
+ // b) renew succeeds or is NAK'd,
+ // c) rebind succeeds or is NAK'd, or
+ // c) the lease expires,
+ //
+ // but never when initial address acquisition fails. The latter
+ // condition is now governed by the provisioning timeout.
+ case DhcpClient.CMD_POST_DHCP_ACTION:
+ stopDhcpAction();
+
+ switch (msg.arg1) {
+ case DhcpClient.DHCP_SUCCESS:
+ handleIPv4Success((DhcpResults) msg.obj);
+ break;
+ case DhcpClient.DHCP_FAILURE:
+ handleIPv4Failure();
+ break;
+ default:
+ logError("Unknown CMD_POST_DHCP_ACTION status: %s", msg.arg1);
+ }
+ break;
+
+ case DhcpClient.CMD_ON_QUIT:
+ // DHCPv4 quit early for some reason.
+ logError("Unexpected CMD_ON_QUIT.");
+ mDhcpClient = null;
+ break;
+
+ default:
+ return NOT_HANDLED;
+ }
+
+ mMsgStateLogger.handled(this, getCurrentState());
+ return HANDLED;
+ }
+ }
+
+ private static class MessageHandlingLogger {
+ public String processedInState;
+ public String receivedInState;
+
+ public void reset() {
+ processedInState = null;
+ receivedInState = null;
+ }
+
+ public void handled(State processedIn, IState receivedIn) {
+ processedInState = processedIn.getClass().getSimpleName();
+ receivedInState = receivedIn.getName();
+ }
+
+ public String toString() {
+ return String.format("rcvd_in=%s, proc_in=%s",
+ receivedInState, processedInState);
+ }
+ }
+
+ private static void setNeighborParameters(
+ INetd netd, String ifName, int numSolicits, int interSolicitIntervalMs)
+ throws RemoteException, IllegalArgumentException {
+ Preconditions.checkNotNull(netd);
+ Preconditions.checkArgument(!TextUtils.isEmpty(ifName));
+ Preconditions.checkArgument(numSolicits > 0);
+ Preconditions.checkArgument(interSolicitIntervalMs > 0);
+
+ for (int family : new Integer[]{INetd.IPV4, INetd.IPV6}) {
+ netd.setProcSysNet(family, INetd.NEIGH, ifName, "retrans_time_ms",
+ Integer.toString(interSolicitIntervalMs));
+ netd.setProcSysNet(family, INetd.NEIGH, ifName, "ucast_solicit",
+ Integer.toString(numSolicits));
+ }
+ }
+
+ // TODO: extract out into CollectionUtils.
+ static <T> boolean any(Iterable<T> coll, Predicate<T> fn) {
+ for (T t : coll) {
+ if (fn.test(t)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static <T> boolean all(Iterable<T> coll, Predicate<T> fn) {
+ return !any(coll, not(fn));
+ }
+
+ static <T> Predicate<T> not(Predicate<T> fn) {
+ return (t) -> !fn.test(t);
+ }
+
+ static <T> String join(String delimiter, Collection<T> coll) {
+ return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter));
+ }
+
+ static <T> T find(Iterable<T> coll, Predicate<T> fn) {
+ for (T t: coll) {
+ if (fn.test(t)) {
+ return t;
+ }
+ }
+ return null;
+ }
+
+ static <T> List<T> findAll(Collection<T> coll, Predicate<T> fn) {
+ return coll.stream().filter(fn).collect(Collectors.toList());
+ }
+}
diff --git a/src/android/net/ip/IpNeighborMonitor.java b/src/android/net/ip/IpNeighborMonitor.java
new file mode 100644
index 0000000..eb993a4
--- /dev/null
+++ b/src/android/net/ip/IpNeighborMonitor.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.ip;
+
+import static android.net.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static android.net.netlink.NetlinkConstants.hexify;
+import static android.net.netlink.NetlinkConstants.stringForNlMsgType;
+import static android.net.util.SocketUtils.makeNetlinkSocketAddress;
+
+import android.net.MacAddress;
+import android.net.netlink.NetlinkErrorMessage;
+import android.net.netlink.NetlinkMessage;
+import android.net.netlink.NetlinkSocket;
+import android.net.netlink.RtNetlinkNeighborMessage;
+import android.net.netlink.StructNdMsg;
+import android.net.util.PacketReader;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.internal.util.BitUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.StringJoiner;
+
+
+/**
+ * IpNeighborMonitor.
+ *
+ * Monitors the kernel rtnetlink neighbor notifications and presents to callers
+ * NeighborEvents describing each event. Callers can provide a consumer instance
+ * to both filter (e.g. by interface index and IP address) and handle the
+ * generated NeighborEvents.
+ *
+ * @hide
+ */
+public class IpNeighborMonitor extends PacketReader {
+ private static final String TAG = IpNeighborMonitor.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ /**
+ * Make the kernel perform neighbor reachability detection (IPv4 ARP or IPv6 ND)
+ * for the given IP address on the specified interface index.
+ *
+ * @return 0 if the request was successfully passed to the kernel; otherwise return
+ * a non-zero error code.
+ */
+ public static int startKernelNeighborProbe(int ifIndex, InetAddress ip) {
+ final String msgSnippet = "probing ip=" + ip.getHostAddress() + "%" + ifIndex;
+ if (DBG) { Log.d(TAG, msgSnippet); }
+
+ final byte[] msg = RtNetlinkNeighborMessage.newNewNeighborMessage(
+ 1, ip, StructNdMsg.NUD_PROBE, ifIndex, null);
+
+ try {
+ NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_ROUTE, msg);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Error " + msgSnippet + ": " + e);
+ return -e.errno;
+ }
+
+ return 0;
+ }
+
+ public static class NeighborEvent {
+ final long elapsedMs;
+ final short msgType;
+ final int ifindex;
+ final InetAddress ip;
+ final short nudState;
+ final MacAddress macAddr;
+
+ public NeighborEvent(long elapsedMs, short msgType, int ifindex, InetAddress ip,
+ short nudState, MacAddress macAddr) {
+ this.elapsedMs = elapsedMs;
+ this.msgType = msgType;
+ this.ifindex = ifindex;
+ this.ip = ip;
+ this.nudState = nudState;
+ this.macAddr = macAddr;
+ }
+
+ boolean isConnected() {
+ return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateConnected(nudState);
+ }
+
+ boolean isValid() {
+ return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateValid(nudState);
+ }
+
+ @Override
+ public String toString() {
+ final StringJoiner j = new StringJoiner(",", "NeighborEvent{", "}");
+ return j.add("@" + elapsedMs)
+ .add(stringForNlMsgType(msgType))
+ .add("if=" + ifindex)
+ .add(ip.getHostAddress())
+ .add(StructNdMsg.stringForNudState(nudState))
+ .add("[" + macAddr + "]")
+ .toString();
+ }
+ }
+
+ public interface NeighborEventConsumer {
+ // Every neighbor event received on the netlink socket is passed in
+ // here. Subclasses should filter for events of interest.
+ public void accept(NeighborEvent event);
+ }
+
+ private final SharedLog mLog;
+ private final NeighborEventConsumer mConsumer;
+
+ public IpNeighborMonitor(Handler h, SharedLog log, NeighborEventConsumer cb) {
+ super(h, NetlinkSocket.DEFAULT_RECV_BUFSIZE);
+ mLog = log.forSubComponent(TAG);
+ mConsumer = (cb != null) ? cb : (event) -> { /* discard */ };
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ FileDescriptor fd = null;
+
+ try {
+ fd = NetlinkSocket.forProto(OsConstants.NETLINK_ROUTE);
+ Os.bind(fd, makeNetlinkSocketAddress(0, OsConstants.RTMGRP_NEIGH));
+ NetlinkSocket.connectToKernel(fd);
+
+ if (VDBG) {
+ final SocketAddress nlAddr = Os.getsockname(fd);
+ Log.d(TAG, "bound to sockaddr_nl{" + nlAddr.toString() + "}");
+ }
+ } catch (ErrnoException|SocketException e) {
+ logError("Failed to create rtnetlink socket", e);
+ IoUtils.closeQuietly(fd);
+ return null;
+ }
+
+ return fd;
+ }
+
+ @Override
+ protected void handlePacket(byte[] recvbuf, int length) {
+ final long whenMs = SystemClock.elapsedRealtime();
+
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(recvbuf, 0, length);
+ byteBuffer.order(ByteOrder.nativeOrder());
+
+ parseNetlinkMessageBuffer(byteBuffer, whenMs);
+ }
+
+ private void parseNetlinkMessageBuffer(ByteBuffer byteBuffer, long whenMs) {
+ while (byteBuffer.remaining() > 0) {
+ final int position = byteBuffer.position();
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer);
+ if (nlMsg == null || nlMsg.getHeader() == null) {
+ byteBuffer.position(position);
+ mLog.e("unparsable netlink msg: " + hexify(byteBuffer));
+ break;
+ }
+
+ final int srcPortId = nlMsg.getHeader().nlmsg_pid;
+ if (srcPortId != 0) {
+ mLog.e("non-kernel source portId: " + BitUtils.uint32(srcPortId));
+ break;
+ }
+
+ if (nlMsg instanceof NetlinkErrorMessage) {
+ mLog.e("netlink error: " + nlMsg);
+ continue;
+ } else if (!(nlMsg instanceof RtNetlinkNeighborMessage)) {
+ mLog.i("non-rtnetlink neighbor msg: " + nlMsg);
+ continue;
+ }
+
+ evaluateRtNetlinkNeighborMessage((RtNetlinkNeighborMessage) nlMsg, whenMs);
+ }
+ }
+
+ private void evaluateRtNetlinkNeighborMessage(
+ RtNetlinkNeighborMessage neighMsg, long whenMs) {
+ final short msgType = neighMsg.getHeader().nlmsg_type;
+ final StructNdMsg ndMsg = neighMsg.getNdHeader();
+ if (ndMsg == null) {
+ mLog.e("RtNetlinkNeighborMessage without ND message header!");
+ return;
+ }
+
+ final int ifindex = ndMsg.ndm_ifindex;
+ final InetAddress destination = neighMsg.getDestination();
+ final short nudState =
+ (msgType == RTM_DELNEIGH)
+ ? StructNdMsg.NUD_NONE
+ : ndMsg.ndm_state;
+
+ final NeighborEvent event = new NeighborEvent(
+ whenMs, msgType, ifindex, destination, nudState,
+ getMacAddress(neighMsg.getLinkLayerAddress()));
+
+ if (VDBG) {
+ Log.d(TAG, neighMsg.toString());
+ }
+ if (DBG) {
+ Log.d(TAG, event.toString());
+ }
+
+ mConsumer.accept(event);
+ }
+
+ private static MacAddress getMacAddress(byte[] linkLayerAddress) {
+ if (linkLayerAddress != null) {
+ try {
+ return MacAddress.fromBytes(linkLayerAddress);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to parse link-layer address: " + hexify(linkLayerAddress));
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java
new file mode 100644
index 0000000..761db68
--- /dev/null
+++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -0,0 +1,393 @@
+/*
+ * 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.ip;
+
+import static android.net.metrics.IpReachabilityEvent.NUD_FAILED;
+import static android.net.metrics.IpReachabilityEvent.NUD_FAILED_ORGANIC;
+import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST;
+import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST_ORGANIC;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
+import android.net.ip.IpNeighborMonitor.NeighborEvent;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.IpReachabilityEvent;
+import android.net.netlink.StructNdMsg;
+import android.net.util.InterfaceParams;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.DumpUtils.Dump;
+
+import java.io.PrintWriter;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * IpReachabilityMonitor.
+ *
+ * Monitors on-link IP reachability and notifies callers whenever any on-link
+ * addresses of interest appear to have become unresponsive.
+ *
+ * This code does not concern itself with "why" a neighbour might have become
+ * unreachable. Instead, it primarily reacts to the kernel's notion of IP
+ * reachability for each of the neighbours we know to be critically important
+ * to normal network connectivity. As such, it is often "just the messenger":
+ * the neighbours about which it warns are already deemed by the kernel to have
+ * become unreachable.
+ *
+ *
+ * How it works:
+ *
+ * 1. The "on-link neighbours of interest" found in a given LinkProperties
+ * instance are added to a "watch list" via #updateLinkProperties().
+ * This usually means all default gateways and any on-link DNS servers.
+ *
+ * 2. We listen continuously for netlink neighbour messages (RTM_NEWNEIGH,
+ * RTM_DELNEIGH), watching only for neighbours in the watch list.
+ *
+ * - A neighbour going into NUD_REACHABLE, NUD_STALE, NUD_DELAY, and
+ * even NUD_PROBE is perfectly normal; we merely record the new state.
+ *
+ * - A neighbour's entry may be deleted (RTM_DELNEIGH), for example due
+ * to garbage collection. This is not necessarily of immediate
+ * concern; we record the neighbour as moving to NUD_NONE.
+ *
+ * - A neighbour transitioning to NUD_FAILED (for any reason) is
+ * critically important and is handled as described below in #4.
+ *
+ * 3. All on-link neighbours in the watch list can be forcibly "probed" by
+ * calling #probeAll(). This should be called whenever it is important to
+ * verify that critical neighbours on the link are still reachable, e.g.
+ * when roaming between BSSIDs.
+ *
+ * - The kernel will send unicast ARP requests for IPv4 neighbours and
+ * unicast NS packets for IPv6 neighbours. The expected replies will
+ * likely be unicast.
+ *
+ * - The forced probing is done holding a wakelock. The kernel may,
+ * however, initiate probing of a neighbor on its own, i.e. whenever
+ * a neighbour has expired from NUD_DELAY.
+ *
+ * - The kernel sends:
+ *
+ * /proc/sys/net/ipv{4,6}/neigh/<ifname>/ucast_solicit
+ *
+ * number of probes (usually 3) every:
+ *
+ * /proc/sys/net/ipv{4,6}/neigh/<ifname>/retrans_time_ms
+ *
+ * number of milliseconds (usually 1000ms). This normally results in
+ * 3 unicast packets, 1 per second.
+ *
+ * - If no response is received to any of the probe packets, the kernel
+ * marks the neighbour as being in state NUD_FAILED, and the listening
+ * process in #2 will learn of it.
+ *
+ * 4. We call the supplied Callback#notifyLost() function if the loss of a
+ * neighbour in NUD_FAILED would cause IPv4 or IPv6 configuration to
+ * become incomplete (a loss of provisioning).
+ *
+ * - For example, losing all our IPv4 on-link DNS servers (or losing
+ * our only IPv6 default gateway) constitutes a loss of IPv4 (IPv6)
+ * provisioning; Callback#notifyLost() would be called.
+ *
+ * - Since it can be non-trivial to reacquire certain IP provisioning
+ * state it may be best for the link to disconnect completely and
+ * reconnect afresh.
+ *
+ * Accessing an instance of this class from multiple threads is NOT safe.
+ *
+ * @hide
+ */
+public class IpReachabilityMonitor {
+ private static final String TAG = "IpReachabilityMonitor";
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ public interface Callback {
+ // This callback function must execute as quickly as possible as it is
+ // run on the same thread that listens to kernel neighbor updates.
+ //
+ // TODO: refactor to something like notifyProvisioningLost(String msg).
+ public void notifyLost(InetAddress ip, String logMsg);
+ }
+
+ /**
+ * Encapsulates IpReachabilityMonitor depencencies on systems that hinder unit testing.
+ * TODO: consider also wrapping MultinetworkPolicyTracker in this interface.
+ */
+ interface Dependencies {
+ void acquireWakeLock(long durationMs);
+
+ static Dependencies makeDefault(Context context, String iface) {
+ final String lockName = TAG + "." + iface;
+ final PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ final WakeLock lock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, lockName);
+
+ return new Dependencies() {
+ public void acquireWakeLock(long durationMs) {
+ lock.acquire(durationMs);
+ }
+ };
+ }
+ }
+
+ private final InterfaceParams mInterfaceParams;
+ private final IpNeighborMonitor mIpNeighborMonitor;
+ private final SharedLog mLog;
+ private final Callback mCallback;
+ private final Dependencies mDependencies;
+ private final boolean mUsingMultinetworkPolicyTracker;
+ private final ConnectivityManager mCm;
+ private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
+ private LinkProperties mLinkProperties = new LinkProperties();
+ private Map<InetAddress, NeighborEvent> mNeighborWatchList = new HashMap<>();
+ // Time in milliseconds of the last forced probe request.
+ private volatile long mLastProbeTimeMs;
+
+ public IpReachabilityMonitor(
+ Context context, InterfaceParams ifParams, Handler h, SharedLog log, Callback callback,
+ boolean usingMultinetworkPolicyTracker) {
+ this(context, ifParams, h, log, callback, usingMultinetworkPolicyTracker,
+ Dependencies.makeDefault(context, ifParams.name));
+ }
+
+ @VisibleForTesting
+ IpReachabilityMonitor(Context context, InterfaceParams ifParams, Handler h, SharedLog log,
+ Callback callback, boolean usingMultinetworkPolicyTracker, Dependencies dependencies) {
+ if (ifParams == null) throw new IllegalArgumentException("null InterfaceParams");
+
+ mInterfaceParams = ifParams;
+ mLog = log.forSubComponent(TAG);
+ mCallback = callback;
+ mUsingMultinetworkPolicyTracker = usingMultinetworkPolicyTracker;
+ mCm = context.getSystemService(ConnectivityManager.class);
+ mDependencies = dependencies;
+
+ mIpNeighborMonitor = new IpNeighborMonitor(h, mLog,
+ (NeighborEvent event) -> {
+ if (mInterfaceParams.index != event.ifindex) return;
+ if (!mNeighborWatchList.containsKey(event.ip)) return;
+
+ final NeighborEvent prev = mNeighborWatchList.put(event.ip, event);
+
+ // TODO: Consider what to do with other states that are not within
+ // NeighborEvent#isValid() (i.e. NUD_NONE, NUD_INCOMPLETE).
+ if (event.nudState == StructNdMsg.NUD_FAILED) {
+ mLog.w("ALERT neighbor went from: " + prev + " to: " + event);
+ handleNeighborLost(event);
+ }
+ });
+ mIpNeighborMonitor.start();
+ }
+
+ public void stop() {
+ mIpNeighborMonitor.stop();
+ clearLinkProperties();
+ }
+
+ public void dump(PrintWriter pw) {
+ DumpUtils.dumpAsync(
+ mIpNeighborMonitor.getHandler(),
+ new Dump() {
+ @Override
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(describeWatchList("\n"));
+ }
+ },
+ pw, "", 1000);
+ }
+
+ private String describeWatchList() { return describeWatchList(" "); }
+
+ private String describeWatchList(String sep) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("iface{" + mInterfaceParams + "}," + sep);
+ sb.append("ntable=[" + sep);
+ String delimiter = "";
+ for (Map.Entry<InetAddress, NeighborEvent> entry : mNeighborWatchList.entrySet()) {
+ sb.append(delimiter).append(entry.getKey().getHostAddress() + "/" + entry.getValue());
+ delimiter = "," + sep;
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+
+ private static boolean isOnLink(List<RouteInfo> routes, InetAddress ip) {
+ for (RouteInfo route : routes) {
+ if (!route.hasGateway() && route.matches(ip)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void updateLinkProperties(LinkProperties lp) {
+ if (!mInterfaceParams.name.equals(lp.getInterfaceName())) {
+ // TODO: figure out whether / how to cope with interface changes.
+ Log.wtf(TAG, "requested LinkProperties interface '" + lp.getInterfaceName() +
+ "' does not match: " + mInterfaceParams.name);
+ return;
+ }
+
+ mLinkProperties = new LinkProperties(lp);
+ Map<InetAddress, NeighborEvent> newNeighborWatchList = new HashMap<>();
+
+ final List<RouteInfo> routes = mLinkProperties.getRoutes();
+ for (RouteInfo route : routes) {
+ if (route.hasGateway()) {
+ InetAddress gw = route.getGateway();
+ if (isOnLink(routes, gw)) {
+ newNeighborWatchList.put(gw, mNeighborWatchList.getOrDefault(gw, null));
+ }
+ }
+ }
+
+ for (InetAddress dns : lp.getDnsServers()) {
+ if (isOnLink(routes, dns)) {
+ newNeighborWatchList.put(dns, mNeighborWatchList.getOrDefault(dns, null));
+ }
+ }
+
+ mNeighborWatchList = newNeighborWatchList;
+ if (DBG) { Log.d(TAG, "watch: " + describeWatchList()); }
+ }
+
+ public void clearLinkProperties() {
+ mLinkProperties.clear();
+ mNeighborWatchList.clear();
+ if (DBG) { Log.d(TAG, "clear: " + describeWatchList()); }
+ }
+
+ private void handleNeighborLost(NeighborEvent event) {
+ final LinkProperties whatIfLp = new LinkProperties(mLinkProperties);
+
+ InetAddress ip = null;
+ for (Map.Entry<InetAddress, NeighborEvent> entry : mNeighborWatchList.entrySet()) {
+ // TODO: Consider using NeighborEvent#isValid() here; it's more
+ // strict but may interact badly if other entries are somehow in
+ // NUD_INCOMPLETE (say, during network attach).
+ if (entry.getValue().nudState != StructNdMsg.NUD_FAILED) continue;
+
+ ip = entry.getKey();
+ for (RouteInfo route : mLinkProperties.getRoutes()) {
+ if (ip.equals(route.getGateway())) {
+ whatIfLp.removeRoute(route);
+ }
+ }
+
+ if (avoidingBadLinks() || !(ip instanceof Inet6Address)) {
+ // We should do this unconditionally, but alas we cannot: b/31827713.
+ whatIfLp.removeDnsServer(ip);
+ }
+ }
+
+ final boolean lostProvisioning =
+ (mLinkProperties.isIPv4Provisioned() && !whatIfLp.isIPv4Provisioned())
+ || (mLinkProperties.isIPv6Provisioned() && !whatIfLp.isIPv6Provisioned());
+
+ if (lostProvisioning) {
+ final String logMsg = "FAILURE: LOST_PROVISIONING, " + event;
+ Log.w(TAG, logMsg);
+ if (mCallback != null) {
+ // TODO: remove |ip| when the callback signature no longer has
+ // an InetAddress argument.
+ mCallback.notifyLost(ip, logMsg);
+ }
+ }
+ logNudFailed(lostProvisioning);
+ }
+
+ private boolean avoidingBadLinks() {
+ return !mUsingMultinetworkPolicyTracker || mCm.getAvoidBadWifi();
+ }
+
+ public void probeAll() {
+ final List<InetAddress> ipProbeList = new ArrayList<>(mNeighborWatchList.keySet());
+
+ if (!ipProbeList.isEmpty()) {
+ // Keep the CPU awake long enough to allow all ARP/ND
+ // probes a reasonable chance at success. See b/23197666.
+ //
+ // The wakelock we use is (by default) refcounted, and this version
+ // of acquire(timeout) queues a release message to keep acquisitions
+ // and releases balanced.
+ mDependencies.acquireWakeLock(getProbeWakeLockDuration());
+ }
+
+ for (InetAddress ip : ipProbeList) {
+ final int rval = IpNeighborMonitor.startKernelNeighborProbe(mInterfaceParams.index, ip);
+ mLog.log(String.format("put neighbor %s into NUD_PROBE state (rval=%d)",
+ ip.getHostAddress(), rval));
+ logEvent(IpReachabilityEvent.PROBE, rval);
+ }
+ mLastProbeTimeMs = SystemClock.elapsedRealtime();
+ }
+
+ private static long getProbeWakeLockDuration() {
+ // Ideally, this would be computed by examining the values of:
+ //
+ // /proc/sys/net/ipv[46]/neigh/<ifname>/ucast_solicit
+ //
+ // and:
+ //
+ // /proc/sys/net/ipv[46]/neigh/<ifname>/retrans_time_ms
+ //
+ // For now, just make some assumptions.
+ final long numUnicastProbes = 3;
+ final long retransTimeMs = 1000;
+ final long gracePeriodMs = 500;
+ return (numUnicastProbes * retransTimeMs) + gracePeriodMs;
+ }
+
+ private void logEvent(int probeType, int errorCode) {
+ int eventType = probeType | (errorCode & 0xff);
+ mMetricsLog.log(mInterfaceParams.name, new IpReachabilityEvent(eventType));
+ }
+
+ private void logNudFailed(boolean lostProvisioning) {
+ long duration = SystemClock.elapsedRealtime() - mLastProbeTimeMs;
+ boolean isFromProbe = (duration < getProbeWakeLockDuration());
+ int eventType = nudFailureEventType(isFromProbe, lostProvisioning);
+ mMetricsLog.log(mInterfaceParams.name, new IpReachabilityEvent(eventType));
+ }
+
+ /**
+ * Returns the NUD failure event type code corresponding to the given conditions.
+ */
+ private static int nudFailureEventType(boolean isFromProbe, boolean isProvisioningLost) {
+ if (isFromProbe) {
+ return isProvisioningLost ? PROVISIONING_LOST : NUD_FAILED;
+ } else {
+ return isProvisioningLost ? PROVISIONING_LOST_ORGANIC : NUD_FAILED_ORGANIC;
+ }
+ }
+}
diff --git a/src/android/net/util/ConnectivityPacketSummary.java b/src/android/net/util/ConnectivityPacketSummary.java
new file mode 100644
index 0000000..08c3f60
--- /dev/null
+++ b/src/android/net/util/ConnectivityPacketSummary.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.util;
+
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static com.android.server.util.NetworkStackConstants.ARP_HWTYPE_ETHER;
+import static com.android.server.util.NetworkStackConstants.ARP_PAYLOAD_LEN;
+import static com.android.server.util.NetworkStackConstants.ARP_REPLY;
+import static com.android.server.util.NetworkStackConstants.ARP_REQUEST;
+import static com.android.server.util.NetworkStackConstants.DHCP4_CLIENT_PORT;
+import static com.android.server.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.server.util.NetworkStackConstants.ETHER_DST_ADDR_OFFSET;
+import static com.android.server.util.NetworkStackConstants.ETHER_HEADER_LEN;
+import static com.android.server.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET;
+import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_ARP;
+import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_IPV4;
+import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_OFFSET;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_HEADER_MIN_LEN;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_MIN_LENGTH;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
+import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_LEN;
+import static com.android.server.util.NetworkStackConstants.IPV4_DST_ADDR_OFFSET;
+import static com.android.server.util.NetworkStackConstants.IPV4_FLAGS_OFFSET;
+import static com.android.server.util.NetworkStackConstants.IPV4_FRAGMENT_MASK;
+import static com.android.server.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.server.util.NetworkStackConstants.IPV4_IHL_MASK;
+import static com.android.server.util.NetworkStackConstants.IPV4_PROTOCOL_OFFSET;
+import static com.android.server.util.NetworkStackConstants.IPV4_SRC_ADDR_OFFSET;
+import static com.android.server.util.NetworkStackConstants.IPV6_ADDR_LEN;
+import static com.android.server.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.server.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET;
+import static com.android.server.util.NetworkStackConstants.IPV6_SRC_ADDR_OFFSET;
+import static com.android.server.util.NetworkStackConstants.UDP_HEADER_LEN;
+
+import android.net.MacAddress;
+import android.net.dhcp.DhcpPacket;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.StringJoiner;
+
+
+/**
+ * Critical connectivity packet summarizing class.
+ *
+ * Outputs short descriptions of ARP, DHCPv4, and IPv6 RS/RA/NS/NA packets.
+ *
+ * @hide
+ */
+public class ConnectivityPacketSummary {
+ private static final String TAG = ConnectivityPacketSummary.class.getSimpleName();
+
+ private final byte[] mHwAddr;
+ private final byte[] mBytes;
+ private final int mLength;
+ private final ByteBuffer mPacket;
+ private final String mSummary;
+
+ public static String summarize(MacAddress hwaddr, byte[] buffer) {
+ return summarize(hwaddr, buffer, buffer.length);
+ }
+
+ // Methods called herein perform some but by no means all error checking.
+ // They may throw runtime exceptions on malformed packets.
+ public static String summarize(MacAddress macAddr, byte[] buffer, int length) {
+ if ((macAddr == null) || (buffer == null)) return null;
+ length = Math.min(length, buffer.length);
+ return (new ConnectivityPacketSummary(macAddr, buffer, length)).toString();
+ }
+
+ private ConnectivityPacketSummary(MacAddress macAddr, byte[] buffer, int length) {
+ mHwAddr = macAddr.toByteArray();
+ mBytes = buffer;
+ mLength = Math.min(length, mBytes.length);
+ mPacket = ByteBuffer.wrap(mBytes, 0, mLength);
+ mPacket.order(ByteOrder.BIG_ENDIAN);
+
+ final StringJoiner sj = new StringJoiner(" ");
+ // TODO: support other link-layers, or even no link-layer header.
+ parseEther(sj);
+ mSummary = sj.toString();
+ }
+
+ public String toString() {
+ return mSummary;
+ }
+
+ private void parseEther(StringJoiner sj) {
+ if (mPacket.remaining() < ETHER_HEADER_LEN) {
+ sj.add("runt:").add(asString(mPacket.remaining()));
+ return;
+ }
+
+ mPacket.position(ETHER_SRC_ADDR_OFFSET);
+ final ByteBuffer srcMac = (ByteBuffer) mPacket.slice().limit(ETHER_ADDR_LEN);
+ sj.add(ByteBuffer.wrap(mHwAddr).equals(srcMac) ? "TX" : "RX");
+ sj.add(getMacAddressString(srcMac));
+
+ mPacket.position(ETHER_DST_ADDR_OFFSET);
+ final ByteBuffer dstMac = (ByteBuffer) mPacket.slice().limit(ETHER_ADDR_LEN);
+ sj.add(">").add(getMacAddressString(dstMac));
+
+ mPacket.position(ETHER_TYPE_OFFSET);
+ final int etherType = asUint(mPacket.getShort());
+ switch (etherType) {
+ case ETHER_TYPE_ARP:
+ sj.add("arp");
+ parseARP(sj);
+ break;
+ case ETHER_TYPE_IPV4:
+ sj.add("ipv4");
+ parseIPv4(sj);
+ break;
+ case ETHER_TYPE_IPV6:
+ sj.add("ipv6");
+ parseIPv6(sj);
+ break;
+ default:
+ // Unknown ether type.
+ sj.add("ethtype").add(asString(etherType));
+ break;
+ }
+ }
+
+ private void parseARP(StringJoiner sj) {
+ if (mPacket.remaining() < ARP_PAYLOAD_LEN) {
+ sj.add("runt:").add(asString(mPacket.remaining()));
+ return;
+ }
+
+ if (asUint(mPacket.getShort()) != ARP_HWTYPE_ETHER ||
+ asUint(mPacket.getShort()) != ETHER_TYPE_IPV4 ||
+ asUint(mPacket.get()) != ETHER_ADDR_LEN ||
+ asUint(mPacket.get()) != IPV4_ADDR_LEN) {
+ sj.add("unexpected header");
+ return;
+ }
+
+ final int opCode = asUint(mPacket.getShort());
+
+ final String senderHwAddr = getMacAddressString(mPacket);
+ final String senderIPv4 = getIPv4AddressString(mPacket);
+ getMacAddressString(mPacket); // target hardware address, unused
+ final String targetIPv4 = getIPv4AddressString(mPacket);
+
+ if (opCode == ARP_REQUEST) {
+ sj.add("who-has").add(targetIPv4);
+ } else if (opCode == ARP_REPLY) {
+ sj.add("reply").add(senderIPv4).add(senderHwAddr);
+ } else {
+ sj.add("unknown opcode").add(asString(opCode));
+ }
+ }
+
+ private void parseIPv4(StringJoiner sj) {
+ if (!mPacket.hasRemaining()) {
+ sj.add("runt");
+ return;
+ }
+
+ final int startOfIpLayer = mPacket.position();
+ final int ipv4HeaderLength = (mPacket.get(startOfIpLayer) & IPV4_IHL_MASK) * 4;
+ if (mPacket.remaining() < ipv4HeaderLength ||
+ mPacket.remaining() < IPV4_HEADER_MIN_LEN) {
+ sj.add("runt:").add(asString(mPacket.remaining()));
+ return;
+ }
+ final int startOfTransportLayer = startOfIpLayer + ipv4HeaderLength;
+
+ mPacket.position(startOfIpLayer + IPV4_FLAGS_OFFSET);
+ final int flagsAndFragment = asUint(mPacket.getShort());
+ final boolean isFragment = (flagsAndFragment & IPV4_FRAGMENT_MASK) != 0;
+
+ mPacket.position(startOfIpLayer + IPV4_PROTOCOL_OFFSET);
+ final int protocol = asUint(mPacket.get());
+
+ mPacket.position(startOfIpLayer + IPV4_SRC_ADDR_OFFSET);
+ final String srcAddr = getIPv4AddressString(mPacket);
+
+ mPacket.position(startOfIpLayer + IPV4_DST_ADDR_OFFSET);
+ final String dstAddr = getIPv4AddressString(mPacket);
+
+ sj.add(srcAddr).add(">").add(dstAddr);
+
+ mPacket.position(startOfTransportLayer);
+ if (protocol == IPPROTO_UDP) {
+ sj.add("udp");
+ if (isFragment) sj.add("fragment");
+ else parseUDP(sj);
+ } else {
+ sj.add("proto").add(asString(protocol));
+ if (isFragment) sj.add("fragment");
+ }
+ }
+
+ private void parseIPv6(StringJoiner sj) {
+ if (mPacket.remaining() < IPV6_HEADER_LEN) {
+ sj.add("runt:").add(asString(mPacket.remaining()));
+ return;
+ }
+
+ final int startOfIpLayer = mPacket.position();
+
+ mPacket.position(startOfIpLayer + IPV6_PROTOCOL_OFFSET);
+ final int protocol = asUint(mPacket.get());
+
+ mPacket.position(startOfIpLayer + IPV6_SRC_ADDR_OFFSET);
+ final String srcAddr = getIPv6AddressString(mPacket);
+ final String dstAddr = getIPv6AddressString(mPacket);
+
+ sj.add(srcAddr).add(">").add(dstAddr);
+
+ mPacket.position(startOfIpLayer + IPV6_HEADER_LEN);
+ if (protocol == IPPROTO_ICMPV6) {
+ sj.add("icmp6");
+ parseICMPv6(sj);
+ } else {
+ sj.add("proto").add(asString(protocol));
+ }
+ }
+
+ private void parseICMPv6(StringJoiner sj) {
+ if (mPacket.remaining() < ICMPV6_HEADER_MIN_LEN) {
+ sj.add("runt:").add(asString(mPacket.remaining()));
+ return;
+ }
+
+ final int icmp6Type = asUint(mPacket.get());
+ final int icmp6Code = asUint(mPacket.get());
+ mPacket.getShort(); // checksum, unused
+
+ switch (icmp6Type) {
+ case ICMPV6_ROUTER_SOLICITATION:
+ sj.add("rs");
+ parseICMPv6RouterSolicitation(sj);
+ break;
+ case ICMPV6_ROUTER_ADVERTISEMENT:
+ sj.add("ra");
+ parseICMPv6RouterAdvertisement(sj);
+ break;
+ case ICMPV6_NEIGHBOR_SOLICITATION:
+ sj.add("ns");
+ parseICMPv6NeighborMessage(sj);
+ break;
+ case ICMPV6_NEIGHBOR_ADVERTISEMENT:
+ sj.add("na");
+ parseICMPv6NeighborMessage(sj);
+ break;
+ default:
+ sj.add("type").add(asString(icmp6Type));
+ sj.add("code").add(asString(icmp6Code));
+ break;
+ }
+ }
+
+ private void parseICMPv6RouterSolicitation(StringJoiner sj) {
+ final int RESERVED = 4;
+ if (mPacket.remaining() < RESERVED) {
+ sj.add("runt:").add(asString(mPacket.remaining()));
+ return;
+ }
+
+ mPacket.position(mPacket.position() + RESERVED);
+ parseICMPv6NeighborDiscoveryOptions(sj);
+ }
+
+ private void parseICMPv6RouterAdvertisement(StringJoiner sj) {
+ final int FLAGS_AND_TIMERS = 3 * 4;
+ if (mPacket.remaining() < FLAGS_AND_TIMERS) {
+ sj.add("runt:").add(asString(mPacket.remaining()));
+ return;
+ }
+
+ mPacket.position(mPacket.position() + FLAGS_AND_TIMERS);
+ parseICMPv6NeighborDiscoveryOptions(sj);
+ }
+
+ private void parseICMPv6NeighborMessage(StringJoiner sj) {
+ final int RESERVED = 4;
+ final int minReq = RESERVED + IPV6_ADDR_LEN;
+ if (mPacket.remaining() < minReq) {
+ sj.add("runt:").add(asString(mPacket.remaining()));
+ return;
+ }
+
+ mPacket.position(mPacket.position() + RESERVED);
+ sj.add(getIPv6AddressString(mPacket));
+ parseICMPv6NeighborDiscoveryOptions(sj);
+ }
+
+ private void parseICMPv6NeighborDiscoveryOptions(StringJoiner sj) {
+ // All ND options are TLV, where T is one byte and L is one byte equal
+ // to the length of T + L + V in units of 8 octets.
+ while (mPacket.remaining() >= ICMPV6_ND_OPTION_MIN_LENGTH) {
+ final int ndType = asUint(mPacket.get());
+ final int ndLength = asUint(mPacket.get());
+ final int ndBytes = ndLength * ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR - 2;
+ if (ndBytes < 0 || ndBytes > mPacket.remaining()) {
+ sj.add("<malformed>");
+ break;
+ }
+ final int position = mPacket.position();
+
+ switch (ndType) {
+ case ICMPV6_ND_OPTION_SLLA:
+ sj.add("slla");
+ sj.add(getMacAddressString(mPacket));
+ break;
+ case ICMPV6_ND_OPTION_TLLA:
+ sj.add("tlla");
+ sj.add(getMacAddressString(mPacket));
+ break;
+ case ICMPV6_ND_OPTION_MTU:
+ sj.add("mtu");
+ final short reserved = mPacket.getShort();
+ sj.add(asString(mPacket.getInt()));
+ break;
+ default:
+ // Skip.
+ break;
+ }
+
+ mPacket.position(position + ndBytes);
+ }
+ }
+
+ private void parseUDP(StringJoiner sj) {
+ if (mPacket.remaining() < UDP_HEADER_LEN) {
+ sj.add("runt:").add(asString(mPacket.remaining()));
+ return;
+ }
+
+ final int previous = mPacket.position();
+ final int srcPort = asUint(mPacket.getShort());
+ final int dstPort = asUint(mPacket.getShort());
+ sj.add(asString(srcPort)).add(">").add(asString(dstPort));
+
+ mPacket.position(previous + UDP_HEADER_LEN);
+ if (srcPort == DHCP4_CLIENT_PORT || dstPort == DHCP4_CLIENT_PORT) {
+ sj.add("dhcp4");
+ parseDHCPv4(sj);
+ }
+ }
+
+ private void parseDHCPv4(StringJoiner sj) {
+ final DhcpPacket dhcpPacket;
+ try {
+ dhcpPacket = DhcpPacket.decodeFullPacket(mBytes, mLength, DhcpPacket.ENCAP_L2);
+ sj.add(dhcpPacket.toString());
+ } catch (DhcpPacket.ParseException e) {
+ sj.add("parse error: " + e);
+ }
+ }
+
+ private static String getIPv4AddressString(ByteBuffer ipv4) {
+ return getIpAddressString(ipv4, IPV4_ADDR_LEN);
+ }
+
+ private static String getIPv6AddressString(ByteBuffer ipv6) {
+ return getIpAddressString(ipv6, IPV6_ADDR_LEN);
+ }
+
+ private static String getIpAddressString(ByteBuffer ip, int byteLength) {
+ if (ip == null || ip.remaining() < byteLength) return "invalid";
+
+ byte[] bytes = new byte[byteLength];
+ ip.get(bytes, 0, byteLength);
+ try {
+ InetAddress addr = InetAddress.getByAddress(bytes);
+ return addr.getHostAddress();
+ } catch (UnknownHostException uhe) {
+ return "unknown";
+ }
+ }
+
+ private static String getMacAddressString(ByteBuffer mac) {
+ if (mac == null || mac.remaining() < ETHER_ADDR_LEN) return "invalid";
+
+ byte[] bytes = new byte[ETHER_ADDR_LEN];
+ mac.get(bytes, 0, bytes.length);
+ Object[] printableBytes = new Object[bytes.length];
+ int i = 0;
+ for (byte b : bytes) printableBytes[i++] = new Byte(b);
+
+ final String MAC48_FORMAT = "%02x:%02x:%02x:%02x:%02x:%02x";
+ return String.format(MAC48_FORMAT, printableBytes);
+ }
+
+ /**
+ * Convenience method to convert an int to a String.
+ */
+ public static String asString(int i) {
+ return Integer.toString(i);
+ }
+
+ /**
+ * Convenience method to read a byte as an unsigned int.
+ */
+ public static int asUint(byte b) {
+ return (b & 0xff);
+ }
+
+ /**
+ * Convenience method to read a short as an unsigned int.
+ */
+ public static int asUint(short s) {
+ return (s & 0xffff);
+ }
+}
diff --git a/src/android/net/util/FdEventsReader.java b/src/android/net/util/FdEventsReader.java
new file mode 100644
index 0000000..8bbf449
--- /dev/null
+++ b/src/android/net/util/FdEventsReader.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.util;
+
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+
+
+/**
+ * This class encapsulates the mechanics of registering a file descriptor
+ * with a thread's Looper and handling read events (and errors).
+ *
+ * Subclasses MUST implement createFd() and SHOULD override handlePacket(). They MAY override
+ * onStop() and onStart().
+ *
+ * Subclasses can expect a call life-cycle like the following:
+ *
+ * [1] when a client calls start(), createFd() is called, followed by the onStart() hook if all
+ * goes well. Implementations may override onStart() for additional initialization.
+ *
+ * [2] yield, waiting for read event or error notification:
+ *
+ * [a] readPacket() && handlePacket()
+ *
+ * [b] if (no error):
+ * goto 2
+ * else:
+ * goto 3
+ *
+ * [3] when a client calls stop(), the onStop() hook is called (unless already stopped or never
+ * started). Implementations may override onStop() for additional cleanup.
+ *
+ * The packet receive buffer is recycled on every read call, so subclasses
+ * should make any copies they would like inside their handlePacket()
+ * implementation.
+ *
+ * All public methods MUST only be called from the same thread with which
+ * the Handler constructor argument is associated.
+ *
+ * @hide
+ */
+public abstract class FdEventsReader<BufferType> {
+ private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+ private static final int UNREGISTER_THIS_FD = 0;
+
+ @NonNull
+ private final Handler mHandler;
+ @NonNull
+ private final MessageQueue mQueue;
+ @NonNull
+ private final BufferType mBuffer;
+ @Nullable
+ private FileDescriptor mFd;
+ private long mPacketsReceived;
+
+ protected static void closeFd(FileDescriptor fd) {
+ IoUtils.closeQuietly(fd);
+ }
+
+ protected FdEventsReader(@NonNull Handler h, @NonNull BufferType buffer) {
+ mHandler = h;
+ mQueue = mHandler.getLooper().getQueue();
+ mBuffer = buffer;
+ }
+
+ public void start() {
+ if (onCorrectThread()) {
+ createAndRegisterFd();
+ } else {
+ mHandler.post(() -> {
+ logError("start() called from off-thread", null);
+ createAndRegisterFd();
+ });
+ }
+ }
+
+ public void stop() {
+ if (onCorrectThread()) {
+ unregisterAndDestroyFd();
+ } else {
+ mHandler.post(() -> {
+ logError("stop() called from off-thread", null);
+ unregisterAndDestroyFd();
+ });
+ }
+ }
+
+ @NonNull
+ public Handler getHandler() { return mHandler; }
+
+ protected abstract int recvBufSize(@NonNull BufferType buffer);
+
+ public int recvBufSize() { return recvBufSize(mBuffer); }
+
+ /**
+ * Get the number of successful calls to {@link #readPacket(FileDescriptor, Object)}.
+ *
+ * <p>A call was successful if {@link #readPacket(FileDescriptor, Object)} returned a value > 0.
+ */
+ public final long numPacketsReceived() { return mPacketsReceived; }
+
+ /**
+ * Subclasses MUST create the listening socket here, including setting
+ * all desired socket options, interface or address/port binding, etc.
+ */
+ @Nullable
+ protected abstract FileDescriptor createFd();
+
+ /**
+ * Implementations MUST return the bytes read or throw an Exception.
+ *
+ * <p>The caller may throw a {@link ErrnoException} with {@link OsConstants#EAGAIN} or
+ * {@link OsConstants#EINTR}, in which case {@link FdEventsReader} will ignore the buffer
+ * contents and respectively wait for further input or retry the read immediately. For all other
+ * exceptions, the {@link FdEventsReader} will be stopped with no more interactions with this
+ * method.
+ */
+ protected abstract int readPacket(@NonNull FileDescriptor fd, @NonNull BufferType buffer)
+ throws Exception;
+
+ /**
+ * Called by the main loop for every packet. Any desired copies of
+ * |recvbuf| should be made in here, as the underlying byte array is
+ * reused across all reads.
+ */
+ protected void handlePacket(@NonNull BufferType recvbuf, int length) {}
+
+ /**
+ * Called by the main loop to log errors. In some cases |e| may be null.
+ */
+ protected void logError(@NonNull String msg, @Nullable Exception e) {}
+
+ /**
+ * Called by start(), if successful, just prior to returning.
+ */
+ protected void onStart() {}
+
+ /**
+ * Called by stop() just prior to returning.
+ */
+ protected void onStop() {}
+
+ private void createAndRegisterFd() {
+ if (mFd != null) return;
+
+ try {
+ mFd = createFd();
+ if (mFd != null) {
+ // Force the socket to be non-blocking.
+ IoUtils.setBlocking(mFd, false);
+ }
+ } catch (Exception e) {
+ logError("Failed to create socket: ", e);
+ closeFd(mFd);
+ mFd = null;
+ }
+
+ if (mFd == null) return;
+
+ mQueue.addOnFileDescriptorEventListener(
+ mFd,
+ FD_EVENTS,
+ (fd, events) -> {
+ // Always call handleInput() so read/recvfrom are given
+ // a proper chance to encounter a meaningful errno and
+ // perhaps log a useful error message.
+ if (!isRunning() || !handleInput()) {
+ unregisterAndDestroyFd();
+ return UNREGISTER_THIS_FD;
+ }
+ return FD_EVENTS;
+ });
+ onStart();
+ }
+
+ private boolean isRunning() { return (mFd != null) && mFd.valid(); }
+
+ // Keep trying to read until we get EAGAIN/EWOULDBLOCK or some fatal error.
+ private boolean handleInput() {
+ while (isRunning()) {
+ final int bytesRead;
+
+ try {
+ bytesRead = readPacket(mFd, mBuffer);
+ if (bytesRead < 1) {
+ if (isRunning()) logError("Socket closed, exiting", null);
+ break;
+ }
+ mPacketsReceived++;
+ } catch (ErrnoException e) {
+ if (e.errno == OsConstants.EAGAIN) {
+ // We've read everything there is to read this time around.
+ return true;
+ } else if (e.errno == OsConstants.EINTR) {
+ continue;
+ } else {
+ if (isRunning()) logError("readPacket error: ", e);
+ break;
+ }
+ } catch (Exception e) {
+ if (isRunning()) logError("readPacket error: ", e);
+ break;
+ }
+
+ try {
+ handlePacket(mBuffer, bytesRead);
+ } catch (Exception e) {
+ logError("handlePacket error: ", e);
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ private void unregisterAndDestroyFd() {
+ if (mFd == null) return;
+
+ mQueue.removeOnFileDescriptorEventListener(mFd);
+ closeFd(mFd);
+ mFd = null;
+ onStop();
+ }
+
+ private boolean onCorrectThread() {
+ return (mHandler.getLooper() == Looper.myLooper());
+ }
+}
diff --git a/src/android/net/util/PacketReader.java b/src/android/net/util/PacketReader.java
new file mode 100644
index 0000000..4aec6b6
--- /dev/null
+++ b/src/android/net/util/PacketReader.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.util;
+
+import static java.lang.Math.max;
+
+import android.os.Handler;
+import android.system.Os;
+
+import java.io.FileDescriptor;
+
+/**
+ * Specialization of {@link FdEventsReader} that reads packets into a byte array.
+ *
+ * TODO: rename this class to something more correctly descriptive (something
+ * like [or less horrible than] FdReadEventsHandler?).
+ *
+ * @hide
+ */
+public abstract class PacketReader extends FdEventsReader<byte[]> {
+
+ public static final int DEFAULT_RECV_BUF_SIZE = 2 * 1024;
+
+ protected PacketReader(Handler h) {
+ this(h, DEFAULT_RECV_BUF_SIZE);
+ }
+
+ protected PacketReader(Handler h, int recvBufSize) {
+ super(h, new byte[max(recvBufSize, DEFAULT_RECV_BUF_SIZE)]);
+ }
+
+ @Override
+ protected final int recvBufSize(byte[] buffer) {
+ return buffer.length;
+ }
+
+ /**
+ * Subclasses MAY override this to change the default read() implementation
+ * in favour of, say, recvfrom().
+ *
+ * Implementations MUST return the bytes read or throw an Exception.
+ */
+ @Override
+ protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception {
+ return Os.read(fd, packetBuffer, 0, packetBuffer.length);
+ }
+}
diff --git a/src/com/android/server/NetworkStackService.java b/src/com/android/server/NetworkStackService.java
index cca71e7..4080ddf 100644
--- a/src/com/android/server/NetworkStackService.java
+++ b/src/com/android/server/NetworkStackService.java
@@ -39,6 +39,8 @@ import android.net.dhcp.DhcpServer;
import android.net.dhcp.DhcpServingParams;
import android.net.dhcp.DhcpServingParamsParcel;
import android.net.dhcp.IDhcpServerCallbacks;
+import android.net.ip.IIpClientCallbacks;
+import android.net.ip.IpClient;
import android.net.shared.PrivateDnsConfig;
import android.net.util.SharedLog;
import android.os.IBinder;
@@ -50,7 +52,11 @@ import com.android.server.connectivity.NetworkMonitor;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
/**
* Android service used to start the network stack when bound to via an intent.
@@ -80,6 +86,8 @@ public class NetworkStackService extends Service {
private static final int NUM_VALIDATION_LOG_LINES = 20;
private final Context mContext;
private final ConnectivityManager mCm;
+ @GuardedBy("mIpClients")
+ private final ArrayList<WeakReference<IpClient>> mIpClients = new ArrayList<>();
private static final int MAX_VALIDATION_LOGS = 10;
@GuardedBy("mValidationLogs")
@@ -138,6 +146,24 @@ public class NetworkStackService extends Service {
}
@Override
+ public void makeIpClient(String ifName, IIpClientCallbacks cb) throws RemoteException {
+ final IpClient ipClient = new IpClient(mContext, ifName, cb);
+
+ synchronized (mIpClients) {
+ final Iterator<WeakReference<IpClient>> it = mIpClients.iterator();
+ while (it.hasNext()) {
+ final IpClient ipc = it.next().get();
+ if (ipc == null) {
+ it.remove();
+ }
+ }
+ mIpClients.add(new WeakReference<>(ipClient));
+ }
+
+ cb.onIpClientCreated(ipClient.makeConnector());
+ }
+
+ @Override
protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout,
@Nullable String[] args) {
checkDumpPermission();
@@ -145,6 +171,33 @@ public class NetworkStackService extends Service {
pw.println("NetworkStack logs:");
mLog.dump(fd, pw, args);
+ // Dump full IpClient logs for non-GCed clients
+ pw.println();
+ pw.println("Recently active IpClient logs:");
+ final ArrayList<IpClient> ipClients = new ArrayList<>();
+ final HashSet<String> dumpedIpClientIfaces = new HashSet<>();
+ synchronized (mIpClients) {
+ for (WeakReference<IpClient> ipcRef : mIpClients) {
+ final IpClient ipc = ipcRef.get();
+ if (ipc != null) {
+ ipClients.add(ipc);
+ }
+ }
+ }
+
+ for (IpClient ipc : ipClients) {
+ pw.println(ipc.getName());
+ pw.increaseIndent();
+ ipc.dump(fd, pw, args);
+ pw.decreaseIndent();
+ dumpedIpClientIfaces.add(ipc.getInterfaceName());
+ }
+
+ // State machine and connectivity metrics logs are kept for GCed IpClients
+ pw.println();
+ pw.println("Other IpClient logs:");
+ IpClient.dumpAllLogs(fout, dumpedIpClientIfaces);
+
pw.println();
pw.println("Validation logs (most recent first):");
synchronized (mValidationLogs) {
diff --git a/src/com/android/server/util/NetworkStackConstants.java b/src/com/android/server/util/NetworkStackConstants.java
index bb5900c..eedaf30 100644
--- a/src/com/android/server/util/NetworkStackConstants.java
+++ b/src/com/android/server/util/NetworkStackConstants.java
@@ -32,12 +32,103 @@ public final class NetworkStackConstants {
public static final int IPV4_MAX_MTU = 65_535;
/**
+ * Ethernet constants.
+ *
+ * See also:
+ * - https://tools.ietf.org/html/rfc894
+ * - https://tools.ietf.org/html/rfc2464
+ * - https://tools.ietf.org/html/rfc7042
+ * - http://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml
+ * - http://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml
+ */
+ public static final int ETHER_DST_ADDR_OFFSET = 0;
+ public static final int ETHER_SRC_ADDR_OFFSET = 6;
+ public static final int ETHER_ADDR_LEN = 6;
+ public static final int ETHER_TYPE_OFFSET = 12;
+ public static final int ETHER_TYPE_LENGTH = 2;
+ public static final int ETHER_TYPE_ARP = 0x0806;
+ public static final int ETHER_TYPE_IPV4 = 0x0800;
+ public static final int ETHER_TYPE_IPV6 = 0x86dd;
+ public static final int ETHER_HEADER_LEN = 14;
+
+ /**
+ * ARP constants.
+ *
+ * See also:
+ * - https://tools.ietf.org/html/rfc826
+ * - http://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml
+ */
+ public static final int ARP_PAYLOAD_LEN = 28; // For Ethernet+IPv4.
+ public static final int ARP_REQUEST = 1;
+ public static final int ARP_REPLY = 2;
+ public static final int ARP_HWTYPE_RESERVED_LO = 0;
+ public static final int ARP_HWTYPE_ETHER = 1;
+ public static final int ARP_HWTYPE_RESERVED_HI = 0xffff;
+
+ /**
+ * IPv4 constants.
+ *
+ * See also:
+ * - https://tools.ietf.org/html/rfc791
+ */
+ public static final int IPV4_HEADER_MIN_LEN = 20;
+ public static final int IPV4_IHL_MASK = 0xf;
+ public static final int IPV4_FLAGS_OFFSET = 6;
+ public static final int IPV4_FRAGMENT_MASK = 0x1fff;
+ public static final int IPV4_PROTOCOL_OFFSET = 9;
+ public static final int IPV4_SRC_ADDR_OFFSET = 12;
+ public static final int IPV4_DST_ADDR_OFFSET = 16;
+ public static final int IPV4_ADDR_LEN = 4;
+
+ /**
+ * IPv6 constants.
+ *
+ * See also:
+ * - https://tools.ietf.org/html/rfc2460
+ */
+ public static final int IPV6_ADDR_LEN = 16;
+ public static final int IPV6_HEADER_LEN = 40;
+ public static final int IPV6_PROTOCOL_OFFSET = 6;
+ public static final int IPV6_SRC_ADDR_OFFSET = 8;
+ public static final int IPV6_DST_ADDR_OFFSET = 24;
+
+ /**
+ * ICMPv6 constants.
+ *
+ * See also:
+ * - https://tools.ietf.org/html/rfc4443
+ * - https://tools.ietf.org/html/rfc4861
+ */
+ public static final int ICMPV6_HEADER_MIN_LEN = 4;
+ public static final int ICMPV6_ECHO_REPLY_TYPE = 129;
+ public static final int ICMPV6_ECHO_REQUEST_TYPE = 128;
+ public static final int ICMPV6_ROUTER_SOLICITATION = 133;
+ public static final int ICMPV6_ROUTER_ADVERTISEMENT = 134;
+ public static final int ICMPV6_NEIGHBOR_SOLICITATION = 135;
+ public static final int ICMPV6_NEIGHBOR_ADVERTISEMENT = 136;
+ public static final int ICMPV6_ND_OPTION_MIN_LENGTH = 8;
+ public static final int ICMPV6_ND_OPTION_LENGTH_SCALING_FACTOR = 8;
+ public static final int ICMPV6_ND_OPTION_SLLA = 1;
+ public static final int ICMPV6_ND_OPTION_TLLA = 2;
+ public static final int ICMPV6_ND_OPTION_MTU = 5;
+
+ /**
+ * UDP constants.
+ *
+ * See also:
+ * - https://tools.ietf.org/html/rfc768
+ */
+ public static final int UDP_HEADER_LEN = 8;
+
+
+ /**
* DHCP constants.
*
* See also:
* - https://tools.ietf.org/html/rfc2131
*/
public static final int INFINITE_LEASE = 0xffffffff;
+ public static final int DHCP4_CLIENT_PORT = 68;
private NetworkStackConstants() {
throw new UnsupportedOperationException("This class is not to be instantiated");