diff options
author | Remi NGUYEN VAN <reminv@google.com> | 2019-01-10 19:12:46 +0900 |
---|---|---|
committer | Remi NGUYEN VAN <reminv@google.com> | 2019-01-23 16:44:21 +0900 |
commit | 4a2eb87a8e16874938fa1ce7d39bbfa7b59b9890 (patch) | |
tree | 45acea97a40f77dce3a557b121efe06254d7858f /src | |
parent | 3fab1c749775db02c1409009ff374542bad929cb (diff) |
Move IpClient to NetworkStack
Test: atest FrameworksNetTests NetworkStackTests
Bug: b/112869080
Change-Id: I7d00848c052382cd1b6ce458868bed6a1e9e8ec5
Diffstat (limited to 'src')
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"); |