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 | |
parent | 3fab1c749775db02c1409009ff374542bad929cb (diff) |
Move IpClient to NetworkStack
Test: atest FrameworksNetTests NetworkStackTests
Bug: b/112869080
Change-Id: I7d00848c052382cd1b6ce458868bed6a1e9e8ec5
35 files changed, 13645 insertions, 3 deletions
@@ -24,7 +24,7 @@ java_library { ":services-networkstack-shared-srcs", ], static_libs: [ - "dhcp-packet-lib", + "services-netlink-lib", ] } diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 7f8bb93..5ab833b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -28,6 +28,7 @@ <!-- Launch captive portal app as specific user --> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> <uses-permission android:name="android.permission.NETWORK_STACK" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> <application android:label="NetworkStack" android:defaultToDeviceProtectedStorage="true" 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"); diff --git a/tests/Android.bp b/tests/Android.bp index bd7ff2a..45fa2dc 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -16,9 +16,12 @@ android_test { name: "NetworkStackTests", + certificate: "platform", srcs: ["src/**/*.java"], + resource_dirs: ["res"], static_libs: [ "android-support-test", + "frameworks-base-testutils", "mockito-target-extended-minus-junit4", "NetworkStackLib", "testables", @@ -26,10 +29,70 @@ android_test { libs: [ "android.test.runner", "android.test.base", + "android.test.mock", ], jni_libs: [ // For mockito extended "libdexmakerjvmtiagent", "libstaticjvmtiagent", - ] -}
\ No newline at end of file + // For ApfTest + "libartbase", + "libbacktrace", + "libbase", + "libbinder", + "libbinderthreadstate", + "libc++", + "libcrypto", + "libcutils", + "libdexfile", + "libhidl-gen-utils", + "libhidlbase", + "libhidltransport", + "libhwbinder", + "liblog", + "liblzma", + "libnativehelper", + "libnetworkstacktestsjni", + "libpackagelistparser", + "libpcre2", + "libprocessgroup", + "libselinux", + "libui", + "libutils", + "libvintf", + "libvndksupport", + "libtinyxml2", + "libunwindstack", + "libutilscallstack", + "libziparchive", + "libz", + "netd_aidl_interface-cpp", + ], +} + +cc_library_shared { + name: "libnetworkstacktestsjni", + srcs: [ + "jni/**/*.cpp" + ], + cflags: [ + "-Wall", + "-Wextra", + "-Werror", + ], + include_dirs: [ + "hardware/google/apf", + ], + shared_libs: [ + "libbinder", + "liblog", + "libcutils", + "libnativehelper", + "netd_aidl_interface-cpp", + ], + static_libs: [ + "libapf", + "libpcap", + ], + +} diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml index 8b8474f..9cb2c21 100644 --- a/tests/AndroidManifest.xml +++ b/tests/AndroidManifest.xml @@ -15,6 +15,35 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.server.networkstack.tests"> + + <uses-permission android:name="android.permission.READ_LOGS" /> + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + <uses-permission android:name="android.permission.READ_PHONE_STATE" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.BROADCAST_STICKY" /> + <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" /> + <uses-permission android:name="android.permission.MANAGE_APP_TOKENS" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> + <uses-permission android:name="android.permission.REAL_GET_TASKS" /> + <uses-permission android:name="android.permission.GET_DETAILED_TASKS" /> + <uses-permission android:name="android.permission.MANAGE_NETWORK_POLICY" /> + <uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" /> + <uses-permission android:name="android.permission.CONNECTIVITY_INTERNAL" /> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> + <uses-permission android:name="android.permission.MANAGE_USERS" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> + <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" /> + <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" /> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> + <uses-permission android:name="android.permission.PACKET_KEEPALIVE_OFFLOAD" /> + <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT" /> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" /> + <uses-permission android:name="android.permission.INSTALL_PACKAGES" /> + <uses-permission android:name="android.permission.NETWORK_STACK" /> + <application android:debuggable="true"> <uses-library android:name="android.test.runner" /> </application> diff --git a/tests/jni/apf_jni.cpp b/tests/jni/apf_jni.cpp new file mode 100644 index 0000000..4222adf --- /dev/null +++ b/tests/jni/apf_jni.cpp @@ -0,0 +1,248 @@ +/* + * Copyright 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. + */ + +#include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedUtfChars.h> +#include <jni.h> +#include <pcap.h> +#include <stdlib.h> +#include <string> +#include <utils/Log.h> +#include <vector> + +#include "apf_interpreter.h" +#include "nativehelper/scoped_primitive_array.h" + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) + +// JNI function acting as simply call-through to native APF interpreter. +static jint com_android_server_ApfTest_apfSimulate( + JNIEnv* env, jclass, jbyteArray jprogram, jbyteArray jpacket, + jbyteArray jdata, jint filter_age) { + + ScopedByteArrayRO packet(env, jpacket); + uint32_t packet_len = (uint32_t)packet.size(); + uint32_t program_len = env->GetArrayLength(jprogram); + uint32_t data_len = jdata ? env->GetArrayLength(jdata) : 0; + std::vector<uint8_t> buf(program_len + data_len, 0); + + env->GetByteArrayRegion(jprogram, 0, program_len, reinterpret_cast<jbyte*>(buf.data())); + if (jdata) { + // Merge program and data into a single buffer. + env->GetByteArrayRegion(jdata, 0, data_len, + reinterpret_cast<jbyte*>(buf.data() + program_len)); + } + + jint result = + accept_packet(buf.data(), program_len, program_len + data_len, + reinterpret_cast<const uint8_t*>(packet.get()), packet_len, filter_age); + + if (jdata) { + env->SetByteArrayRegion(jdata, 0, data_len, + reinterpret_cast<jbyte*>(buf.data() + program_len)); + } + + return result; +} + +class ScopedPcap { + public: + explicit ScopedPcap(pcap_t* pcap) : pcap_ptr(pcap) {} + ~ScopedPcap() { + pcap_close(pcap_ptr); + } + + pcap_t* get() const { return pcap_ptr; }; + private: + pcap_t* const pcap_ptr; +}; + +class ScopedFILE { + public: + explicit ScopedFILE(FILE* fp) : file(fp) {} + ~ScopedFILE() { + fclose(file); + } + + FILE* get() const { return file; }; + private: + FILE* const file; +}; + +static void throwException(JNIEnv* env, const std::string& error) { + jclass newExcCls = env->FindClass("java/lang/IllegalStateException"); + if (newExcCls == 0) { + abort(); + return; + } + env->ThrowNew(newExcCls, error.c_str()); +} + +static jstring com_android_server_ApfTest_compileToBpf(JNIEnv* env, jclass, jstring jfilter) { + ScopedUtfChars filter(env, jfilter); + std::string bpf_string; + ScopedPcap pcap(pcap_open_dead(DLT_EN10MB, 65535)); + if (pcap.get() == NULL) { + throwException(env, "pcap_open_dead failed"); + return NULL; + } + + // Compile "filter" to a BPF program + bpf_program bpf; + if (pcap_compile(pcap.get(), &bpf, filter.c_str(), 0, PCAP_NETMASK_UNKNOWN)) { + throwException(env, "pcap_compile failed"); + return NULL; + } + + // Translate BPF program to human-readable format + const struct bpf_insn* insn = bpf.bf_insns; + for (uint32_t i = 0; i < bpf.bf_len; i++) { + bpf_string += bpf_image(insn++, i); + bpf_string += "\n"; + } + + return env->NewStringUTF(bpf_string.c_str()); +} + +static jboolean com_android_server_ApfTest_compareBpfApf(JNIEnv* env, jclass, jstring jfilter, + jstring jpcap_filename, jbyteArray japf_program) { + ScopedUtfChars filter(env, jfilter); + ScopedUtfChars pcap_filename(env, jpcap_filename); + ScopedByteArrayRO apf_program(env, japf_program); + + // Open pcap file for BPF filtering + ScopedFILE bpf_fp(fopen(pcap_filename.c_str(), "rb")); + char pcap_error[PCAP_ERRBUF_SIZE]; + ScopedPcap bpf_pcap(pcap_fopen_offline(bpf_fp.get(), pcap_error)); + if (bpf_pcap.get() == NULL) { + throwException(env, "pcap_fopen_offline failed: " + std::string(pcap_error)); + return false; + } + + // Open pcap file for APF filtering + ScopedFILE apf_fp(fopen(pcap_filename.c_str(), "rb")); + ScopedPcap apf_pcap(pcap_fopen_offline(apf_fp.get(), pcap_error)); + if (apf_pcap.get() == NULL) { + throwException(env, "pcap_fopen_offline failed: " + std::string(pcap_error)); + return false; + } + + // Compile "filter" to a BPF program + bpf_program bpf; + if (pcap_compile(bpf_pcap.get(), &bpf, filter.c_str(), 0, PCAP_NETMASK_UNKNOWN)) { + throwException(env, "pcap_compile failed"); + return false; + } + + // Install BPF filter on bpf_pcap + if (pcap_setfilter(bpf_pcap.get(), &bpf)) { + throwException(env, "pcap_setfilter failed"); + return false; + } + + while (1) { + pcap_pkthdr bpf_header, apf_header; + // Run BPF filter to the next matching packet. + const uint8_t* bpf_packet = pcap_next(bpf_pcap.get(), &bpf_header); + + // Run APF filter to the next matching packet. + const uint8_t* apf_packet; + do { + apf_packet = pcap_next(apf_pcap.get(), &apf_header); + } while (apf_packet != NULL && !accept_packet( + reinterpret_cast<uint8_t*>(const_cast<int8_t*>(apf_program.get())), + apf_program.size(), 0 /* data_len */, + apf_packet, apf_header.len, 0 /* filter_age */)); + + // Make sure both filters matched the same packet. + if (apf_packet == NULL && bpf_packet == NULL) + break; + if (apf_packet == NULL || bpf_packet == NULL) + return false; + if (apf_header.len != bpf_header.len || + apf_header.ts.tv_sec != bpf_header.ts.tv_sec || + apf_header.ts.tv_usec != bpf_header.ts.tv_usec || + memcmp(apf_packet, bpf_packet, apf_header.len)) + return false; + } + return true; +} + +static jboolean com_android_server_ApfTest_dropsAllPackets(JNIEnv* env, jclass, jbyteArray jprogram, + jbyteArray jdata, jstring jpcap_filename) { + ScopedUtfChars pcap_filename(env, jpcap_filename); + ScopedByteArrayRO apf_program(env, jprogram); + uint32_t apf_program_len = (uint32_t)apf_program.size(); + uint32_t data_len = env->GetArrayLength(jdata); + pcap_pkthdr apf_header; + const uint8_t* apf_packet; + char pcap_error[PCAP_ERRBUF_SIZE]; + std::vector<uint8_t> buf(apf_program_len + data_len, 0); + + // Merge program and data into a single buffer. + env->GetByteArrayRegion(jprogram, 0, apf_program_len, reinterpret_cast<jbyte*>(buf.data())); + env->GetByteArrayRegion(jdata, 0, data_len, + reinterpret_cast<jbyte*>(buf.data() + apf_program_len)); + + // Open pcap file + ScopedFILE apf_fp(fopen(pcap_filename.c_str(), "rb")); + ScopedPcap apf_pcap(pcap_fopen_offline(apf_fp.get(), pcap_error)); + + if (apf_pcap.get() == NULL) { + throwException(env, "pcap_fopen_offline failed: " + std::string(pcap_error)); + return false; + } + + while ((apf_packet = pcap_next(apf_pcap.get(), &apf_header)) != NULL) { + int result = accept_packet(buf.data(), apf_program_len, + apf_program_len + data_len, apf_packet, apf_header.len, 0); + + // Return false once packet passes the filter + if (result) { + env->SetByteArrayRegion(jdata, 0, data_len, + reinterpret_cast<jbyte*>(buf.data() + apf_program_len)); + return false; + } + } + + env->SetByteArrayRegion(jdata, 0, data_len, + reinterpret_cast<jbyte*>(buf.data() + apf_program_len)); + return true; +} + +extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + ALOGE("ERROR: GetEnv failed"); + return -1; + } + + static JNINativeMethod gMethods[] = { + { "apfSimulate", "([B[B[BI)I", + (void*)com_android_server_ApfTest_apfSimulate }, + { "compileToBpf", "(Ljava/lang/String;)Ljava/lang/String;", + (void*)com_android_server_ApfTest_compileToBpf }, + { "compareBpfApf", "(Ljava/lang/String;Ljava/lang/String;[B)Z", + (void*)com_android_server_ApfTest_compareBpfApf }, + { "dropsAllPackets", "([B[BLjava/lang/String;)Z", + (void*)com_android_server_ApfTest_dropsAllPackets }, + }; + + jniRegisterNativeMethods(env, "android/net/apf/ApfTest", + gMethods, ARRAY_SIZE(gMethods)); + + return JNI_VERSION_1_6; +} diff --git a/tests/res/raw/apf.pcap b/tests/res/raw/apf.pcap Binary files differnew file mode 100644 index 0000000..963165f --- /dev/null +++ b/tests/res/raw/apf.pcap diff --git a/tests/res/raw/apfPcap.pcap b/tests/res/raw/apfPcap.pcap Binary files differnew file mode 100644 index 0000000..6f69c4a --- /dev/null +++ b/tests/res/raw/apfPcap.pcap diff --git a/tests/src/android/net/apf/ApfTest.java b/tests/src/android/net/apf/ApfTest.java new file mode 100644 index 0000000..f76e412 --- /dev/null +++ b/tests/src/android/net/apf/ApfTest.java @@ -0,0 +1,1802 @@ +/* + * Copyright (C) 2012 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.system.OsConstants.AF_UNIX; +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_STREAM; + +import static com.android.internal.util.BitUtils.bytesToBEInt; +import static com.android.server.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.apf.ApfFilter.ApfConfiguration; +import android.net.apf.ApfGenerator.IllegalInstructionException; +import android.net.apf.ApfGenerator.Register; +import android.net.ip.IIpClientCallbacks; +import android.net.ip.IpClient; +import android.net.ip.IpClient.IpClientCallbacksWrapper; +import android.net.ip.IpClientCallbacks; +import android.net.metrics.IpConnectivityLog; +import android.net.metrics.RaEvent; +import android.net.util.InterfaceParams; +import android.net.util.SharedLog; +import android.os.ConditionVariable; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.system.ErrnoException; +import android.system.Os; +import android.text.format.DateUtils; +import android.util.Log; + +import com.android.internal.util.HexDump; +import com.android.server.networkstack.tests.R; +import com.android.server.util.NetworkStackConstants; + +import libcore.io.IoUtils; +import libcore.io.Streams; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Random; + +/** + * Tests for APF program generator and interpreter. + * + * Build, install and run with: + * runtest frameworks-net -c android.net.apf.ApfTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ApfTest { + private static final int TIMEOUT_MS = 500; + private static final int MIN_APF_VERSION = 2; + + @Mock IpConnectivityLog mLog; + @Mock Context mContext; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + // Load up native shared library containing APF interpreter exposed via JNI. + System.loadLibrary("networkstacktestsjni"); + } + + private static final String TAG = "ApfTest"; + // Expected return codes from APF interpreter. + private static final int PASS = 1; + private static final int DROP = 0; + // Interpreter will just accept packets without link layer headers, so pad fake packet to at + // least the minimum packet size. + private static final int MIN_PKT_SIZE = 15; + + private static final ApfCapabilities MOCK_APF_CAPABILITIES = + new ApfCapabilities(2, 1700, ARPHRD_ETHER); + + private static final boolean DROP_MULTICAST = true; + private static final boolean ALLOW_MULTICAST = false; + + private static final boolean DROP_802_3_FRAMES = true; + private static final boolean ALLOW_802_3_FRAMES = false; + + // Constants for opcode encoding + private static final byte LI_OP = (byte)(13 << 3); + private static final byte LDDW_OP = (byte)(22 << 3); + private static final byte STDW_OP = (byte)(23 << 3); + private static final byte SIZE0 = (byte)(0 << 1); + private static final byte SIZE8 = (byte)(1 << 1); + private static final byte SIZE16 = (byte)(2 << 1); + private static final byte SIZE32 = (byte)(3 << 1); + private static final byte R1 = 1; + + private static ApfConfiguration getDefaultConfig() { + ApfFilter.ApfConfiguration config = new ApfConfiguration(); + config.apfCapabilities = MOCK_APF_CAPABILITIES; + config.multicastFilter = ALLOW_MULTICAST; + config.ieee802_3Filter = ALLOW_802_3_FRAMES; + config.ethTypeBlackList = new int[0]; + return config; + } + + private static String label(int code) { + switch (code) { + case PASS: return "PASS"; + case DROP: return "DROP"; + default: return "UNKNOWN"; + } + } + + private static void assertReturnCodesEqual(int expected, int got) { + assertEquals(label(expected), label(got)); + } + + private void assertVerdict(int expected, byte[] program, byte[] packet, int filterAge) { + assertReturnCodesEqual(expected, apfSimulate(program, packet, null, filterAge)); + } + + private void assertVerdict(int expected, byte[] program, byte[] packet) { + assertReturnCodesEqual(expected, apfSimulate(program, packet, null, 0)); + } + + private void assertPass(byte[] program, byte[] packet, int filterAge) { + assertVerdict(PASS, program, packet, filterAge); + } + + private void assertPass(byte[] program, byte[] packet) { + assertVerdict(PASS, program, packet); + } + + private void assertDrop(byte[] program, byte[] packet, int filterAge) { + assertVerdict(DROP, program, packet, filterAge); + } + + private void assertDrop(byte[] program, byte[] packet) { + assertVerdict(DROP, program, packet); + } + + private void assertProgramEquals(byte[] expected, byte[] program) throws AssertionError { + // assertArrayEquals() would only print one byte, making debugging difficult. + if (!java.util.Arrays.equals(expected, program)) { + throw new AssertionError( + "\nexpected: " + HexDump.toHexString(expected) + + "\nactual: " + HexDump.toHexString(program)); + } + } + + private void assertDataMemoryContents( + int expected, byte[] program, byte[] packet, byte[] data, byte[] expected_data) + throws IllegalInstructionException, Exception { + assertReturnCodesEqual(expected, apfSimulate(program, packet, data, 0 /* filterAge */)); + + // assertArrayEquals() would only print one byte, making debugging difficult. + if (!java.util.Arrays.equals(expected_data, data)) { + throw new Exception( + "\nprogram: " + HexDump.toHexString(program) + + "\ndata memory: " + HexDump.toHexString(data) + + "\nexpected: " + HexDump.toHexString(expected_data)); + } + } + + private void assertVerdict(int expected, ApfGenerator gen, byte[] packet, int filterAge) + throws IllegalInstructionException { + assertReturnCodesEqual(expected, apfSimulate(gen.generate(), packet, null, + filterAge)); + } + + private void assertPass(ApfGenerator gen, byte[] packet, int filterAge) + throws IllegalInstructionException { + assertVerdict(PASS, gen, packet, filterAge); + } + + private void assertDrop(ApfGenerator gen, byte[] packet, int filterAge) + throws IllegalInstructionException { + assertVerdict(DROP, gen, packet, filterAge); + } + + private void assertPass(ApfGenerator gen) + throws IllegalInstructionException { + assertVerdict(PASS, gen, new byte[MIN_PKT_SIZE], 0); + } + + private void assertDrop(ApfGenerator gen) + throws IllegalInstructionException { + assertVerdict(DROP, gen, new byte[MIN_PKT_SIZE], 0); + } + + /** + * Test each instruction by generating a program containing the instruction, + * generating bytecode for that program and running it through the + * interpreter to verify it functions correctly. + */ + @Test + public void testApfInstructions() throws IllegalInstructionException { + // Empty program should pass because having the program counter reach the + // location immediately after the program indicates the packet should be + // passed to the AP. + ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION); + assertPass(gen); + + // Test jumping to pass label. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJump(gen.PASS_LABEL); + byte[] program = gen.generate(); + assertEquals(1, program.length); + assertEquals((14 << 3) | (0 << 1) | 0, program[0]); + assertPass(program, new byte[MIN_PKT_SIZE], 0); + + // Test jumping to drop label. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJump(gen.DROP_LABEL); + program = gen.generate(); + assertEquals(2, program.length); + assertEquals((14 << 3) | (1 << 1) | 0, program[0]); + assertEquals(1, program[1]); + assertDrop(program, new byte[15], 15); + + // Test jumping if equal to 0. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJumpIfR0Equals(0, gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if not equal to 0. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJumpIfR0NotEquals(0, gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfR0NotEquals(0, gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if registers equal. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJumpIfR0EqualsR1(gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if registers not equal. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJumpIfR0NotEqualsR1(gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfR0NotEqualsR1(gen.DROP_LABEL); + assertDrop(gen); + + // Test load immediate. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test add. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addAdd(1234567890); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test subtract. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addAdd(-1234567890); + gen.addJumpIfR0Equals(-1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test or. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addOr(1234567890); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test and. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addAnd(123456789); + gen.addJumpIfR0Equals(1234567890 & 123456789, gen.DROP_LABEL); + assertDrop(gen); + + // Test left shift. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addLeftShift(1); + gen.addJumpIfR0Equals(1234567890 << 1, gen.DROP_LABEL); + assertDrop(gen); + + // Test right shift. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addRightShift(1); + gen.addJumpIfR0Equals(1234567890 >> 1, gen.DROP_LABEL); + assertDrop(gen); + + // Test multiply. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 123456789); + gen.addMul(2); + gen.addJumpIfR0Equals(123456789 * 2, gen.DROP_LABEL); + assertDrop(gen); + + // Test divide. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addDiv(2); + gen.addJumpIfR0Equals(1234567890 / 2, gen.DROP_LABEL); + assertDrop(gen); + + // Test divide by zero. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addDiv(0); + gen.addJump(gen.DROP_LABEL); + assertPass(gen); + + // Test add. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 1234567890); + gen.addAddR1(); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test subtract. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, -1234567890); + gen.addAddR1(); + gen.addJumpIfR0Equals(-1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test or. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 1234567890); + gen.addOrR1(); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test and. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addLoadImmediate(Register.R1, 123456789); + gen.addAndR1(); + gen.addJumpIfR0Equals(1234567890 & 123456789, gen.DROP_LABEL); + assertDrop(gen); + + // Test left shift. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addLoadImmediate(Register.R1, 1); + gen.addLeftShiftR1(); + gen.addJumpIfR0Equals(1234567890 << 1, gen.DROP_LABEL); + assertDrop(gen); + + // Test right shift. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addLoadImmediate(Register.R1, -1); + gen.addLeftShiftR1(); + gen.addJumpIfR0Equals(1234567890 >> 1, gen.DROP_LABEL); + assertDrop(gen); + + // Test multiply. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 123456789); + gen.addLoadImmediate(Register.R1, 2); + gen.addMulR1(); + gen.addJumpIfR0Equals(123456789 * 2, gen.DROP_LABEL); + assertDrop(gen); + + // Test divide. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addLoadImmediate(Register.R1, 2); + gen.addDivR1(); + gen.addJumpIfR0Equals(1234567890 / 2, gen.DROP_LABEL); + assertDrop(gen); + + // Test divide by zero. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addDivR1(); + gen.addJump(gen.DROP_LABEL); + assertPass(gen); + + // Test byte load. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoad8(Register.R0, 1); + gen.addJumpIfR0Equals(45, gen.DROP_LABEL); + assertDrop(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test out of bounds load. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoad8(Register.R0, 16); + gen.addJumpIfR0Equals(0, gen.DROP_LABEL); + assertPass(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test half-word load. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoad16(Register.R0, 1); + gen.addJumpIfR0Equals((45 << 8) | 67, gen.DROP_LABEL); + assertDrop(gen, new byte[]{123,45,67,0,0,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test word load. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoad32(Register.R0, 1); + gen.addJumpIfR0Equals((45 << 24) | (67 << 16) | (89 << 8) | 12, gen.DROP_LABEL); + assertDrop(gen, new byte[]{123,45,67,89,12,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test byte indexed load. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 1); + gen.addLoad8Indexed(Register.R0, 0); + gen.addJumpIfR0Equals(45, gen.DROP_LABEL); + assertDrop(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test out of bounds indexed load. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 8); + gen.addLoad8Indexed(Register.R0, 8); + gen.addJumpIfR0Equals(0, gen.DROP_LABEL); + assertPass(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test half-word indexed load. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 1); + gen.addLoad16Indexed(Register.R0, 0); + gen.addJumpIfR0Equals((45 << 8) | 67, gen.DROP_LABEL); + assertDrop(gen, new byte[]{123,45,67,0,0,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test word indexed load. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 1); + gen.addLoad32Indexed(Register.R0, 0); + gen.addJumpIfR0Equals((45 << 24) | (67 << 16) | (89 << 8) | 12, gen.DROP_LABEL); + assertDrop(gen, new byte[]{123,45,67,89,12,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test jumping if greater than. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJumpIfR0GreaterThan(0, gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfR0GreaterThan(0, gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if less than. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJumpIfR0LessThan(0, gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJumpIfR0LessThan(1, gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if any bits set. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJumpIfR0AnyBitsSet(3, gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfR0AnyBitsSet(3, gen.DROP_LABEL); + assertDrop(gen); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 3); + gen.addJumpIfR0AnyBitsSet(3, gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if register greater than. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJumpIfR0GreaterThanR1(gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 2); + gen.addLoadImmediate(Register.R1, 1); + gen.addJumpIfR0GreaterThanR1(gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if register less than. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJumpIfR0LessThanR1(gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 1); + gen.addJumpIfR0LessThanR1(gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if any bits set in register. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 3); + gen.addJumpIfR0AnyBitsSetR1(gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 3); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfR0AnyBitsSetR1(gen.DROP_LABEL); + assertDrop(gen); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 3); + gen.addLoadImmediate(Register.R0, 3); + gen.addJumpIfR0AnyBitsSetR1(gen.DROP_LABEL); + assertDrop(gen); + + // Test load from memory. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadFromMemory(Register.R0, 0); + gen.addJumpIfR0Equals(0, gen.DROP_LABEL); + assertDrop(gen); + + // Test store to memory. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 1234567890); + gen.addStoreToMemory(Register.R1, 12); + gen.addLoadFromMemory(Register.R0, 12); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test filter age pre-filled memory. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadFromMemory(Register.R0, gen.FILTER_AGE_MEMORY_SLOT); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen, new byte[MIN_PKT_SIZE], 1234567890); + + // Test packet size pre-filled memory. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT); + gen.addJumpIfR0Equals(MIN_PKT_SIZE, gen.DROP_LABEL); + assertDrop(gen); + + // Test IPv4 header size pre-filled memory. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); + gen.addJumpIfR0Equals(20, gen.DROP_LABEL); + assertDrop(gen, new byte[]{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x45}, 0); + + // Test not. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addNot(Register.R0); + gen.addJumpIfR0Equals(~1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test negate. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addNeg(Register.R0); + gen.addJumpIfR0Equals(-1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test move. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 1234567890); + gen.addMove(Register.R0); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addMove(Register.R1); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test swap. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R1, 1234567890); + gen.addSwap(); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addSwap(); + gen.addJumpIfR0Equals(0, gen.DROP_LABEL); + assertDrop(gen); + + // Test jump if bytes not equal. + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{123}, gen.DROP_LABEL); + program = gen.generate(); + assertEquals(6, program.length); + assertEquals((13 << 3) | (1 << 1) | 0, program[0]); + assertEquals(1, program[1]); + assertEquals(((20 << 3) | (1 << 1) | 0) - 256, program[2]); + assertEquals(1, program[3]); + assertEquals(1, program[4]); + assertEquals(123, program[5]); + assertDrop(program, new byte[MIN_PKT_SIZE], 0); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{123}, gen.DROP_LABEL); + byte[] packet123 = {0,123,0,0,0,0,0,0,0,0,0,0,0,0,0}; + assertPass(gen, packet123, 0); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{123}, gen.DROP_LABEL); + assertDrop(gen, packet123, 0); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{1,2,30,4,5}, gen.DROP_LABEL); + byte[] packet12345 = {0,1,2,3,4,5,0,0,0,0,0,0,0,0,0}; + assertDrop(gen, packet12345, 0); + gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{1,2,3,4,5}, gen.DROP_LABEL); + assertPass(gen, packet12345, 0); + } + + @Test(expected = ApfGenerator.IllegalInstructionException.class) + public void testApfGeneratorWantsV2OrGreater() throws Exception { + // The minimum supported APF version is 2. + new ApfGenerator(1); + } + + @Test + public void testApfDataOpcodesWantApfV3() throws IllegalInstructionException, Exception { + ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION); + try { + gen.addStoreData(Register.R0, 0); + fail(); + } catch (IllegalInstructionException expected) { + /* pass */ + } + try { + gen.addLoadData(Register.R0, 0); + fail(); + } catch (IllegalInstructionException expected) { + /* pass */ + } + } + + /** + * Test that the generator emits immediates using the shortest possible encoding. + */ + @Test + public void testImmediateEncoding() throws IllegalInstructionException { + ApfGenerator gen; + + // 0-byte immediate: li R0, 0 + gen = new ApfGenerator(4); + gen.addLoadImmediate(Register.R0, 0); + assertProgramEquals(new byte[]{LI_OP | SIZE0}, gen.generate()); + + // 1-byte immediate: li R0, 42 + gen = new ApfGenerator(4); + gen.addLoadImmediate(Register.R0, 42); + assertProgramEquals(new byte[]{LI_OP | SIZE8, 42}, gen.generate()); + + // 2-byte immediate: li R1, 0x1234 + gen = new ApfGenerator(4); + gen.addLoadImmediate(Register.R1, 0x1234); + assertProgramEquals(new byte[]{LI_OP | SIZE16 | R1, 0x12, 0x34}, gen.generate()); + + // 4-byte immediate: li R0, 0x12345678 + gen = new ApfGenerator(3); + gen.addLoadImmediate(Register.R0, 0x12345678); + assertProgramEquals( + new byte[]{LI_OP | SIZE32, 0x12, 0x34, 0x56, 0x78}, + gen.generate()); + } + + /** + * Test that the generator emits negative immediates using the shortest possible encoding. + */ + @Test + public void testNegativeImmediateEncoding() throws IllegalInstructionException { + ApfGenerator gen; + + // 1-byte negative immediate: li R0, -42 + gen = new ApfGenerator(3); + gen.addLoadImmediate(Register.R0, -42); + assertProgramEquals(new byte[]{LI_OP | SIZE8, -42}, gen.generate()); + + // 2-byte negative immediate: li R1, -0x1122 + gen = new ApfGenerator(3); + gen.addLoadImmediate(Register.R1, -0x1122); + assertProgramEquals(new byte[]{LI_OP | SIZE16 | R1, (byte)0xEE, (byte)0xDE}, + gen.generate()); + + // 4-byte negative immediate: li R0, -0x11223344 + gen = new ApfGenerator(3); + gen.addLoadImmediate(Register.R0, -0x11223344); + assertProgramEquals( + new byte[]{LI_OP | SIZE32, (byte)0xEE, (byte)0xDD, (byte)0xCC, (byte)0xBC}, + gen.generate()); + } + + /** + * Test that the generator correctly emits positive and negative immediates for LDDW/STDW. + */ + @Test + public void testLoadStoreDataEncoding() throws IllegalInstructionException { + ApfGenerator gen; + + // Load data with no offset: lddw R0, [0 + r1] + gen = new ApfGenerator(3); + gen.addLoadData(Register.R0, 0); + assertProgramEquals(new byte[]{LDDW_OP | SIZE0}, gen.generate()); + + // Store data with 8bit negative offset: lddw r0, [-42 + r1] + gen = new ApfGenerator(3); + gen.addStoreData(Register.R0, -42); + assertProgramEquals(new byte[]{STDW_OP | SIZE8, -42}, gen.generate()); + + // Store data to R1 with 16bit negative offset: stdw r1, [-0x1122 + r0] + gen = new ApfGenerator(3); + gen.addStoreData(Register.R1, -0x1122); + assertProgramEquals(new byte[]{STDW_OP | SIZE16 | R1, (byte)0xEE, (byte)0xDE}, + gen.generate()); + + // Load data to R1 with 32bit negative offset: lddw r1, [0xDEADBEEF + r0] + gen = new ApfGenerator(3); + gen.addLoadData(Register.R1, 0xDEADBEEF); + assertProgramEquals( + new byte[]{LDDW_OP | SIZE32 | R1, (byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF}, + gen.generate()); + } + + /** + * Test that the interpreter correctly executes STDW with a negative 8bit offset + */ + @Test + public void testApfDataWrite() throws IllegalInstructionException, Exception { + byte[] packet = new byte[MIN_PKT_SIZE]; + byte[] data = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}; + byte[] expected_data = data.clone(); + + // No memory access instructions: should leave the data segment untouched. + ApfGenerator gen = new ApfGenerator(3); + assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data); + + // Expect value 0x87654321 to be stored starting from address -11 from the end of the + // data buffer, in big-endian order. + gen = new ApfGenerator(3); + gen.addLoadImmediate(Register.R0, 0x87654321); + gen.addLoadImmediate(Register.R1, -5); + gen.addStoreData(Register.R0, -6); // -5 + -6 = -11 (offset +5 with data_len=16) + expected_data[5] = (byte)0x87; + expected_data[6] = (byte)0x65; + expected_data[7] = (byte)0x43; + expected_data[8] = (byte)0x21; + assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data); + } + + /** + * Test that the interpreter correctly executes LDDW with a negative 16bit offset + */ + @Test + public void testApfDataRead() throws IllegalInstructionException, Exception { + // Program that DROPs if address 10 (-6) contains 0x87654321. + ApfGenerator gen = new ApfGenerator(3); + gen.addLoadImmediate(Register.R1, 1000); + gen.addLoadData(Register.R0, -1006); // 1000 + -1006 = -6 (offset +10 with data_len=16) + gen.addJumpIfR0Equals(0x87654321, gen.DROP_LABEL); + byte[] program = gen.generate(); + byte[] packet = new byte[MIN_PKT_SIZE]; + + // Content is incorrect (last byte does not match) -> PASS + byte[] data = new byte[16]; + data[10] = (byte)0x87; + data[11] = (byte)0x65; + data[12] = (byte)0x43; + data[13] = (byte)0x00; // != 0x21 + byte[] expected_data = data.clone(); + assertDataMemoryContents(PASS, program, packet, data, expected_data); + + // Fix the last byte -> conditional jump taken -> DROP + data[13] = (byte)0x21; + expected_data = data; + assertDataMemoryContents(DROP, program, packet, data, expected_data); + } + + /** + * Test that the interpreter correctly executes LDDW followed by a STDW. + * To cover a few more edge cases, LDDW has a 0bit offset, while STDW has a positive 8bit + * offset. + */ + @Test + public void testApfDataReadModifyWrite() throws IllegalInstructionException, Exception { + ApfGenerator gen = new ApfGenerator(3); + gen.addLoadImmediate(Register.R1, -22); + gen.addLoadData(Register.R0, 0); // Load from address 32 -22 + 0 = 10 + gen.addAdd(0x78453412); // 87654321 + 78453412 = FFAA7733 + gen.addStoreData(Register.R0, 4); // Write back to address 32 -22 + 4 = 14 + + byte[] packet = new byte[MIN_PKT_SIZE]; + byte[] data = new byte[32]; + data[10] = (byte)0x87; + data[11] = (byte)0x65; + data[12] = (byte)0x43; + data[13] = (byte)0x21; + byte[] expected_data = data.clone(); + expected_data[14] = (byte)0xFF; + expected_data[15] = (byte)0xAA; + expected_data[16] = (byte)0x77; + expected_data[17] = (byte)0x33; + assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data); + } + + @Test + public void testApfDataBoundChecking() throws IllegalInstructionException, Exception { + byte[] packet = new byte[MIN_PKT_SIZE]; + byte[] data = new byte[32]; + byte[] expected_data = data; + + // Program that DROPs unconditionally. This is our the baseline. + ApfGenerator gen = new ApfGenerator(3); + gen.addLoadImmediate(Register.R0, 3); + gen.addLoadData(Register.R1, 7); + gen.addJump(gen.DROP_LABEL); + assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data); + + // Same program as before, but this time we're trying to load past the end of the data. + gen = new ApfGenerator(3); + gen.addLoadImmediate(Register.R0, 20); + gen.addLoadData(Register.R1, 15); // 20 + 15 > 32 + gen.addJump(gen.DROP_LABEL); // Not reached. + assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data); + + // Subtracting an immediate should work... + gen = new ApfGenerator(3); + gen.addLoadImmediate(Register.R0, 20); + gen.addLoadData(Register.R1, -4); + gen.addJump(gen.DROP_LABEL); + assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data); + + // ...and underflowing simply wraps around to the end of the buffer... + gen = new ApfGenerator(3); + gen.addLoadImmediate(Register.R0, 20); + gen.addLoadData(Register.R1, -30); + gen.addJump(gen.DROP_LABEL); + assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data); + + // ...but doesn't allow accesses before the start of the buffer + gen = new ApfGenerator(3); + gen.addLoadImmediate(Register.R0, 20); + gen.addLoadData(Register.R1, -1000); + gen.addJump(gen.DROP_LABEL); // Not reached. + assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data); + } + + /** + * Generate some BPF programs, translate them to APF, then run APF and BPF programs + * over packet traces and verify both programs filter out the same packets. + */ + @Test + public void testApfAgainstBpf() throws Exception { + String[] tcpdump_filters = new String[]{ "udp", "tcp", "icmp", "icmp6", "udp port 53", + "arp", "dst 239.255.255.250", "arp or tcp or udp port 53", "net 192.168.1.0/24", + "arp or icmp6 or portrange 53-54", "portrange 53-54 or portrange 100-50000", + "tcp[tcpflags] & (tcp-ack|tcp-fin) != 0 and (ip[2:2] > 57 or icmp)" }; + String pcap_filename = stageFile(R.raw.apf); + for (String tcpdump_filter : tcpdump_filters) { + byte[] apf_program = Bpf2Apf.convert(compileToBpf(tcpdump_filter)); + assertTrue("Failed to match for filter: " + tcpdump_filter, + compareBpfApf(tcpdump_filter, pcap_filename, apf_program)); + } + } + + /** + * Generate APF program, run pcap file though APF filter, then check all the packets in the file + * should be dropped. + */ + @Test + public void testApfFilterPcapFile() throws Exception { + final byte[] MOCK_PCAP_IPV4_ADDR = {(byte) 172, 16, 7, (byte) 151}; + String pcapFilename = stageFile(R.raw.apfPcap); + MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_PCAP_IPV4_ADDR), 16); + LinkProperties lp = new LinkProperties(); + lp.addLinkAddress(link); + + ApfConfiguration config = getDefaultConfig(); + ApfCapabilities MOCK_APF_PCAP_CAPABILITIES = new ApfCapabilities(4, 1700, ARPHRD_ETHER); + config.apfCapabilities = MOCK_APF_PCAP_CAPABILITIES; + config.multicastFilter = DROP_MULTICAST; + config.ieee802_3Filter = DROP_802_3_FRAMES; + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + apfFilter.setLinkProperties(lp); + byte[] program = ipClientCallback.getApfProgram(); + byte[] data = new byte[ApfFilter.Counter.totalSize()]; + final boolean result; + + result = dropsAllPackets(program, data, pcapFilename); + Log.i(TAG, "testApfFilterPcapFile(): Data counters: " + HexDump.toHexString(data, false)); + + assertTrue("Failed to drop all packets by filter. \nAPF counters:" + + HexDump.toHexString(data, false), result); + } + + private class MockIpClientCallback extends IpClientCallbacksWrapper { + private final ConditionVariable mGotApfProgram = new ConditionVariable(); + private byte[] mLastApfProgram; + + MockIpClientCallback() { + super(mock(IIpClientCallbacks.class), mock(SharedLog.class)); + } + + @Override + public void installPacketFilter(byte[] filter) { + mLastApfProgram = filter; + mGotApfProgram.open(); + } + + public void resetApfProgramWait() { + mGotApfProgram.close(); + } + + public byte[] getApfProgram() { + assertTrue(mGotApfProgram.block(TIMEOUT_MS)); + return mLastApfProgram; + } + + public void assertNoProgramUpdate() { + assertFalse(mGotApfProgram.block(TIMEOUT_MS)); + } + } + + private static class TestApfFilter extends ApfFilter { + public static final byte[] MOCK_MAC_ADDR = {1,2,3,4,5,6}; + + private FileDescriptor mWriteSocket; + private final long mFixedTimeMs = SystemClock.elapsedRealtime(); + + public TestApfFilter(Context context, ApfConfiguration config, + IpClientCallbacksWrapper ipClientCallback, IpConnectivityLog log) throws Exception { + super(context, config, InterfaceParams.getByName("lo"), ipClientCallback, log); + } + + // Pretend an RA packet has been received and show it to ApfFilter. + public void pretendPacketReceived(byte[] packet) throws IOException, ErrnoException { + // ApfFilter's ReceiveThread will be waiting to read this. + Os.write(mWriteSocket, packet, 0, packet.length); + } + + @Override + protected long currentTimeSeconds() { + return mFixedTimeMs / DateUtils.SECOND_IN_MILLIS; + } + + @Override + void maybeStartFilter() { + mHardwareAddress = MOCK_MAC_ADDR; + installNewProgramLocked(); + + // Create two sockets, "readSocket" and "mWriteSocket" and connect them together. + FileDescriptor readSocket = new FileDescriptor(); + mWriteSocket = new FileDescriptor(); + try { + Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mWriteSocket, readSocket); + } catch (ErrnoException e) { + fail(); + return; + } + // Now pass readSocket to ReceiveThread as if it was setup to read raw RAs. + // This allows us to pretend RA packets have been recieved via pretendPacketReceived(). + mReceiveThread = new ReceiveThread(readSocket); + mReceiveThread.start(); + } + + @Override + public void shutdown() { + super.shutdown(); + IoUtils.closeQuietly(mWriteSocket); + } + } + + 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 byte[] ETH_BROADCAST_MAC_ADDRESS = + {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff }; + + private static final int IPV4_VERSION_IHL_OFFSET = ETH_HEADER_LEN + 0; + 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 byte[] IPV4_BROADCAST_ADDRESS = + {(byte) 255, (byte) 255, (byte) 255, (byte) 255}; + + private static final int IPV6_NEXT_HEADER_OFFSET = ETH_HEADER_LEN + 6; + private static final int IPV6_HEADER_LEN = 40; + private static final int IPV6_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 24; + // 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 byte[] IPV6_ALL_ROUTERS_ADDRESS = + { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 }; + + private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN; + private static final int ICMP6_ROUTER_SOLICITATION = 133; + private static final int ICMP6_ROUTER_ADVERTISEMENT = 134; + private static final int ICMP6_NEIGHBOR_SOLICITATION = 135; + private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136; + + private static final int ICMP6_RA_HEADER_LEN = 16; + private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET = + ETH_HEADER_LEN + IPV6_HEADER_LEN + 6; + private static final int ICMP6_RA_CHECKSUM_OFFSET = + ETH_HEADER_LEN + IPV6_HEADER_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_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_PREFERRED_LIFETIME_OFFSET = 8; + + // 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_OPTION_LEN = 8; + private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4; + private static final int ICMP6_4_BYTE_LIFETIME_LEN = 4; + + private static final int UDP_HEADER_LEN = 8; + private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 22; + + private static final int DHCP_CLIENT_PORT = 68; + private static final int DHCP_CLIENT_MAC_OFFSET = ETH_HEADER_LEN + UDP_HEADER_LEN + 48; + + private static final int ARP_HEADER_OFFSET = ETH_HEADER_LEN; + private static final byte[] ARP_IPV4_REQUEST_HEADER = { + 0, 1, // Hardware type: Ethernet (1) + 8, 0, // Protocol type: IP (0x0800) + 6, // Hardware size: 6 + 4, // Protocol size: 4 + 0, 1 // Opcode: request (1) + }; + private static final byte[] ARP_IPV4_REPLY_HEADER = { + 0, 1, // Hardware type: Ethernet (1) + 8, 0, // Protocol type: IP (0x0800) + 6, // Hardware size: 6 + 4, // Protocol size: 4 + 0, 2 // 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; + + private static final byte[] MOCK_IPV4_ADDR = {10, 0, 0, 1}; + private static final byte[] MOCK_BROADCAST_IPV4_ADDR = {10, 0, 31, (byte) 255}; // prefix = 19 + private static final byte[] MOCK_MULTICAST_IPV4_ADDR = {(byte) 224, 0, 0, 1}; + private static final byte[] ANOTHER_IPV4_ADDR = {10, 0, 0, 2}; + private static final byte[] IPV4_SOURCE_ADDR = {10, 0, 0, 3}; + private static final byte[] ANOTHER_IPV4_SOURCE_ADDR = {(byte) 192, 0, 2, 1}; + private static final byte[] BUG_PROBE_SOURCE_ADDR1 = {0, 0, 1, 2}; + private static final byte[] BUG_PROBE_SOURCE_ADDR2 = {3, 4, 0, 0}; + private static final byte[] IPV4_ANY_HOST_ADDR = {0, 0, 0, 0}; + + // Helper to initialize a default apfFilter. + private ApfFilter setupApfFilter( + IpClientCallbacksWrapper ipClientCallback, ApfConfiguration config) throws Exception { + LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 19); + LinkProperties lp = new LinkProperties(); + lp.addLinkAddress(link); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + apfFilter.setLinkProperties(lp); + return apfFilter; + } + + @Test + public void testApfFilterIPv4() throws Exception { + MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 19); + LinkProperties lp = new LinkProperties(); + lp.addLinkAddress(link); + + ApfConfiguration config = getDefaultConfig(); + config.multicastFilter = DROP_MULTICAST; + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + apfFilter.setLinkProperties(lp); + + byte[] program = ipClientCallback.getApfProgram(); + + // Verify empty packet of 100 zero bytes is passed + ByteBuffer packet = ByteBuffer.wrap(new byte[100]); + assertPass(program, packet.array()); + + // Verify unicast IPv4 packet is passed + put(packet, ETH_DEST_ADDR_OFFSET, TestApfFilter.MOCK_MAC_ADDR); + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP); + put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_IPV4_ADDR); + assertPass(program, packet.array()); + + // Verify L2 unicast to IPv4 broadcast addresses is dropped (b/30231088) + put(packet, IPV4_DEST_ADDR_OFFSET, IPV4_BROADCAST_ADDRESS); + assertDrop(program, packet.array()); + put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_BROADCAST_IPV4_ADDR); + assertDrop(program, packet.array()); + + // Verify multicast/broadcast IPv4, not DHCP to us, is dropped + put(packet, ETH_DEST_ADDR_OFFSET, ETH_BROADCAST_MAC_ADDRESS); + assertDrop(program, packet.array()); + packet.put(IPV4_VERSION_IHL_OFFSET, (byte)0x45); + assertDrop(program, packet.array()); + packet.put(IPV4_PROTOCOL_OFFSET, (byte)IPPROTO_UDP); + assertDrop(program, packet.array()); + packet.putShort(UDP_DESTINATION_PORT_OFFSET, (short)DHCP_CLIENT_PORT); + assertDrop(program, packet.array()); + put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_MULTICAST_IPV4_ADDR); + assertDrop(program, packet.array()); + put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_BROADCAST_IPV4_ADDR); + assertDrop(program, packet.array()); + put(packet, IPV4_DEST_ADDR_OFFSET, IPV4_BROADCAST_ADDRESS); + assertDrop(program, packet.array()); + + // Verify broadcast IPv4 DHCP to us is passed + put(packet, DHCP_CLIENT_MAC_OFFSET, TestApfFilter.MOCK_MAC_ADDR); + assertPass(program, packet.array()); + + // Verify unicast IPv4 DHCP to us is passed + put(packet, ETH_DEST_ADDR_OFFSET, TestApfFilter.MOCK_MAC_ADDR); + assertPass(program, packet.array()); + + apfFilter.shutdown(); + } + + @Test + public void testApfFilterIPv6() throws Exception { + MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + ApfConfiguration config = getDefaultConfig(); + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + byte[] program = ipClientCallback.getApfProgram(); + + // Verify empty IPv6 packet is passed + ByteBuffer packet = ByteBuffer.wrap(new byte[100]); + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6); + assertPass(program, packet.array()); + + // Verify empty ICMPv6 packet is passed + packet.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_ICMPV6); + assertPass(program, packet.array()); + + // Verify empty ICMPv6 NA packet is passed + packet.put(ICMP6_TYPE_OFFSET, (byte)ICMP6_NEIGHBOR_ANNOUNCEMENT); + assertPass(program, packet.array()); + + // Verify ICMPv6 NA to ff02::1 is dropped + put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_NODES_ADDRESS); + assertDrop(program, packet.array()); + + // Verify ICMPv6 RS to any is dropped + packet.put(ICMP6_TYPE_OFFSET, (byte)ICMP6_ROUTER_SOLICITATION); + assertDrop(program, packet.array()); + put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_ROUTERS_ADDRESS); + assertDrop(program, packet.array()); + + apfFilter.shutdown(); + } + + @Test + public void testApfFilterMulticast() throws Exception { + final byte[] unicastIpv4Addr = {(byte)192,0,2,63}; + final byte[] broadcastIpv4Addr = {(byte)192,0,2,(byte)255}; + final byte[] multicastIpv4Addr = {(byte)224,0,0,1}; + final byte[] multicastIpv6Addr = {(byte)0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,(byte)0xfb}; + + MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + LinkAddress link = new LinkAddress(InetAddress.getByAddress(unicastIpv4Addr), 24); + LinkProperties lp = new LinkProperties(); + lp.addLinkAddress(link); + + ApfConfiguration config = getDefaultConfig(); + config.ieee802_3Filter = DROP_802_3_FRAMES; + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + apfFilter.setLinkProperties(lp); + + byte[] program = ipClientCallback.getApfProgram(); + + // Construct IPv4 and IPv6 multicast packets. + ByteBuffer mcastv4packet = ByteBuffer.wrap(new byte[100]); + mcastv4packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP); + put(mcastv4packet, IPV4_DEST_ADDR_OFFSET, multicastIpv4Addr); + + ByteBuffer mcastv6packet = ByteBuffer.wrap(new byte[100]); + mcastv6packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6); + mcastv6packet.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_UDP); + put(mcastv6packet, IPV6_DEST_ADDR_OFFSET, multicastIpv6Addr); + + // Construct IPv4 broadcast packet. + ByteBuffer bcastv4packet1 = ByteBuffer.wrap(new byte[100]); + bcastv4packet1.put(ETH_BROADCAST_MAC_ADDRESS); + bcastv4packet1.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP); + put(bcastv4packet1, IPV4_DEST_ADDR_OFFSET, multicastIpv4Addr); + + ByteBuffer bcastv4packet2 = ByteBuffer.wrap(new byte[100]); + bcastv4packet2.put(ETH_BROADCAST_MAC_ADDRESS); + bcastv4packet2.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP); + put(bcastv4packet2, IPV4_DEST_ADDR_OFFSET, IPV4_BROADCAST_ADDRESS); + + // Construct IPv4 broadcast with L2 unicast address packet (b/30231088). + ByteBuffer bcastv4unicastl2packet = ByteBuffer.wrap(new byte[100]); + bcastv4unicastl2packet.put(TestApfFilter.MOCK_MAC_ADDR); + bcastv4unicastl2packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP); + put(bcastv4unicastl2packet, IPV4_DEST_ADDR_OFFSET, broadcastIpv4Addr); + + // Verify initially disabled multicast filter is off + assertPass(program, mcastv4packet.array()); + assertPass(program, mcastv6packet.array()); + assertPass(program, bcastv4packet1.array()); + assertPass(program, bcastv4packet2.array()); + assertPass(program, bcastv4unicastl2packet.array()); + + // Turn on multicast filter and verify it works + ipClientCallback.resetApfProgramWait(); + apfFilter.setMulticastFilter(true); + program = ipClientCallback.getApfProgram(); + assertDrop(program, mcastv4packet.array()); + assertDrop(program, mcastv6packet.array()); + assertDrop(program, bcastv4packet1.array()); + assertDrop(program, bcastv4packet2.array()); + assertDrop(program, bcastv4unicastl2packet.array()); + + // Turn off multicast filter and verify it's off + ipClientCallback.resetApfProgramWait(); + apfFilter.setMulticastFilter(false); + program = ipClientCallback.getApfProgram(); + assertPass(program, mcastv4packet.array()); + assertPass(program, mcastv6packet.array()); + assertPass(program, bcastv4packet1.array()); + assertPass(program, bcastv4packet2.array()); + assertPass(program, bcastv4unicastl2packet.array()); + + // Verify it can be initialized to on + ipClientCallback.resetApfProgramWait(); + apfFilter.shutdown(); + config.multicastFilter = DROP_MULTICAST; + config.ieee802_3Filter = DROP_802_3_FRAMES; + apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + apfFilter.setLinkProperties(lp); + program = ipClientCallback.getApfProgram(); + assertDrop(program, mcastv4packet.array()); + assertDrop(program, mcastv6packet.array()); + assertDrop(program, bcastv4packet1.array()); + assertDrop(program, bcastv4unicastl2packet.array()); + + // Verify that ICMPv6 multicast is not dropped. + mcastv6packet.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_ICMPV6); + assertPass(program, mcastv6packet.array()); + + apfFilter.shutdown(); + } + + @Test + public void testApfFilterMulticastPingWhileDozing() throws Exception { + MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + ApfFilter apfFilter = setupApfFilter(ipClientCallback, getDefaultConfig()); + + // Construct a multicast ICMPv6 ECHO request. + final byte[] multicastIpv6Addr = {(byte)0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,(byte)0xfb}; + ByteBuffer packet = ByteBuffer.wrap(new byte[100]); + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6); + packet.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_ICMPV6); + packet.put(ICMP6_TYPE_OFFSET, (byte)ICMPV6_ECHO_REQUEST_TYPE); + put(packet, IPV6_DEST_ADDR_OFFSET, multicastIpv6Addr); + + // Normally, we let multicast pings alone... + assertPass(ipClientCallback.getApfProgram(), packet.array()); + + // ...and even while dozing... + apfFilter.setDozeMode(true); + assertPass(ipClientCallback.getApfProgram(), packet.array()); + + // ...but when the multicast filter is also enabled, drop the multicast pings to save power. + apfFilter.setMulticastFilter(true); + assertDrop(ipClientCallback.getApfProgram(), packet.array()); + + // However, we should still let through all other ICMPv6 types. + ByteBuffer raPacket = ByteBuffer.wrap(packet.array().clone()); + raPacket.put(ICMP6_TYPE_OFFSET, (byte) NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT); + assertPass(ipClientCallback.getApfProgram(), raPacket.array()); + + // Now wake up from doze mode to ensure that we no longer drop the packets. + // (The multicast filter is still enabled at this point). + apfFilter.setDozeMode(false); + assertPass(ipClientCallback.getApfProgram(), packet.array()); + + apfFilter.shutdown(); + } + + @Test + public void testApfFilter802_3() throws Exception { + MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + ApfConfiguration config = getDefaultConfig(); + ApfFilter apfFilter = setupApfFilter(ipClientCallback, config); + byte[] program = ipClientCallback.getApfProgram(); + + // Verify empty packet of 100 zero bytes is passed + // Note that eth-type = 0 makes it an IEEE802.3 frame + ByteBuffer packet = ByteBuffer.wrap(new byte[100]); + assertPass(program, packet.array()); + + // Verify empty packet with IPv4 is passed + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP); + assertPass(program, packet.array()); + + // Verify empty IPv6 packet is passed + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6); + assertPass(program, packet.array()); + + // Now turn on the filter + ipClientCallback.resetApfProgramWait(); + apfFilter.shutdown(); + config.ieee802_3Filter = DROP_802_3_FRAMES; + apfFilter = setupApfFilter(ipClientCallback, config); + program = ipClientCallback.getApfProgram(); + + // Verify that IEEE802.3 frame is dropped + // In this case ethtype is used for payload length + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)(100 - 14)); + assertDrop(program, packet.array()); + + // Verify that IPv4 (as example of Ethernet II) frame will pass + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP); + assertPass(program, packet.array()); + + // Verify that IPv6 (as example of Ethernet II) frame will pass + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6); + assertPass(program, packet.array()); + + apfFilter.shutdown(); + } + + @Test + public void testApfFilterEthTypeBL() throws Exception { + final int[] emptyBlackList = {}; + final int[] ipv4BlackList = {ETH_P_IP}; + final int[] ipv4Ipv6BlackList = {ETH_P_IP, ETH_P_IPV6}; + + MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + ApfConfiguration config = getDefaultConfig(); + ApfFilter apfFilter = setupApfFilter(ipClientCallback, config); + byte[] program = ipClientCallback.getApfProgram(); + + // Verify empty packet of 100 zero bytes is passed + // Note that eth-type = 0 makes it an IEEE802.3 frame + ByteBuffer packet = ByteBuffer.wrap(new byte[100]); + assertPass(program, packet.array()); + + // Verify empty packet with IPv4 is passed + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP); + assertPass(program, packet.array()); + + // Verify empty IPv6 packet is passed + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6); + assertPass(program, packet.array()); + + // Now add IPv4 to the black list + ipClientCallback.resetApfProgramWait(); + apfFilter.shutdown(); + config.ethTypeBlackList = ipv4BlackList; + apfFilter = setupApfFilter(ipClientCallback, config); + program = ipClientCallback.getApfProgram(); + + // Verify that IPv4 frame will be dropped + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP); + assertDrop(program, packet.array()); + + // Verify that IPv6 frame will pass + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6); + assertPass(program, packet.array()); + + // Now let us have both IPv4 and IPv6 in the black list + ipClientCallback.resetApfProgramWait(); + apfFilter.shutdown(); + config.ethTypeBlackList = ipv4Ipv6BlackList; + apfFilter = setupApfFilter(ipClientCallback, config); + program = ipClientCallback.getApfProgram(); + + // Verify that IPv4 frame will be dropped + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP); + assertDrop(program, packet.array()); + + // Verify that IPv6 frame will be dropped + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6); + assertDrop(program, packet.array()); + + apfFilter.shutdown(); + } + + private byte[] getProgram(MockIpClientCallback cb, ApfFilter filter, LinkProperties lp) { + cb.resetApfProgramWait(); + filter.setLinkProperties(lp); + return cb.getApfProgram(); + } + + private void verifyArpFilter(byte[] program, int filterResult) { + // Verify ARP request packet + assertPass(program, arpRequestBroadcast(MOCK_IPV4_ADDR)); + assertVerdict(filterResult, program, arpRequestBroadcast(ANOTHER_IPV4_ADDR)); + assertDrop(program, arpRequestBroadcast(IPV4_ANY_HOST_ADDR)); + + // Verify ARP reply packets from different source ip + assertDrop(program, arpReply(IPV4_ANY_HOST_ADDR, IPV4_ANY_HOST_ADDR)); + assertPass(program, arpReply(ANOTHER_IPV4_SOURCE_ADDR, IPV4_ANY_HOST_ADDR)); + assertPass(program, arpReply(BUG_PROBE_SOURCE_ADDR1, IPV4_ANY_HOST_ADDR)); + assertPass(program, arpReply(BUG_PROBE_SOURCE_ADDR2, IPV4_ANY_HOST_ADDR)); + + // Verify unicast ARP reply packet is always accepted. + assertPass(program, arpReply(IPV4_SOURCE_ADDR, MOCK_IPV4_ADDR)); + assertPass(program, arpReply(IPV4_SOURCE_ADDR, ANOTHER_IPV4_ADDR)); + assertPass(program, arpReply(IPV4_SOURCE_ADDR, IPV4_ANY_HOST_ADDR)); + + // Verify GARP reply packets are always filtered + assertDrop(program, garpReply()); + } + + @Test + public void testApfFilterArp() throws Exception { + MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + ApfConfiguration config = getDefaultConfig(); + config.multicastFilter = DROP_MULTICAST; + config.ieee802_3Filter = DROP_802_3_FRAMES; + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + + // Verify initially ARP request filter is off, and GARP filter is on. + verifyArpFilter(ipClientCallback.getApfProgram(), PASS); + + // Inform ApfFilter of our address and verify ARP filtering is on + LinkAddress linkAddress = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 24); + LinkProperties lp = new LinkProperties(); + assertTrue(lp.addLinkAddress(linkAddress)); + verifyArpFilter(getProgram(ipClientCallback, apfFilter, lp), DROP); + + // Inform ApfFilter of loss of IP and verify ARP filtering is off + verifyArpFilter(getProgram(ipClientCallback, apfFilter, new LinkProperties()), PASS); + + apfFilter.shutdown(); + } + + private static byte[] arpReply(byte[] sip, byte[] tip) { + ByteBuffer packet = ByteBuffer.wrap(new byte[100]); + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_ARP); + put(packet, ARP_HEADER_OFFSET, ARP_IPV4_REPLY_HEADER); + put(packet, ARP_SOURCE_IP_ADDRESS_OFFSET, sip); + put(packet, ARP_TARGET_IP_ADDRESS_OFFSET, tip); + return packet.array(); + } + + private static byte[] arpRequestBroadcast(byte[] tip) { + ByteBuffer packet = ByteBuffer.wrap(new byte[100]); + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_ARP); + put(packet, ETH_DEST_ADDR_OFFSET, ETH_BROADCAST_MAC_ADDRESS); + put(packet, ARP_HEADER_OFFSET, ARP_IPV4_REQUEST_HEADER); + put(packet, ARP_TARGET_IP_ADDRESS_OFFSET, tip); + return packet.array(); + } + + private static byte[] garpReply() { + ByteBuffer packet = ByteBuffer.wrap(new byte[100]); + packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_ARP); + put(packet, ETH_DEST_ADDR_OFFSET, ETH_BROADCAST_MAC_ADDRESS); + put(packet, ARP_HEADER_OFFSET, ARP_IPV4_REPLY_HEADER); + put(packet, ARP_TARGET_IP_ADDRESS_OFFSET, IPV4_ANY_HOST_ADDR); + return packet.array(); + } + + // Verify that the last program pushed to the IpClient.Callback properly filters the + // given packet for the given lifetime. + private void verifyRaLifetime(byte[] program, ByteBuffer packet, int lifetime) { + final int FRACTION_OF_LIFETIME = 6; + final int ageLimit = lifetime / FRACTION_OF_LIFETIME; + + // Verify new program should drop RA for 1/6th its lifetime and pass afterwards. + assertDrop(program, packet.array()); + assertDrop(program, packet.array(), ageLimit); + assertPass(program, packet.array(), ageLimit + 1); + assertPass(program, packet.array(), lifetime); + // Verify RA checksum is ignored + final short originalChecksum = packet.getShort(ICMP6_RA_CHECKSUM_OFFSET); + packet.putShort(ICMP6_RA_CHECKSUM_OFFSET, (short)12345); + assertDrop(program, packet.array()); + packet.putShort(ICMP6_RA_CHECKSUM_OFFSET, (short)-12345); + assertDrop(program, packet.array()); + packet.putShort(ICMP6_RA_CHECKSUM_OFFSET, originalChecksum); + + // Verify other changes to RA make it not match filter + final byte originalFirstByte = packet.get(0); + packet.put(0, (byte)-1); + assertPass(program, packet.array()); + packet.put(0, (byte)0); + assertDrop(program, packet.array()); + packet.put(0, originalFirstByte); + } + + // Test that when ApfFilter is shown the given packet, it generates a program to filter it + // for the given lifetime. + private void verifyRaLifetime(TestApfFilter apfFilter, MockIpClientCallback ipClientCallback, + ByteBuffer packet, int lifetime) throws IOException, ErrnoException { + // Verify new program generated if ApfFilter witnesses RA + ipClientCallback.resetApfProgramWait(); + apfFilter.pretendPacketReceived(packet.array()); + byte[] program = ipClientCallback.getApfProgram(); + verifyRaLifetime(program, packet, lifetime); + } + + private void verifyRaEvent(RaEvent expected) { + ArgumentCaptor<IpConnectivityLog.Event> captor = + ArgumentCaptor.forClass(IpConnectivityLog.Event.class); + verify(mLog, atLeastOnce()).log(captor.capture()); + RaEvent got = lastRaEvent(captor.getAllValues()); + if (!raEventEquals(expected, got)) { + assertEquals(expected, got); // fail for printing an assertion error message. + } + } + + private RaEvent lastRaEvent(List<IpConnectivityLog.Event> events) { + RaEvent got = null; + for (Parcelable ev : events) { + if (ev instanceof RaEvent) { + got = (RaEvent) ev; + } + } + return got; + } + + private boolean raEventEquals(RaEvent ev1, RaEvent ev2) { + return (ev1 != null) && (ev2 != null) + && (ev1.routerLifetime == ev2.routerLifetime) + && (ev1.prefixValidLifetime == ev2.prefixValidLifetime) + && (ev1.prefixPreferredLifetime == ev2.prefixPreferredLifetime) + && (ev1.routeInfoLifetime == ev2.routeInfoLifetime) + && (ev1.rdnssLifetime == ev2.rdnssLifetime) + && (ev1.dnsslLifetime == ev2.dnsslLifetime); + } + + private void assertInvalidRa(TestApfFilter apfFilter, MockIpClientCallback ipClientCallback, + ByteBuffer packet) throws IOException, ErrnoException { + ipClientCallback.resetApfProgramWait(); + apfFilter.pretendPacketReceived(packet.array()); + ipClientCallback.assertNoProgramUpdate(); + } + + @Test + public void testApfFilterRa() throws Exception { + MockIpClientCallback ipClientCallback = new MockIpClientCallback(); + ApfConfiguration config = getDefaultConfig(); + config.multicastFilter = DROP_MULTICAST; + config.ieee802_3Filter = DROP_802_3_FRAMES; + TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog); + byte[] program = ipClientCallback.getApfProgram(); + + final int ROUTER_LIFETIME = 1000; + final int PREFIX_VALID_LIFETIME = 200; + final int PREFIX_PREFERRED_LIFETIME = 100; + final int RDNSS_LIFETIME = 300; + final int ROUTE_LIFETIME = 400; + // Note that lifetime of 2000 will be ignored in favor of shorter route lifetime of 1000. + final int DNSSL_LIFETIME = 2000; + final int VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET = ETH_HEADER_LEN; + // IPv6, traffic class = 0, flow label = 0x12345 + final int VERSION_TRAFFIC_CLASS_FLOW_LABEL = 0x60012345; + + // Verify RA is passed the first time + ByteBuffer basePacket = ByteBuffer.wrap(new byte[ICMP6_RA_OPTION_OFFSET]); + basePacket.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IPV6); + basePacket.putInt(VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET, + VERSION_TRAFFIC_CLASS_FLOW_LABEL); + basePacket.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_ICMPV6); + basePacket.put(ICMP6_TYPE_OFFSET, (byte)ICMP6_ROUTER_ADVERTISEMENT); + basePacket.putShort(ICMP6_RA_ROUTER_LIFETIME_OFFSET, (short)ROUTER_LIFETIME); + basePacket.position(IPV6_DEST_ADDR_OFFSET); + basePacket.put(IPV6_ALL_NODES_ADDRESS); + assertPass(program, basePacket.array()); + + verifyRaLifetime(apfFilter, ipClientCallback, basePacket, ROUTER_LIFETIME); + verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, -1, -1)); + + ByteBuffer newFlowLabelPacket = ByteBuffer.wrap(new byte[ICMP6_RA_OPTION_OFFSET]); + basePacket.clear(); + newFlowLabelPacket.put(basePacket); + // Check that changes are ignored in every byte of the flow label. + newFlowLabelPacket.putInt(VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET, + VERSION_TRAFFIC_CLASS_FLOW_LABEL + 0x11111); + + // Ensure zero-length options cause the packet to be silently skipped. + // Do this before we test other packets. http://b/29586253 + ByteBuffer zeroLengthOptionPacket = ByteBuffer.wrap( + new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]); + basePacket.clear(); + zeroLengthOptionPacket.put(basePacket); + zeroLengthOptionPacket.put((byte)ICMP6_PREFIX_OPTION_TYPE); + zeroLengthOptionPacket.put((byte)0); + assertInvalidRa(apfFilter, ipClientCallback, zeroLengthOptionPacket); + + // Generate several RAs with different options and lifetimes, and verify when + // ApfFilter is shown these packets, it generates programs to filter them for the + // appropriate lifetime. + ByteBuffer prefixOptionPacket = ByteBuffer.wrap( + new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_PREFIX_OPTION_LEN]); + basePacket.clear(); + prefixOptionPacket.put(basePacket); + prefixOptionPacket.put((byte)ICMP6_PREFIX_OPTION_TYPE); + prefixOptionPacket.put((byte)(ICMP6_PREFIX_OPTION_LEN / 8)); + prefixOptionPacket.putInt( + ICMP6_RA_OPTION_OFFSET + ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET, + PREFIX_PREFERRED_LIFETIME); + prefixOptionPacket.putInt( + ICMP6_RA_OPTION_OFFSET + ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET, + PREFIX_VALID_LIFETIME); + verifyRaLifetime( + apfFilter, ipClientCallback, prefixOptionPacket, PREFIX_PREFERRED_LIFETIME); + verifyRaEvent(new RaEvent( + ROUTER_LIFETIME, PREFIX_VALID_LIFETIME, PREFIX_PREFERRED_LIFETIME, -1, -1, -1)); + + ByteBuffer rdnssOptionPacket = ByteBuffer.wrap( + new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]); + basePacket.clear(); + rdnssOptionPacket.put(basePacket); + rdnssOptionPacket.put((byte)ICMP6_RDNSS_OPTION_TYPE); + rdnssOptionPacket.put((byte)(ICMP6_4_BYTE_OPTION_LEN / 8)); + rdnssOptionPacket.putInt( + ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_LIFETIME_OFFSET, RDNSS_LIFETIME); + verifyRaLifetime(apfFilter, ipClientCallback, rdnssOptionPacket, RDNSS_LIFETIME); + verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, RDNSS_LIFETIME, -1)); + + ByteBuffer routeInfoOptionPacket = ByteBuffer.wrap( + new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]); + basePacket.clear(); + routeInfoOptionPacket.put(basePacket); + routeInfoOptionPacket.put((byte)ICMP6_ROUTE_INFO_OPTION_TYPE); + routeInfoOptionPacket.put((byte)(ICMP6_4_BYTE_OPTION_LEN / 8)); + routeInfoOptionPacket.putInt( + ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_LIFETIME_OFFSET, ROUTE_LIFETIME); + verifyRaLifetime(apfFilter, ipClientCallback, routeInfoOptionPacket, ROUTE_LIFETIME); + verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, ROUTE_LIFETIME, -1, -1)); + + ByteBuffer dnsslOptionPacket = ByteBuffer.wrap( + new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]); + basePacket.clear(); + dnsslOptionPacket.put(basePacket); + dnsslOptionPacket.put((byte)ICMP6_DNSSL_OPTION_TYPE); + dnsslOptionPacket.put((byte)(ICMP6_4_BYTE_OPTION_LEN / 8)); + dnsslOptionPacket.putInt( + ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_LIFETIME_OFFSET, DNSSL_LIFETIME); + verifyRaLifetime(apfFilter, ipClientCallback, dnsslOptionPacket, ROUTER_LIFETIME); + verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, -1, DNSSL_LIFETIME)); + + // Verify that current program filters all five RAs: + program = ipClientCallback.getApfProgram(); + verifyRaLifetime(program, basePacket, ROUTER_LIFETIME); + verifyRaLifetime(program, newFlowLabelPacket, ROUTER_LIFETIME); + verifyRaLifetime(program, prefixOptionPacket, PREFIX_PREFERRED_LIFETIME); + verifyRaLifetime(program, rdnssOptionPacket, RDNSS_LIFETIME); + verifyRaLifetime(program, routeInfoOptionPacket, ROUTE_LIFETIME); + verifyRaLifetime(program, dnsslOptionPacket, ROUTER_LIFETIME); + + apfFilter.shutdown(); + } + + /** + * Stage a file for testing, i.e. make it native accessible. Given a resource ID, + * copy that resource into the app's data directory and return the path to it. + */ + private String stageFile(int rawId) throws Exception { + File file = new File(InstrumentationRegistry.getContext().getFilesDir(), "staged_file"); + new File(file.getParent()).mkdirs(); + InputStream in = null; + OutputStream out = null; + try { + in = InstrumentationRegistry.getContext().getResources().openRawResource(rawId); + out = new FileOutputStream(file); + Streams.copy(in, out); + } finally { + if (in != null) in.close(); + if (out != null) out.close(); + } + return file.getAbsolutePath(); + } + + private static void put(ByteBuffer buffer, int position, byte[] bytes) { + final int original = buffer.position(); + buffer.position(position); + buffer.put(bytes); + buffer.position(original); + } + + @Test + public void testRaParsing() throws Exception { + final int maxRandomPacketSize = 512; + final Random r = new Random(); + MockIpClientCallback cb = new MockIpClientCallback(); + ApfConfiguration config = getDefaultConfig(); + config.multicastFilter = DROP_MULTICAST; + config.ieee802_3Filter = DROP_802_3_FRAMES; + TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog); + for (int i = 0; i < 1000; i++) { + byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)]; + r.nextBytes(packet); + try { + apfFilter.new Ra(packet, packet.length); + } catch (ApfFilter.InvalidRaException e) { + } catch (Exception e) { + throw new Exception("bad packet: " + HexDump.toHexString(packet), e); + } + } + } + + @Test + public void testRaProcessing() throws Exception { + final int maxRandomPacketSize = 512; + final Random r = new Random(); + MockIpClientCallback cb = new MockIpClientCallback(); + ApfConfiguration config = getDefaultConfig(); + config.multicastFilter = DROP_MULTICAST; + config.ieee802_3Filter = DROP_802_3_FRAMES; + TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog); + for (int i = 0; i < 1000; i++) { + byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)]; + r.nextBytes(packet); + try { + apfFilter.processRa(packet, packet.length); + } catch (Exception e) { + throw new Exception("bad packet: " + HexDump.toHexString(packet), e); + } + } + } + + /** + * Call the APF interpreter to run {@code program} on {@code packet} with persistent memory + * segment {@data} pretending the filter was installed {@code filter_age} seconds ago. + */ + private native static int apfSimulate(byte[] program, byte[] packet, byte[] data, + int filter_age); + + /** + * Compile a tcpdump human-readable filter (e.g. "icmp" or "tcp port 54") into a BPF + * prorgam and return a human-readable dump of the BPF program identical to "tcpdump -d". + */ + private native static String compileToBpf(String filter); + + /** + * Open packet capture file {@code pcap_filename} and filter the packets using tcpdump + * human-readable filter (e.g. "icmp" or "tcp port 54") compiled to a BPF program and + * at the same time using APF program {@code apf_program}. Return {@code true} if + * both APF and BPF programs filter out exactly the same packets. + */ + private native static boolean compareBpfApf(String filter, String pcap_filename, + byte[] apf_program); + + + /** + * Open packet capture file {@code pcapFilename} and run it through APF filter. Then + * checks whether all the packets are dropped and populates data[] {@code data} with + * the APF counters. + */ + private native static boolean dropsAllPackets(byte[] program, byte[] data, String pcapFilename); + + @Test + public void testBroadcastAddress() throws Exception { + assertEqualsIp("255.255.255.255", ApfFilter.ipv4BroadcastAddress(IPV4_ANY_HOST_ADDR, 0)); + assertEqualsIp("0.0.0.0", ApfFilter.ipv4BroadcastAddress(IPV4_ANY_HOST_ADDR, 32)); + assertEqualsIp("0.0.3.255", ApfFilter.ipv4BroadcastAddress(IPV4_ANY_HOST_ADDR, 22)); + assertEqualsIp("0.255.255.255", ApfFilter.ipv4BroadcastAddress(IPV4_ANY_HOST_ADDR, 8)); + + assertEqualsIp("255.255.255.255", ApfFilter.ipv4BroadcastAddress(MOCK_IPV4_ADDR, 0)); + assertEqualsIp("10.0.0.1", ApfFilter.ipv4BroadcastAddress(MOCK_IPV4_ADDR, 32)); + assertEqualsIp("10.0.0.255", ApfFilter.ipv4BroadcastAddress(MOCK_IPV4_ADDR, 24)); + assertEqualsIp("10.0.255.255", ApfFilter.ipv4BroadcastAddress(MOCK_IPV4_ADDR, 16)); + } + + public void assertEqualsIp(String expected, int got) throws Exception { + int want = bytesToBEInt(InetAddress.getByName(expected).getAddress()); + assertEquals(want, got); + } +} diff --git a/tests/src/android/net/apf/Bpf2Apf.java b/tests/src/android/net/apf/Bpf2Apf.java new file mode 100644 index 0000000..5d57cde --- /dev/null +++ b/tests/src/android/net/apf/Bpf2Apf.java @@ -0,0 +1,327 @@ +/* + * 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.apf; + +import android.net.apf.ApfGenerator; +import android.net.apf.ApfGenerator.IllegalInstructionException; +import android.net.apf.ApfGenerator.Register; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/** + * BPF to APF translator. + * + * Note: This is for testing purposes only and is not guaranteed to support + * translation of all BPF programs. + * + * Example usage: + * javac net/java/android/net/apf/ApfGenerator.java \ + * tests/servicestests/src/android/net/apf/Bpf2Apf.java + * sudo tcpdump -i em1 -d icmp | java -classpath tests/servicestests/src:net/java \ + * android.net.apf.Bpf2Apf + */ +public class Bpf2Apf { + private static int parseImm(String line, String arg) { + if (!arg.startsWith("#0x")) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + final long val_long = Long.parseLong(arg.substring(3), 16); + if (val_long < 0 || val_long > Long.parseLong("ffffffff", 16)) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + return new Long((val_long << 32) >> 32).intValue(); + } + + /** + * Convert a single line of "tcpdump -d" (human readable BPF program dump) {@code line} into + * APF instruction(s) and append them to {@code gen}. Here's an example line: + * (001) jeq #0x86dd jt 2 jf 7 + */ + private static void convertLine(String line, ApfGenerator gen) + throws IllegalInstructionException { + if (line.indexOf("(") != 0 || line.indexOf(")") != 4 || line.indexOf(" ") != 5) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + int label = Integer.parseInt(line.substring(1, 4)); + gen.defineLabel(Integer.toString(label)); + String opcode = line.substring(6, 10).trim(); + String arg = line.substring(15, Math.min(32, line.length())).trim(); + switch (opcode) { + case "ld": + case "ldh": + case "ldb": + case "ldx": + case "ldxb": + case "ldxh": + Register dest = opcode.contains("x") ? Register.R1 : Register.R0; + if (arg.equals("4*([14]&0xf)")) { + if (!opcode.equals("ldxb")) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + gen.addLoadFromMemory(dest, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); + break; + } + if (arg.equals("#pktlen")) { + if (!opcode.equals("ld")) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + gen.addLoadFromMemory(dest, gen.PACKET_SIZE_MEMORY_SLOT); + break; + } + if (arg.startsWith("#0x")) { + if (!opcode.equals("ld")) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + gen.addLoadImmediate(dest, parseImm(line, arg)); + break; + } + if (arg.startsWith("M[")) { + if (!opcode.startsWith("ld")) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + int memory_slot = Integer.parseInt(arg.substring(2, arg.length() - 1)); + if (memory_slot < 0 || memory_slot >= gen.MEMORY_SLOTS || + // Disallow use of pre-filled slots as BPF programs might + // wrongfully assume they're initialized to 0. + (memory_slot >= gen.FIRST_PREFILLED_MEMORY_SLOT && + memory_slot <= gen.LAST_PREFILLED_MEMORY_SLOT)) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + gen.addLoadFromMemory(dest, memory_slot); + break; + } + if (arg.startsWith("[x + ")) { + int offset = Integer.parseInt(arg.substring(5, arg.length() - 1)); + switch (opcode) { + case "ld": + case "ldx": + gen.addLoad32Indexed(dest, offset); + break; + case "ldh": + case "ldxh": + gen.addLoad16Indexed(dest, offset); + break; + case "ldb": + case "ldxb": + gen.addLoad8Indexed(dest, offset); + break; + } + } else { + int offset = Integer.parseInt(arg.substring(1, arg.length() - 1)); + switch (opcode) { + case "ld": + case "ldx": + gen.addLoad32(dest, offset); + break; + case "ldh": + case "ldxh": + gen.addLoad16(dest, offset); + break; + case "ldb": + case "ldxb": + gen.addLoad8(dest, offset); + break; + } + } + break; + case "st": + case "stx": + Register src = opcode.contains("x") ? Register.R1 : Register.R0; + if (!arg.startsWith("M[")) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + int memory_slot = Integer.parseInt(arg.substring(2, arg.length() - 1)); + if (memory_slot < 0 || memory_slot >= gen.MEMORY_SLOTS || + // Disallow overwriting pre-filled slots + (memory_slot >= gen.FIRST_PREFILLED_MEMORY_SLOT && + memory_slot <= gen.LAST_PREFILLED_MEMORY_SLOT)) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + gen.addStoreToMemory(src, memory_slot); + break; + case "add": + case "and": + case "or": + case "sub": + if (arg.equals("x")) { + switch(opcode) { + case "add": + gen.addAddR1(); + break; + case "and": + gen.addAndR1(); + break; + case "or": + gen.addOrR1(); + break; + case "sub": + gen.addNeg(Register.R1); + gen.addAddR1(); + gen.addNeg(Register.R1); + break; + } + } else { + int imm = parseImm(line, arg); + switch(opcode) { + case "add": + gen.addAdd(imm); + break; + case "and": + gen.addAnd(imm); + break; + case "or": + gen.addOr(imm); + break; + case "sub": + gen.addAdd(-imm); + break; + } + } + break; + case "jeq": + case "jset": + case "jgt": + case "jge": + int val = 0; + boolean reg_compare; + if (arg.startsWith("x")) { + reg_compare = true; + } else { + reg_compare = false; + val = parseImm(line, arg); + } + int jt_offset = line.indexOf("jt"); + int jf_offset = line.indexOf("jf"); + String true_label = line.substring(jt_offset + 2, jf_offset).trim(); + String false_label = line.substring(jf_offset + 2).trim(); + boolean true_label_is_fallthrough = Integer.parseInt(true_label) == label + 1; + boolean false_label_is_fallthrough = Integer.parseInt(false_label) == label + 1; + if (true_label_is_fallthrough && false_label_is_fallthrough) + break; + switch (opcode) { + case "jeq": + if (!true_label_is_fallthrough) { + if (reg_compare) { + gen.addJumpIfR0EqualsR1(true_label); + } else { + gen.addJumpIfR0Equals(val, true_label); + } + } + if (!false_label_is_fallthrough) { + if (!true_label_is_fallthrough) { + gen.addJump(false_label); + } else if (reg_compare) { + gen.addJumpIfR0NotEqualsR1(false_label); + } else { + gen.addJumpIfR0NotEquals(val, false_label); + } + } + break; + case "jset": + if (reg_compare) { + gen.addJumpIfR0AnyBitsSetR1(true_label); + } else { + gen.addJumpIfR0AnyBitsSet(val, true_label); + } + if (!false_label_is_fallthrough) { + gen.addJump(false_label); + } + break; + case "jgt": + if (!true_label_is_fallthrough || + // We have no less-than-or-equal-to register to register + // comparison instruction, so in this case we'll jump + // around an unconditional jump. + (!false_label_is_fallthrough && reg_compare)) { + if (reg_compare) { + gen.addJumpIfR0GreaterThanR1(true_label); + } else { + gen.addJumpIfR0GreaterThan(val, true_label); + } + } + if (!false_label_is_fallthrough) { + if (!true_label_is_fallthrough || reg_compare) { + gen.addJump(false_label); + } else { + gen.addJumpIfR0LessThan(val + 1, false_label); + } + } + break; + case "jge": + if (!false_label_is_fallthrough || + // We have no greater-than-or-equal-to register to register + // comparison instruction, so in this case we'll jump + // around an unconditional jump. + (!true_label_is_fallthrough && reg_compare)) { + if (reg_compare) { + gen.addJumpIfR0LessThanR1(false_label); + } else { + gen.addJumpIfR0LessThan(val, false_label); + } + } + if (!true_label_is_fallthrough) { + if (!false_label_is_fallthrough || reg_compare) { + gen.addJump(true_label); + } else { + gen.addJumpIfR0GreaterThan(val - 1, true_label); + } + } + break; + } + break; + case "ret": + if (arg.equals("#0")) { + gen.addJump(gen.DROP_LABEL); + } else { + gen.addJump(gen.PASS_LABEL); + } + break; + case "tax": + gen.addMove(Register.R1); + break; + case "txa": + gen.addMove(Register.R0); + break; + default: + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + } + + /** + * Convert the output of "tcpdump -d" (human readable BPF program dump) {@code bpf} into an APF + * program and return it. + */ + public static byte[] convert(String bpf) throws IllegalInstructionException { + ApfGenerator gen = new ApfGenerator(3); + for (String line : bpf.split("\\n")) convertLine(line, gen); + return gen.generate(); + } + + /** + * Convert the output of "tcpdump -d" (human readable BPF program dump) piped in stdin into an + * APF program and output it via stdout. + */ + public static void main(String[] args) throws Exception { + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + String line = null; + StringBuilder responseData = new StringBuilder(); + ApfGenerator gen = new ApfGenerator(3); + while ((line = in.readLine()) != null) convertLine(line, gen); + System.out.write(gen.generate()); + } +} diff --git a/tests/src/android/net/dhcp/DhcpPacketTest.java b/tests/src/android/net/dhcp/DhcpPacketTest.java new file mode 100644 index 0000000..a592809 --- /dev/null +++ b/tests/src/android/net/dhcp/DhcpPacketTest.java @@ -0,0 +1,1075 @@ +/* + * 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.NetworkUtils.getBroadcastAddress; +import static android.net.NetworkUtils.getPrefixMaskAsInet4Address; +import static android.net.dhcp.DhcpPacket.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.annotation.Nullable; +import android.net.DhcpResults; +import android.net.LinkAddress; +import android.net.NetworkUtils; +import android.net.metrics.DhcpErrorEvent; +import android.support.test.runner.AndroidJUnit4; +import android.support.test.filters.SmallTest; + +import com.android.internal.util.HexDump; + +import java.io.ByteArrayOutputStream; +import java.net.Inet4Address; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Random; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DhcpPacketTest { + + private static final Inet4Address SERVER_ADDR = v4Address("192.0.2.1"); + private static final Inet4Address CLIENT_ADDR = v4Address("192.0.2.234"); + private static final int PREFIX_LENGTH = 22; + private static final Inet4Address NETMASK = getPrefixMaskAsInet4Address(PREFIX_LENGTH); + private static final Inet4Address BROADCAST_ADDR = getBroadcastAddress( + SERVER_ADDR, PREFIX_LENGTH); + private static final String HOSTNAME = "testhostname"; + private static final short MTU = 1500; + // Use our own empty address instead of Inet4Address.ANY or INADDR_ANY to ensure that the code + // doesn't use == instead of equals when comparing addresses. + private static final Inet4Address ANY = (Inet4Address) v4Address("0.0.0.0"); + + private static final byte[] CLIENT_MAC = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }; + + private static final Inet4Address v4Address(String addrString) throws IllegalArgumentException { + return (Inet4Address) NetworkUtils.numericToInetAddress(addrString); + } + + @Before + public void setUp() { + DhcpPacket.testOverrideVendorId = "android-dhcp-???"; + DhcpPacket.testOverrideHostname = "android-01234567890abcde"; + } + + class TestDhcpPacket extends DhcpPacket { + private byte mType; + // TODO: Make this a map of option numbers to bytes instead. + private byte[] mDomainBytes, mVendorInfoBytes, mLeaseTimeBytes, mNetmaskBytes; + + public TestDhcpPacket(byte type, Inet4Address clientIp, Inet4Address yourIp) { + super(0xdeadbeef, (short) 0, clientIp, yourIp, INADDR_ANY, INADDR_ANY, + CLIENT_MAC, true); + mType = type; + } + + public TestDhcpPacket(byte type) { + this(type, INADDR_ANY, CLIENT_ADDR); + } + + public TestDhcpPacket setDomainBytes(byte[] domainBytes) { + mDomainBytes = domainBytes; + return this; + } + + public TestDhcpPacket setVendorInfoBytes(byte[] vendorInfoBytes) { + mVendorInfoBytes = vendorInfoBytes; + return this; + } + + public TestDhcpPacket setLeaseTimeBytes(byte[] leaseTimeBytes) { + mLeaseTimeBytes = leaseTimeBytes; + return this; + } + + public TestDhcpPacket setNetmaskBytes(byte[] netmaskBytes) { + mNetmaskBytes = netmaskBytes; + return this; + } + + public ByteBuffer buildPacket(int encap, short unusedDestUdp, short unusedSrcUdp) { + ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH); + fillInPacket(encap, CLIENT_ADDR, SERVER_ADDR, + DHCP_CLIENT, DHCP_SERVER, result, DHCP_BOOTREPLY, false); + return result; + } + + public void finishPacket(ByteBuffer buffer) { + addTlv(buffer, DHCP_MESSAGE_TYPE, mType); + if (mDomainBytes != null) { + addTlv(buffer, DHCP_DOMAIN_NAME, mDomainBytes); + } + if (mVendorInfoBytes != null) { + addTlv(buffer, DHCP_VENDOR_INFO, mVendorInfoBytes); + } + if (mLeaseTimeBytes != null) { + addTlv(buffer, DHCP_LEASE_TIME, mLeaseTimeBytes); + } + if (mNetmaskBytes != null) { + addTlv(buffer, DHCP_SUBNET_MASK, mNetmaskBytes); + } + addTlvEnd(buffer); + } + + // Convenience method. + public ByteBuffer build() { + // ENCAP_BOOTP packets don't contain ports, so just pass in 0. + ByteBuffer pkt = buildPacket(ENCAP_BOOTP, (short) 0, (short) 0); + pkt.flip(); + return pkt; + } + } + + private void assertDomainAndVendorInfoParses( + String expectedDomain, byte[] domainBytes, + String expectedVendorInfo, byte[] vendorInfoBytes) throws Exception { + ByteBuffer packet = new TestDhcpPacket(DHCP_MESSAGE_TYPE_OFFER) + .setDomainBytes(domainBytes) + .setVendorInfoBytes(vendorInfoBytes) + .build(); + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP); + assertEquals(expectedDomain, offerPacket.mDomainName); + assertEquals(expectedVendorInfo, offerPacket.mVendorInfo); + } + + @Test + public void testDomainName() throws Exception { + byte[] nullByte = new byte[] { 0x00 }; + byte[] twoNullBytes = new byte[] { 0x00, 0x00 }; + byte[] nonNullDomain = new byte[] { + (byte) 'g', (byte) 'o', (byte) 'o', (byte) '.', (byte) 'g', (byte) 'l' + }; + byte[] trailingNullDomain = new byte[] { + (byte) 'g', (byte) 'o', (byte) 'o', (byte) '.', (byte) 'g', (byte) 'l', 0x00 + }; + byte[] embeddedNullsDomain = new byte[] { + (byte) 'g', (byte) 'o', (byte) 'o', 0x00, 0x00, (byte) 'g', (byte) 'l' + }; + byte[] metered = "ANDROID_METERED".getBytes("US-ASCII"); + + byte[] meteredEmbeddedNull = metered.clone(); + meteredEmbeddedNull[7] = (char) 0; + + byte[] meteredTrailingNull = metered.clone(); + meteredTrailingNull[meteredTrailingNull.length - 1] = (char) 0; + + assertDomainAndVendorInfoParses("", nullByte, "\u0000", nullByte); + assertDomainAndVendorInfoParses("", twoNullBytes, "\u0000\u0000", twoNullBytes); + assertDomainAndVendorInfoParses("goo.gl", nonNullDomain, "ANDROID_METERED", metered); + assertDomainAndVendorInfoParses("goo", embeddedNullsDomain, + "ANDROID\u0000METERED", meteredEmbeddedNull); + assertDomainAndVendorInfoParses("goo.gl", trailingNullDomain, + "ANDROID_METERE\u0000", meteredTrailingNull); + } + + private void assertLeaseTimeParses(boolean expectValid, Integer rawLeaseTime, + long leaseTimeMillis, byte[] leaseTimeBytes) throws Exception { + TestDhcpPacket testPacket = new TestDhcpPacket(DHCP_MESSAGE_TYPE_OFFER); + if (leaseTimeBytes != null) { + testPacket.setLeaseTimeBytes(leaseTimeBytes); + } + ByteBuffer packet = testPacket.build(); + DhcpPacket offerPacket = null; + + if (!expectValid) { + try { + offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP); + fail("Invalid packet parsed successfully: " + offerPacket); + } catch (ParseException expected) { + } + return; + } + + offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP); + assertNotNull(offerPacket); + assertEquals(rawLeaseTime, offerPacket.mLeaseTime); + DhcpResults dhcpResults = offerPacket.toDhcpResults(); // Just check this doesn't crash. + assertEquals(leaseTimeMillis, offerPacket.getLeaseTimeMillis()); + } + + @Test + public void testLeaseTime() throws Exception { + byte[] noLease = null; + byte[] tooShortLease = new byte[] { 0x00, 0x00 }; + byte[] tooLongLease = new byte[] { 0x00, 0x00, 0x00, 60, 0x01 }; + byte[] zeroLease = new byte[] { 0x00, 0x00, 0x00, 0x00 }; + byte[] tenSecondLease = new byte[] { 0x00, 0x00, 0x00, 10 }; + byte[] oneMinuteLease = new byte[] { 0x00, 0x00, 0x00, 60 }; + byte[] fiveMinuteLease = new byte[] { 0x00, 0x00, 0x01, 0x2c }; + byte[] oneDayLease = new byte[] { 0x00, 0x01, 0x51, (byte) 0x80 }; + byte[] maxIntPlusOneLease = new byte[] { (byte) 0x80, 0x00, 0x00, 0x01 }; + byte[] infiniteLease = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff }; + + assertLeaseTimeParses(true, null, 0, noLease); + assertLeaseTimeParses(false, null, 0, tooShortLease); + assertLeaseTimeParses(false, null, 0, tooLongLease); + assertLeaseTimeParses(true, 0, 60 * 1000, zeroLease); + assertLeaseTimeParses(true, 10, 60 * 1000, tenSecondLease); + assertLeaseTimeParses(true, 60, 60 * 1000, oneMinuteLease); + assertLeaseTimeParses(true, 300, 300 * 1000, fiveMinuteLease); + assertLeaseTimeParses(true, 86400, 86400 * 1000, oneDayLease); + assertLeaseTimeParses(true, -2147483647, 2147483649L * 1000, maxIntPlusOneLease); + assertLeaseTimeParses(true, DhcpPacket.INFINITE_LEASE, 0, infiniteLease); + } + + private void checkIpAddress(String expected, Inet4Address clientIp, Inet4Address yourIp, + byte[] netmaskBytes) throws Exception { + checkIpAddress(expected, DHCP_MESSAGE_TYPE_OFFER, clientIp, yourIp, netmaskBytes); + checkIpAddress(expected, DHCP_MESSAGE_TYPE_ACK, clientIp, yourIp, netmaskBytes); + } + + private void checkIpAddress(String expected, byte type, + Inet4Address clientIp, Inet4Address yourIp, + byte[] netmaskBytes) throws Exception { + ByteBuffer packet = new TestDhcpPacket(type, clientIp, yourIp) + .setNetmaskBytes(netmaskBytes) + .build(); + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_BOOTP); + DhcpResults results = offerPacket.toDhcpResults(); + + if (expected != null) { + LinkAddress expectedAddress = new LinkAddress(expected); + assertEquals(expectedAddress, results.ipAddress); + } else { + assertNull(results); + } + } + + @Test + public void testIpAddress() throws Exception { + byte[] slash11Netmask = new byte[] { (byte) 0xff, (byte) 0xe0, 0x00, 0x00 }; + byte[] slash24Netmask = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00 }; + byte[] invalidNetmask = new byte[] { (byte) 0xff, (byte) 0xfb, (byte) 0xff, 0x00 }; + Inet4Address example1 = v4Address("192.0.2.1"); + Inet4Address example2 = v4Address("192.0.2.43"); + + // A packet without any addresses is not valid. + checkIpAddress(null, ANY, ANY, slash24Netmask); + + // ClientIP is used iff YourIP is not present. + checkIpAddress("192.0.2.1/24", example2, example1, slash24Netmask); + checkIpAddress("192.0.2.43/11", example2, ANY, slash11Netmask); + checkIpAddress("192.0.2.43/11", ANY, example2, slash11Netmask); + + // Invalid netmasks are ignored. + checkIpAddress(null, example2, ANY, invalidNetmask); + + // If there is no netmask, implicit netmasks are used. + checkIpAddress("192.0.2.43/24", ANY, example2, null); + } + + private void assertDhcpResults(String ipAddress, String gateway, String dnsServersString, + String domains, String serverAddress, String vendorInfo, int leaseDuration, + boolean hasMeteredHint, int mtu, DhcpResults dhcpResults) throws Exception { + assertEquals(new LinkAddress(ipAddress), dhcpResults.ipAddress); + assertEquals(v4Address(gateway), dhcpResults.gateway); + + String[] dnsServerStrings = dnsServersString.split(","); + ArrayList dnsServers = new ArrayList(); + for (String dnsServerString : dnsServerStrings) { + dnsServers.add(v4Address(dnsServerString)); + } + assertEquals(dnsServers, dhcpResults.dnsServers); + + assertEquals(domains, dhcpResults.domains); + assertEquals(v4Address(serverAddress), dhcpResults.serverAddress); + assertEquals(vendorInfo, dhcpResults.vendorInfo); + assertEquals(leaseDuration, dhcpResults.leaseDuration); + assertEquals(hasMeteredHint, dhcpResults.hasMeteredHint()); + assertEquals(mtu, dhcpResults.mtu); + } + + @Test + public void testOffer1() throws Exception { + // TODO: Turn all of these into golden files. This will probably require using + // android.support.test.InstrumentationRegistry for obtaining a Context object + // to read such golden files, along with an appropriate Android.mk. + final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray( + // IP header. + "451001480000000080118849c0a89003c0a89ff7" + + // UDP header. + "004300440134dcfa" + + // BOOTP header. + "02010600c997a63b0000000000000000c0a89ff70000000000000000" + + // MAC address. + "30766ff2a90c00000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Options + "638253633501023604c0a89003330400001c200104fffff0000304c0a89ffe06080808080808080404" + + "3a0400000e103b040000189cff00000000000000000000")); + + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3); + assertTrue(offerPacket instanceof DhcpOfferPacket); // Implicitly checks it's non-null. + DhcpResults dhcpResults = offerPacket.toDhcpResults(); + assertDhcpResults("192.168.159.247/20", "192.168.159.254", "8.8.8.8,8.8.4.4", + null, "192.168.144.3", null, 7200, false, 0, dhcpResults); + } + + @Test + public void testOffer2() throws Exception { + final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray( + // IP header. + "450001518d0600004011144dc0a82b01c0a82bf7" + + // UDP header. + "00430044013d9ac7" + + // BOOTP header. + "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" + + // MAC address. + "30766ff2a90c00000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Options + "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" + + "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff")); + + assertEquals(337, packet.limit()); + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3); + assertTrue(offerPacket instanceof DhcpOfferPacket); // Implicitly checks it's non-null. + DhcpResults dhcpResults = offerPacket.toDhcpResults(); + assertDhcpResults("192.168.43.247/24", "192.168.43.1", "192.168.43.1", + null, "192.168.43.1", "ANDROID_METERED", 3600, true, 0, dhcpResults); + assertTrue(dhcpResults.hasMeteredHint()); + } + + @Test + public void testBadIpPacket() throws Exception { + final byte[] packet = HexDump.hexStringToByteArray( + // IP header. + "450001518d0600004011144dc0a82b01c0a82bf7"); + + try { + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3); + } catch (DhcpPacket.ParseException expected) { + assertDhcpErrorCodes(DhcpErrorEvent.L3_TOO_SHORT, expected.errorCode); + return; + } + fail("Dhcp packet parsing should have failed"); + } + + @Test + public void testBadDhcpPacket() throws Exception { + final byte[] packet = HexDump.hexStringToByteArray( + // IP header. + "450001518d0600004011144dc0a82b01c0a82bf7" + + // UDP header. + "00430044013d9ac7" + + // BOOTP header. + "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000"); + + try { + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3); + } catch (DhcpPacket.ParseException expected) { + assertDhcpErrorCodes(DhcpErrorEvent.L3_TOO_SHORT, expected.errorCode); + return; + } + fail("Dhcp packet parsing should have failed"); + } + + @Test + public void testBadTruncatedOffer() throws Exception { + final byte[] packet = HexDump.hexStringToByteArray( + // IP header. + "450001518d0600004011144dc0a82b01c0a82bf7" + + // UDP header. + "00430044013d9ac7" + + // BOOTP header. + "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" + + // MAC address. + "30766ff2a90c00000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File, missing one byte + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000"); + + try { + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3); + } catch (DhcpPacket.ParseException expected) { + assertDhcpErrorCodes(DhcpErrorEvent.L3_TOO_SHORT, expected.errorCode); + return; + } + fail("Dhcp packet parsing should have failed"); + } + + @Test + public void testBadOfferWithoutACookie() throws Exception { + final byte[] packet = HexDump.hexStringToByteArray( + // IP header. + "450001518d0600004011144dc0a82b01c0a82bf7" + + // UDP header. + "00430044013d9ac7" + + // BOOTP header. + "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" + + // MAC address. + "30766ff2a90c00000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + // No options + ); + + try { + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3); + } catch (DhcpPacket.ParseException expected) { + assertDhcpErrorCodes(DhcpErrorEvent.DHCP_NO_COOKIE, expected.errorCode); + return; + } + fail("Dhcp packet parsing should have failed"); + } + + @Test + public void testOfferWithBadCookie() throws Exception { + final byte[] packet = HexDump.hexStringToByteArray( + // IP header. + "450001518d0600004011144dc0a82b01c0a82bf7" + + // UDP header. + "00430044013d9ac7" + + // BOOTP header. + "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" + + // MAC address. + "30766ff2a90c00000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Bad cookie + "DEADBEEF3501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" + + "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff"); + + try { + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3); + } catch (DhcpPacket.ParseException expected) { + assertDhcpErrorCodes(DhcpErrorEvent.DHCP_BAD_MAGIC_COOKIE, expected.errorCode); + return; + } + fail("Dhcp packet parsing should have failed"); + } + + private void assertDhcpErrorCodes(int expected, int got) { + assertEquals(Integer.toHexString(expected), Integer.toHexString(got)); + } + + @Test + public void testTruncatedOfferPackets() throws Exception { + final byte[] packet = HexDump.hexStringToByteArray( + // IP header. + "450001518d0600004011144dc0a82b01c0a82bf7" + + // UDP header. + "00430044013d9ac7" + + // BOOTP header. + "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" + + // MAC address. + "30766ff2a90c00000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Options + "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" + + "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff"); + + for (int len = 0; len < packet.length; len++) { + try { + DhcpPacket.decodeFullPacket(packet, len, ENCAP_L3); + } catch (ParseException e) { + if (e.errorCode == DhcpErrorEvent.PARSING_ERROR) { + fail(String.format("bad truncated packet of length %d", len)); + } + } + } + } + + @Test + public void testRandomPackets() throws Exception { + final int maxRandomPacketSize = 512; + final Random r = new Random(); + for (int i = 0; i < 10000; i++) { + byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)]; + r.nextBytes(packet); + try { + DhcpPacket.decodeFullPacket(packet, packet.length, ENCAP_L3); + } catch (ParseException e) { + if (e.errorCode == DhcpErrorEvent.PARSING_ERROR) { + fail("bad packet: " + HexDump.toHexString(packet)); + } + } + } + } + + private byte[] mtuBytes(int mtu) { + // 0x1a02: option 26, length 2. 0xff: no more options. + if (mtu > Short.MAX_VALUE - Short.MIN_VALUE) { + throw new IllegalArgumentException( + String.format("Invalid MTU %d, must be 16-bit unsigned", mtu)); + } + String hexString = String.format("1a02%04xff", mtu); + return HexDump.hexStringToByteArray(hexString); + } + + private void checkMtu(ByteBuffer packet, int expectedMtu, byte[] mtuBytes) throws Exception { + if (mtuBytes != null) { + packet.position(packet.capacity() - mtuBytes.length); + packet.put(mtuBytes); + packet.clear(); + } + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3); + assertTrue(offerPacket instanceof DhcpOfferPacket); // Implicitly checks it's non-null. + DhcpResults dhcpResults = offerPacket.toDhcpResults(); + assertDhcpResults("192.168.159.247/20", "192.168.159.254", "8.8.8.8,8.8.4.4", + null, "192.168.144.3", null, 7200, false, expectedMtu, dhcpResults); + } + + @Test + public void testMtu() throws Exception { + final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray( + // IP header. + "451001480000000080118849c0a89003c0a89ff7" + + // UDP header. + "004300440134dcfa" + + // BOOTP header. + "02010600c997a63b0000000000000000c0a89ff70000000000000000" + + // MAC address. + "30766ff2a90c00000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Options + "638253633501023604c0a89003330400001c200104fffff0000304c0a89ffe06080808080808080404" + + "3a0400000e103b040000189cff00000000")); + + checkMtu(packet, 0, null); + checkMtu(packet, 0, mtuBytes(1501)); + checkMtu(packet, 1500, mtuBytes(1500)); + checkMtu(packet, 1499, mtuBytes(1499)); + checkMtu(packet, 1280, mtuBytes(1280)); + checkMtu(packet, 0, mtuBytes(1279)); + checkMtu(packet, 0, mtuBytes(576)); + checkMtu(packet, 0, mtuBytes(68)); + checkMtu(packet, 0, mtuBytes(Short.MIN_VALUE)); + checkMtu(packet, 0, mtuBytes(Short.MAX_VALUE + 3)); + checkMtu(packet, 0, mtuBytes(-1)); + } + + @Test + public void testBadHwaddrLength() throws Exception { + final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray( + // IP header. + "450001518d0600004011144dc0a82b01c0a82bf7" + + // UDP header. + "00430044013d9ac7" + + // BOOTP header. + "02010600dfc23d1f0002000000000000c0a82bf7c0a82b0100000000" + + // MAC address. + "30766ff2a90c00000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Options + "638253633501023604c0a82b01330400000e103a04000007083b0400000c4e0104ffffff00" + + "1c04c0a82bff0304c0a82b010604c0a82b012b0f414e44524f49445f4d455445524544ff")); + String expectedClientMac = "30766FF2A90C"; + + final int hwAddrLenOffset = 20 + 8 + 2; + assertEquals(6, packet.get(hwAddrLenOffset)); + + // Expect the expected. + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3); + assertNotNull(offerPacket); + assertEquals(6, offerPacket.getClientMac().length); + assertEquals(expectedClientMac, HexDump.toHexString(offerPacket.getClientMac())); + + // Reduce the hardware address length and verify that it shortens the client MAC. + packet.flip(); + packet.put(hwAddrLenOffset, (byte) 5); + offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3); + assertNotNull(offerPacket); + assertEquals(5, offerPacket.getClientMac().length); + assertEquals(expectedClientMac.substring(0, 10), + HexDump.toHexString(offerPacket.getClientMac())); + + packet.flip(); + packet.put(hwAddrLenOffset, (byte) 3); + offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3); + assertNotNull(offerPacket); + assertEquals(3, offerPacket.getClientMac().length); + assertEquals(expectedClientMac.substring(0, 6), + HexDump.toHexString(offerPacket.getClientMac())); + + // Set the the hardware address length to 0xff and verify that we a) don't treat it as -1 + // and crash, and b) hardcode it to 6. + packet.flip(); + packet.put(hwAddrLenOffset, (byte) -1); + offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3); + assertNotNull(offerPacket); + assertEquals(6, offerPacket.getClientMac().length); + assertEquals(expectedClientMac, HexDump.toHexString(offerPacket.getClientMac())); + + // Set the the hardware address length to a positive invalid value (> 16) and verify that we + // hardcode it to 6. + packet.flip(); + packet.put(hwAddrLenOffset, (byte) 17); + offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3); + assertNotNull(offerPacket); + assertEquals(6, offerPacket.getClientMac().length); + assertEquals(expectedClientMac, HexDump.toHexString(offerPacket.getClientMac())); + } + + @Test + public void testPadAndOverloadedOptionsOffer() throws Exception { + // A packet observed in the real world that is interesting for two reasons: + // + // 1. It uses pad bytes, which we previously didn't support correctly. + // 2. It uses DHCP option overloading, which we don't currently support (but it doesn't + // store any information in the overloaded fields). + // + // For now, we just check that it parses correctly. + final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray( + // Ethernet header. + "b4cef6000000e80462236e300800" + + // IP header. + "4500014c00000000ff11741701010101ac119876" + + // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation). + "004300440138ae5a" + + // BOOTP header. + "020106000fa0059f0000000000000000ac1198760000000000000000" + + // MAC address. + "b4cef600000000000000000000000000" + + // Server name. + "ff00000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "ff00000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Options + "638253633501023604010101010104ffff000033040000a8c03401030304ac1101010604ac110101" + + "0000000000000000000000000000000000000000000000ff000000")); + + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2); + assertTrue(offerPacket instanceof DhcpOfferPacket); + DhcpResults dhcpResults = offerPacket.toDhcpResults(); + assertDhcpResults("172.17.152.118/16", "172.17.1.1", "172.17.1.1", + null, "1.1.1.1", null, 43200, false, 0, dhcpResults); + } + + @Test + public void testBug2111() throws Exception { + final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray( + // IP header. + "4500014c00000000ff119beac3eaf3880a3f5d04" + + // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation). + "0043004401387464" + + // BOOTP header. + "0201060002554812000a0000000000000a3f5d040000000000000000" + + // MAC address. + "00904c00000000000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Options. + "638253633501023604c00002fe33040000bfc60104fffff00003040a3f50010608c0000201c0000202" + + "0f0f646f6d61696e3132332e636f2e756b0000000000ff00000000")); + + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3); + assertTrue(offerPacket instanceof DhcpOfferPacket); + DhcpResults dhcpResults = offerPacket.toDhcpResults(); + assertDhcpResults("10.63.93.4/20", "10.63.80.1", "192.0.2.1,192.0.2.2", + "domain123.co.uk", "192.0.2.254", null, 49094, false, 0, dhcpResults); + } + + @Test + public void testBug2136() throws Exception { + final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray( + // Ethernet header. + "bcf5ac000000d0c7890000000800" + + // IP header. + "4500014c00000000ff119beac3eaf3880a3f5d04" + + // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation). + "0043004401387574" + + // BOOTP header. + "0201060163339a3000050000000000000a209ecd0000000000000000" + + // MAC address. + "bcf5ac00000000000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Options. + "6382536335010236040a20ff80330400001c200104fffff00003040a20900106089458413494584135" + + "0f0b6c616e63732e61632e756b000000000000000000ff00000000")); + + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2); + assertTrue(offerPacket instanceof DhcpOfferPacket); + assertEquals("BCF5AC000000", HexDump.toHexString(offerPacket.getClientMac())); + DhcpResults dhcpResults = offerPacket.toDhcpResults(); + assertDhcpResults("10.32.158.205/20", "10.32.144.1", "148.88.65.52,148.88.65.53", + "lancs.ac.uk", "10.32.255.128", null, 7200, false, 0, dhcpResults); + } + + @Test + public void testUdpServerAnySourcePort() throws Exception { + final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray( + // Ethernet header. + "9cd917000000001c2e0000000800" + + // IP header. + "45a00148000040003d115087d18194fb0a0f7af2" + + // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation). + // NOTE: The server source port is not the canonical port 67. + "C29F004401341268" + + // BOOTP header. + "02010600d628ba8200000000000000000a0f7af2000000000a0fc818" + + // MAC address. + "9cd91700000000000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Options. + "6382536335010236040a0169fc3304000151800104ffff000003040a0fc817060cd1818003d1819403" + + "d18180060f0777766d2e6564751c040a0fffffff000000")); + + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2); + assertTrue(offerPacket instanceof DhcpOfferPacket); + assertEquals("9CD917000000", HexDump.toHexString(offerPacket.getClientMac())); + DhcpResults dhcpResults = offerPacket.toDhcpResults(); + assertDhcpResults("10.15.122.242/16", "10.15.200.23", + "209.129.128.3,209.129.148.3,209.129.128.6", + "wvm.edu", "10.1.105.252", null, 86400, false, 0, dhcpResults); + } + + @Test + public void testUdpInvalidDstPort() throws Exception { + final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray( + // Ethernet header. + "9cd917000000001c2e0000000800" + + // IP header. + "45a00148000040003d115087d18194fb0a0f7af2" + + // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation). + // NOTE: The destination port is a non-DHCP port. + "0043aaaa01341268" + + // BOOTP header. + "02010600d628ba8200000000000000000a0f7af2000000000a0fc818" + + // MAC address. + "9cd91700000000000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Options. + "6382536335010236040a0169fc3304000151800104ffff000003040a0fc817060cd1818003d1819403" + + "d18180060f0777766d2e6564751c040a0fffffff000000")); + + try { + DhcpPacket.decodeFullPacket(packet, ENCAP_L2); + fail("Packet with invalid dst port did not throw ParseException"); + } catch (ParseException expected) {} + } + + @Test + public void testMultipleRouters() throws Exception { + final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray( + // Ethernet header. + "fc3d93000000" + "081735000000" + "0800" + + // IP header. + "45000148c2370000ff117ac2c0a8bd02ffffffff" + + // UDP header. TODO: fix invalid checksum (due to MAC address obfuscation). + "0043004401343beb" + + // BOOTP header. + "0201060027f518e20000800000000000c0a8bd310000000000000000" + + // MAC address. + "fc3d9300000000000000000000000000" + + // Server name. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // File. + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + // Options. + "638253633501023604c0abbd023304000070803a04000038403b04000062700104ffffff00" + + "0308c0a8bd01ffffff0006080808080808080404ff000000000000")); + + DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L2); + assertTrue(offerPacket instanceof DhcpOfferPacket); + assertEquals("FC3D93000000", HexDump.toHexString(offerPacket.getClientMac())); + DhcpResults dhcpResults = offerPacket.toDhcpResults(); + assertDhcpResults("192.168.189.49/24", "192.168.189.1", "8.8.8.8,8.8.4.4", + null, "192.171.189.2", null, 28800, false, 0, dhcpResults); + } + + @Test + public void testDiscoverPacket() throws Exception { + short secs = 7; + int transactionId = 0xdeadbeef; + byte[] hwaddr = { + (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a + }; + + ByteBuffer packet = DhcpPacket.buildDiscoverPacket( + DhcpPacket.ENCAP_L2, transactionId, secs, hwaddr, + false /* do unicast */, DhcpClient.REQUESTED_PARAMS); + + byte[] headers = new byte[] { + // Ethernet header. + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a, + (byte) 0x08, (byte) 0x00, + // IP header. + (byte) 0x45, (byte) 0x10, (byte) 0x01, (byte) 0x56, + (byte) 0x00, (byte) 0x00, (byte) 0x40, (byte) 0x00, + (byte) 0x40, (byte) 0x11, (byte) 0x39, (byte) 0x88, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // UDP header. + (byte) 0x00, (byte) 0x44, (byte) 0x00, (byte) 0x43, + (byte) 0x01, (byte) 0x42, (byte) 0x6a, (byte) 0x4a, + // BOOTP. + (byte) 0x01, (byte) 0x01, (byte) 0x06, (byte) 0x00, + (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef, + (byte) 0x00, (byte) 0x07, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, + (byte) 0xb1, (byte) 0x7a + }; + byte[] options = new byte[] { + // Magic cookie 0x63825363. + (byte) 0x63, (byte) 0x82, (byte) 0x53, (byte) 0x63, + // Message type DISCOVER. + (byte) 0x35, (byte) 0x01, (byte) 0x01, + // Client identifier Ethernet, da:01:19:5b:b1:7a. + (byte) 0x3d, (byte) 0x07, + (byte) 0x01, + (byte) 0xda, (byte) 0x01, (byte) 0x19, (byte) 0x5b, (byte) 0xb1, (byte) 0x7a, + // Max message size 1500. + (byte) 0x39, (byte) 0x02, (byte) 0x05, (byte) 0xdc, + // Version "android-dhcp-???". + (byte) 0x3c, (byte) 0x10, + 'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'd', 'h', 'c', 'p', '-', '?', '?', '?', + // Hostname "android-01234567890abcde" + (byte) 0x0c, (byte) 0x18, + 'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', + // Requested parameter list. + (byte) 0x37, (byte) 0x0a, + DHCP_SUBNET_MASK, + DHCP_ROUTER, + DHCP_DNS_SERVER, + DHCP_DOMAIN_NAME, + DHCP_MTU, + DHCP_BROADCAST_ADDRESS, + DHCP_LEASE_TIME, + DHCP_RENEWAL_TIME, + DHCP_REBINDING_TIME, + DHCP_VENDOR_INFO, + // End options. + (byte) 0xff, + // Our packets are always of even length. TODO: find out why and possibly fix it. + (byte) 0x00 + }; + byte[] expected = new byte[DhcpPacket.MIN_PACKET_LENGTH_L2 + options.length]; + assertTrue((expected.length & 1) == 0); + System.arraycopy(headers, 0, expected, 0, headers.length); + System.arraycopy(options, 0, expected, DhcpPacket.MIN_PACKET_LENGTH_L2, options.length); + + byte[] actual = new byte[packet.limit()]; + packet.get(actual); + String msg = + "Expected:\n " + Arrays.toString(expected) + + "\nActual:\n " + Arrays.toString(actual); + assertTrue(msg, Arrays.equals(expected, actual)); + } + + public void checkBuildOfferPacket(int leaseTimeSecs, @Nullable String hostname) + throws Exception { + final int renewalTime = (int) (Integer.toUnsignedLong(leaseTimeSecs) / 2); + final int rebindingTime = (int) (Integer.toUnsignedLong(leaseTimeSecs) * 875 / 1000); + final int transactionId = 0xdeadbeef; + + final ByteBuffer packet = DhcpPacket.buildOfferPacket( + DhcpPacket.ENCAP_BOOTP, transactionId, false /* broadcast */, + SERVER_ADDR, INADDR_ANY /* relayIp */, CLIENT_ADDR /* yourIp */, + CLIENT_MAC, leaseTimeSecs, NETMASK /* netMask */, + BROADCAST_ADDR /* bcAddr */, Collections.singletonList(SERVER_ADDR) /* gateways */, + Collections.singletonList(SERVER_ADDR) /* dnsServers */, + SERVER_ADDR /* dhcpServerIdentifier */, null /* domainName */, hostname, + false /* metered */, MTU); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + // BOOTP headers + bos.write(new byte[] { + (byte) 0x02, (byte) 0x01, (byte) 0x06, (byte) 0x00, + (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // ciaddr + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + }); + // yiaddr + bos.write(CLIENT_ADDR.getAddress()); + // siaddr + bos.write(SERVER_ADDR.getAddress()); + // giaddr + bos.write(INADDR_ANY.getAddress()); + // chaddr + bos.write(CLIENT_MAC); + + // Padding + bos.write(new byte[202]); + + // Options + bos.write(new byte[]{ + // Magic cookie 0x63825363. + (byte) 0x63, (byte) 0x82, (byte) 0x53, (byte) 0x63, + // Message type OFFER. + (byte) 0x35, (byte) 0x01, (byte) 0x02, + }); + // Server ID + bos.write(new byte[] { (byte) 0x36, (byte) 0x04 }); + bos.write(SERVER_ADDR.getAddress()); + // Lease time + bos.write(new byte[] { (byte) 0x33, (byte) 0x04 }); + bos.write(intToByteArray(leaseTimeSecs)); + if (leaseTimeSecs != INFINITE_LEASE) { + // Renewal time + bos.write(new byte[]{(byte) 0x3a, (byte) 0x04}); + bos.write(intToByteArray(renewalTime)); + // Rebinding time + bos.write(new byte[]{(byte) 0x3b, (byte) 0x04}); + bos.write(intToByteArray(rebindingTime)); + } + // Subnet mask + bos.write(new byte[] { (byte) 0x01, (byte) 0x04 }); + bos.write(NETMASK.getAddress()); + // Broadcast address + bos.write(new byte[] { (byte) 0x1c, (byte) 0x04 }); + bos.write(BROADCAST_ADDR.getAddress()); + // Router + bos.write(new byte[] { (byte) 0x03, (byte) 0x04 }); + bos.write(SERVER_ADDR.getAddress()); + // Nameserver + bos.write(new byte[] { (byte) 0x06, (byte) 0x04 }); + bos.write(SERVER_ADDR.getAddress()); + // Hostname + if (hostname != null) { + bos.write(new byte[]{(byte) 0x0c, (byte) hostname.length()}); + bos.write(hostname.getBytes(Charset.forName("US-ASCII"))); + } + // MTU + bos.write(new byte[] { (byte) 0x1a, (byte) 0x02 }); + bos.write(shortToByteArray(MTU)); + // End options. + bos.write(0xff); + + if ((bos.size() & 1) != 0) { + bos.write(0x00); + } + + final byte[] expected = bos.toByteArray(); + final byte[] actual = new byte[packet.limit()]; + packet.get(actual); + final String msg = "Expected:\n " + HexDump.dumpHexString(expected) + + "\nActual:\n " + HexDump.dumpHexString(actual); + assertTrue(msg, Arrays.equals(expected, actual)); + } + + @Test + public void testOfferPacket() throws Exception { + checkBuildOfferPacket(3600, HOSTNAME); + checkBuildOfferPacket(Integer.MAX_VALUE, HOSTNAME); + checkBuildOfferPacket(0x80000000, HOSTNAME); + checkBuildOfferPacket(INFINITE_LEASE, HOSTNAME); + checkBuildOfferPacket(3600, null); + } + + private static byte[] intToByteArray(int val) { + return ByteBuffer.allocate(4).putInt(val).array(); + } + + private static byte[] shortToByteArray(short val) { + return ByteBuffer.allocate(2).putShort(val).array(); + } +} diff --git a/tests/src/android/net/ip/IpClientTest.java b/tests/src/android/net/ip/IpClientTest.java new file mode 100644 index 0000000..f21809f --- /dev/null +++ b/tests/src/android/net/ip/IpClientTest.java @@ -0,0 +1,533 @@ +/* + * 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.LinkPropertiesParcelableUtil.fromStableParcelable; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AlarmManager; +import android.content.Context; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.INetd; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.MacAddress; +import android.net.RouteInfo; +import android.net.shared.InitialConfiguration; +import android.net.shared.ProvisioningConfiguration; +import android.net.util.InterfaceParams; +import android.os.INetworkManagementService; +import android.provider.Settings; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.test.mock.MockContentResolver; + +import com.android.internal.R; +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.server.net.BaseNetworkObserver; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.InetAddress; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Tests for IpClient. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpClientTest { + private static final int DEFAULT_AVOIDBADWIFI_CONFIG_VALUE = 1; + + private static final String VALID = "VALID"; + private static final String INVALID = "INVALID"; + private static final String TEST_IFNAME = "test_wlan0"; + private static final int TEST_IFINDEX = 1001; + // See RFC 7042#section-2.1.2 for EUI-48 documentation values. + private static final MacAddress TEST_MAC = MacAddress.fromString("00:00:5E:00:53:01"); + private static final int TEST_TIMEOUT_MS = 400; + + @Mock private Context mContext; + @Mock private ConnectivityManager mCm; + @Mock private INetworkManagementService mNMService; + @Mock private INetd mNetd; + @Mock private Resources mResources; + @Mock private IIpClientCallbacks mCb; + @Mock private AlarmManager mAlarm; + @Mock private IpClient.Dependencies mDependecies; + private MockContentResolver mContentResolver; + + private BaseNetworkObserver mObserver; + private InterfaceParams mIfParams; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + when(mContext.getSystemService(eq(Context.ALARM_SERVICE))).thenReturn(mAlarm); + when(mContext.getSystemServiceName(ConnectivityManager.class)) + .thenReturn(Context.CONNECTIVITY_SERVICE); + when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mCm); + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getInteger(R.integer.config_networkAvoidBadWifi)) + .thenReturn(DEFAULT_AVOIDBADWIFI_CONFIG_VALUE); + + mContentResolver = new MockContentResolver(); + mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + + mIfParams = null; + + when(mDependecies.getNMS()).thenReturn(mNMService); + when(mDependecies.getNetd()).thenReturn(mNetd); + } + + private void setTestInterfaceParams(String ifname) { + mIfParams = (ifname != null) + ? new InterfaceParams(ifname, TEST_IFINDEX, TEST_MAC) + : null; + when(mDependecies.getInterfaceParams(anyString())).thenReturn(mIfParams); + } + + private IpClient makeIpClient(String ifname) throws Exception { + setTestInterfaceParams(ifname); + final IpClient ipc = new IpClient(mContext, ifname, mCb, mDependecies); + verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceSetEnableIPv6(ifname, false); + verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceClearAddrs(ifname); + ArgumentCaptor<BaseNetworkObserver> arg = + ArgumentCaptor.forClass(BaseNetworkObserver.class); + verify(mNMService, times(1)).registerObserver(arg.capture()); + mObserver = arg.getValue(); + reset(mNMService); + reset(mNetd); + // Verify IpClient doesn't call onLinkPropertiesChange() when it starts. + verify(mCb, never()).onLinkPropertiesChange(any()); + reset(mCb); + return ipc; + } + + private static LinkProperties makeEmptyLinkProperties(String iface) { + final LinkProperties empty = new LinkProperties(); + empty.setInterfaceName(iface); + return empty; + } + + @Test + public void testNullInterfaceNameMostDefinitelyThrows() throws Exception { + setTestInterfaceParams(null); + try { + final IpClient ipc = new IpClient(mContext, null, mCb, mDependecies); + ipc.shutdown(); + fail(); + } catch (NullPointerException npe) { + // Phew; null interface names not allowed. + } + } + + @Test + public void testNullCallbackMostDefinitelyThrows() throws Exception { + final String ifname = "lo"; + setTestInterfaceParams(ifname); + try { + final IpClient ipc = new IpClient(mContext, ifname, null, mDependecies); + ipc.shutdown(); + fail(); + } catch (NullPointerException npe) { + // Phew; null callbacks not allowed. + } + } + + @Test + public void testInvalidInterfaceDoesNotThrow() throws Exception { + setTestInterfaceParams(TEST_IFNAME); + final IpClient ipc = new IpClient(mContext, TEST_IFNAME, mCb, mDependecies); + ipc.shutdown(); + } + + @Test + public void testInterfaceNotFoundFailsImmediately() throws Exception { + setTestInterfaceParams(null); + final IpClient ipc = new IpClient(mContext, TEST_IFNAME, mCb, mDependecies); + ipc.startProvisioning(new ProvisioningConfiguration()); + verify(mCb, times(1)).onProvisioningFailure(any()); + ipc.shutdown(); + } + + @Test + public void testDefaultProvisioningConfiguration() throws Exception { + final String iface = TEST_IFNAME; + final IpClient ipc = makeIpClient(iface); + + ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() + .withoutIPv4() + // TODO: mock IpReachabilityMonitor's dependencies (NetworkInterface, PowerManager) + // and enable it in this test + .withoutIpReachabilityMonitor() + .build(); + + ipc.startProvisioning(config); + verify(mCb, times(1)).setNeighborDiscoveryOffload(true); + verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setFallbackMulticastFilter(false); + verify(mCb, never()).onProvisioningFailure(any()); + + ipc.shutdown(); + verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceSetEnableIPv6(iface, false); + verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceClearAddrs(iface); + verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)) + .onLinkPropertiesChange(argThat( + lp -> fromStableParcelable(lp).equals(makeEmptyLinkProperties(iface)))); + } + + @Test + public void testProvisioningWithInitialConfiguration() throws Exception { + final String iface = TEST_IFNAME; + final IpClient ipc = makeIpClient(iface); + + String[] addresses = { + "fe80::a4be:f92:e1f7:22d1/64", + "fe80::f04a:8f6:6a32:d756/64", + "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64" + }; + String[] prefixes = { "fe80::/64", "fd2c:4e57:8e3c::/64" }; + + ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() + .withoutIPv4() + .withoutIpReachabilityMonitor() + .withInitialConfiguration(conf(links(addresses), prefixes(prefixes), ips())) + .build(); + + ipc.startProvisioning(config); + verify(mCb, times(1)).setNeighborDiscoveryOffload(true); + verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setFallbackMulticastFilter(false); + verify(mCb, never()).onProvisioningFailure(any()); + + for (String addr : addresses) { + String[] parts = addr.split("/"); + verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)) + .interfaceAddAddress(iface, parts[0], Integer.parseInt(parts[1])); + } + + final int lastAddr = addresses.length - 1; + + // Add N - 1 addresses + for (int i = 0; i < lastAddr; i++) { + mObserver.addressUpdated(iface, new LinkAddress(addresses[i])); + verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(any()); + reset(mCb); + } + + // Add Nth address + mObserver.addressUpdated(iface, new LinkAddress(addresses[lastAddr])); + LinkProperties want = linkproperties(links(addresses), routes(prefixes)); + want.setInterfaceName(iface); + verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).onProvisioningSuccess(argThat( + lp -> fromStableParcelable(lp).equals(want))); + + ipc.shutdown(); + verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceSetEnableIPv6(iface, false); + verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceClearAddrs(iface); + verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)) + .onLinkPropertiesChange(argThat( + lp -> fromStableParcelable(lp).equals(makeEmptyLinkProperties(iface)))); + } + + @Test + public void testIsProvisioned() throws Exception { + InitialConfiguration empty = conf(links(), prefixes()); + IsProvisionedTestCase[] testcases = { + // nothing + notProvisionedCase(links(), routes(), dns(), null), + notProvisionedCase(links(), routes(), dns(), empty), + + // IPv4 + provisionedCase(links("192.0.2.12/24"), routes(), dns(), empty), + + // IPv6 + notProvisionedCase( + links("fe80::a4be:f92:e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"), + routes(), dns(), empty), + notProvisionedCase( + links("fe80::a4be:f92:e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"), + routes("fe80::/64", "fd2c:4e57:8e3c::/64"), dns("fd00:1234:5678::1000"), empty), + provisionedCase( + links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"), + routes("::/0"), + dns("2001:db8:dead:beef:f00::02"), empty), + + // Initial configuration + provisionedCase( + links("fe80::e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"), + routes("fe80::/64", "fd2c:4e57:8e3c::/64"), + dns(), + conf(links("fe80::e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"), + prefixes( "fe80::/64", "fd2c:4e57:8e3c::/64"), ips())) + }; + + for (IsProvisionedTestCase testcase : testcases) { + if (IpClient.isProvisioned(testcase.lp, testcase.config) != testcase.isProvisioned) { + fail(testcase.errorMessage()); + } + } + } + + static class IsProvisionedTestCase { + boolean isProvisioned; + LinkProperties lp; + InitialConfiguration config; + + String errorMessage() { + return String.format("expected %s with config %s to be %s, but was %s", + lp, config, provisioned(isProvisioned), provisioned(!isProvisioned)); + } + + static String provisioned(boolean isProvisioned) { + return isProvisioned ? "provisioned" : "not provisioned"; + } + } + + static IsProvisionedTestCase provisionedCase(Set<LinkAddress> lpAddrs, Set<RouteInfo> lpRoutes, + Set<InetAddress> lpDns, InitialConfiguration config) { + return provisioningTest(true, lpAddrs, lpRoutes, lpDns, config); + } + + static IsProvisionedTestCase notProvisionedCase(Set<LinkAddress> lpAddrs, + Set<RouteInfo> lpRoutes, Set<InetAddress> lpDns, InitialConfiguration config) { + return provisioningTest(false, lpAddrs, lpRoutes, lpDns, config); + } + + static IsProvisionedTestCase provisioningTest(boolean isProvisioned, Set<LinkAddress> lpAddrs, + Set<RouteInfo> lpRoutes, Set<InetAddress> lpDns, InitialConfiguration config) { + IsProvisionedTestCase testcase = new IsProvisionedTestCase(); + testcase.isProvisioned = isProvisioned; + testcase.lp = new LinkProperties(); + testcase.lp.setLinkAddresses(lpAddrs); + for (RouteInfo route : lpRoutes) { + testcase.lp.addRoute(route); + } + for (InetAddress dns : lpDns) { + testcase.lp.addDnsServer(dns); + } + testcase.config = config; + return testcase; + } + + @Test + public void testInitialConfigurations() throws Exception { + InitialConfigurationTestCase[] testcases = { + validConf("valid IPv4 configuration", + links("192.0.2.12/24"), prefixes("192.0.2.0/24"), dns("192.0.2.2")), + validConf("another valid IPv4 configuration", + links("192.0.2.12/24"), prefixes("192.0.2.0/24"), dns()), + validConf("valid IPv6 configurations", + links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"), + prefixes("2001:db8:dead:beef::/64", "fe80::/64"), + dns("2001:db8:dead:beef:f00::02")), + validConf("valid IPv6 configurations", + links("fe80::1/64"), prefixes("fe80::/64"), dns()), + validConf("valid IPv6/v4 configuration", + links("2001:db8:dead:beef:f00::a0/48", "192.0.2.12/24"), + prefixes("2001:db8:dead:beef::/64", "192.0.2.0/24"), + dns("192.0.2.2", "2001:db8:dead:beef:f00::02")), + validConf("valid IPv6 configuration without any GUA.", + links("fd00:1234:5678::1/48"), + prefixes("fd00:1234:5678::/48"), + dns("fd00:1234:5678::1000")), + + invalidConf("empty configuration", links(), prefixes(), dns()), + invalidConf("v4 addr and dns not in any prefix", + links("192.0.2.12/24"), prefixes("198.51.100.0/24"), dns("192.0.2.2")), + invalidConf("v4 addr not in any prefix", + links("198.51.2.12/24"), prefixes("198.51.100.0/24"), dns("192.0.2.2")), + invalidConf("v4 dns addr not in any prefix", + links("192.0.2.12/24"), prefixes("192.0.2.0/24"), dns("198.51.100.2")), + invalidConf("v6 addr not in any prefix", + links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"), + prefixes("2001:db8:dead:beef::/64"), + dns("2001:db8:dead:beef:f00::02")), + invalidConf("v6 dns addr not in any prefix", + links("fe80::1/64"), prefixes("fe80::/64"), dns("2001:db8:dead:beef:f00::02")), + invalidConf("default ipv6 route and no GUA", + links("fd01:1111:2222:3333::a0/128"), prefixes("::/0"), dns()), + invalidConf("invalid v6 prefix length", + links("2001:db8:dead:beef:f00::a0/128"), prefixes("2001:db8:dead:beef::/32"), + dns()), + invalidConf("another invalid v6 prefix length", + links("2001:db8:dead:beef:f00::a0/128"), prefixes("2001:db8:dead:beef::/72"), + dns()) + }; + + for (InitialConfigurationTestCase testcase : testcases) { + if (testcase.config.isValid() != testcase.isValid) { + fail(testcase.errorMessage()); + } + } + } + + static class InitialConfigurationTestCase { + String descr; + boolean isValid; + InitialConfiguration config; + public String errorMessage() { + return String.format("%s: expected configuration %s to be %s, but was %s", + descr, config, validString(isValid), validString(!isValid)); + } + static String validString(boolean isValid) { + return isValid ? VALID : INVALID; + } + } + + static InitialConfigurationTestCase validConf(String descr, Set<LinkAddress> links, + Set<IpPrefix> prefixes, Set<InetAddress> dns) { + return confTestCase(descr, true, conf(links, prefixes, dns)); + } + + static InitialConfigurationTestCase invalidConf(String descr, Set<LinkAddress> links, + Set<IpPrefix> prefixes, Set<InetAddress> dns) { + return confTestCase(descr, false, conf(links, prefixes, dns)); + } + + static InitialConfigurationTestCase confTestCase( + String descr, boolean isValid, InitialConfiguration config) { + InitialConfigurationTestCase testcase = new InitialConfigurationTestCase(); + testcase.descr = descr; + testcase.isValid = isValid; + testcase.config = config; + return testcase; + } + + static LinkProperties linkproperties(Set<LinkAddress> addresses, Set<RouteInfo> routes) { + LinkProperties lp = new LinkProperties(); + lp.setLinkAddresses(addresses); + for (RouteInfo route : routes) { + lp.addRoute(route); + } + return lp; + } + + static InitialConfiguration conf(Set<LinkAddress> links, Set<IpPrefix> prefixes) { + return conf(links, prefixes, new HashSet<>()); + } + + static InitialConfiguration conf( + Set<LinkAddress> links, Set<IpPrefix> prefixes, Set<InetAddress> dns) { + InitialConfiguration conf = new InitialConfiguration(); + conf.ipAddresses.addAll(links); + conf.directlyConnectedRoutes.addAll(prefixes); + conf.dnsServers.addAll(dns); + return conf; + } + + static Set<RouteInfo> routes(String... routes) { + return mapIntoSet(routes, (r) -> new RouteInfo(new IpPrefix(r))); + } + + static Set<IpPrefix> prefixes(String... prefixes) { + return mapIntoSet(prefixes, IpPrefix::new); + } + + static Set<LinkAddress> links(String... addresses) { + return mapIntoSet(addresses, LinkAddress::new); + } + + static Set<InetAddress> ips(String... addresses) { + return mapIntoSet(addresses, InetAddress::getByName); + } + + static Set<InetAddress> dns(String... addresses) { + return ips(addresses); + } + + static <A, B> Set<B> mapIntoSet(A[] in, Fn<A, B> fn) { + Set<B> out = new HashSet<>(in.length); + for (A item : in) { + try { + out.add(fn.call(item)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return out; + } + + interface Fn<A,B> { + B call(A a) throws Exception; + } + + @Test + public void testAll() { + List<String> list1 = Arrays.asList(); + List<String> list2 = Arrays.asList("foo"); + List<String> list3 = Arrays.asList("bar", "baz"); + List<String> list4 = Arrays.asList("foo", "bar", "baz"); + + assertTrue(InitialConfiguration.all(list1, (x) -> false)); + assertFalse(InitialConfiguration.all(list2, (x) -> false)); + assertTrue(InitialConfiguration.all(list3, (x) -> true)); + assertTrue(InitialConfiguration.all(list2, (x) -> x.charAt(0) == 'f')); + assertFalse(InitialConfiguration.all(list4, (x) -> x.charAt(0) == 'f')); + } + + @Test + public void testAny() { + List<String> list1 = Arrays.asList(); + List<String> list2 = Arrays.asList("foo"); + List<String> list3 = Arrays.asList("bar", "baz"); + List<String> list4 = Arrays.asList("foo", "bar", "baz"); + + assertFalse(InitialConfiguration.any(list1, (x) -> true)); + assertTrue(InitialConfiguration.any(list2, (x) -> true)); + assertTrue(InitialConfiguration.any(list2, (x) -> x.charAt(0) == 'f')); + assertFalse(InitialConfiguration.any(list3, (x) -> x.charAt(0) == 'f')); + assertTrue(InitialConfiguration.any(list4, (x) -> x.charAt(0) == 'f')); + } + + @Test + public void testFindAll() { + List<String> list1 = Arrays.asList(); + List<String> list2 = Arrays.asList("foo"); + List<String> list3 = Arrays.asList("foo", "bar", "baz"); + + assertEquals(list1, IpClient.findAll(list1, (x) -> true)); + assertEquals(list1, IpClient.findAll(list3, (x) -> false)); + assertEquals(list3, IpClient.findAll(list3, (x) -> true)); + assertEquals(list2, IpClient.findAll(list3, (x) -> x.charAt(0) == 'f')); + } +} diff --git a/tests/src/android/net/ip/IpReachabilityMonitorTest.java b/tests/src/android/net/ip/IpReachabilityMonitorTest.java new file mode 100644 index 0000000..e3b5ddf --- /dev/null +++ b/tests/src/android/net/ip/IpReachabilityMonitorTest.java @@ -0,0 +1,67 @@ +/* + * 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 org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.net.util.InterfaceParams; +import android.net.util.SharedLog; +import android.os.Handler; +import android.os.Looper; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + + +/** + * Tests for IpReachabilityMonitor. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpReachabilityMonitorTest { + + @Mock IpReachabilityMonitor.Callback mCallback; + @Mock IpReachabilityMonitor.Dependencies mDependencies; + @Mock SharedLog mLog; + @Mock Context mContext; + Handler mHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mLog.forSubComponent(anyString())).thenReturn(mLog); + mHandler = new Handler(Looper.getMainLooper()); + } + + IpReachabilityMonitor makeMonitor() { + final InterfaceParams ifParams = new InterfaceParams("fake0", 1, null); + return new IpReachabilityMonitor( + mContext, ifParams, mHandler, mLog, mCallback, false, mDependencies); + } + + @Test + public void testNothing() { + IpReachabilityMonitor monitor = makeMonitor(); + } +} diff --git a/tests/src/android/net/util/ConnectivityPacketSummaryTest.java b/tests/src/android/net/util/ConnectivityPacketSummaryTest.java new file mode 100644 index 0000000..dfaf52a --- /dev/null +++ b/tests/src/android/net/util/ConnectivityPacketSummaryTest.java @@ -0,0 +1,418 @@ +/* + * 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.net.MacAddress; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import libcore.util.HexEncoding; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for ConnectivityPacketSummary. + * + * @hide + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ConnectivityPacketSummaryTest { + private static final MacAddress MYHWADDR = MacAddress.fromString("80:7a:bf:6f:48:f3"); + + private String getSummary(String hexBytes) { + hexBytes = hexBytes.replaceAll("\\s+", ""); + final byte[] bytes = HexEncoding.decode(hexBytes.toCharArray(), false); + return ConnectivityPacketSummary.summarize(MYHWADDR, bytes); + } + + @Test + public void testParseICMPv6DADProbe() { + final String packet = + // Ethernet + "3333FF6F48F3 807ABF6F48F3 86DD" + + // IPv6 + "600000000018 3A FF" + + "00000000000000000000000000000000" + + "FF0200000000000000000001FF6F48F3" + + // ICMPv6 + "87 00 A8E7" + + "00000000" + + "FE80000000000000827ABFFFFE6F48F3"; + + final String expected = + "TX 80:7a:bf:6f:48:f3 > 33:33:ff:6f:48:f3 ipv6" + + " :: > ff02::1:ff6f:48f3 icmp6" + + " ns fe80::827a:bfff:fe6f:48f3"; + + assertEquals(expected, getSummary(packet)); + } + + @Test + public void testParseICMPv6RS() { + final String packet = + // Ethernet + "333300000002 807ABF6F48F3 86DD" + + // IPv6 + "600000000010 3A FF" + + "FE80000000000000827ABFFFFE6F48F3" + + "FF020000000000000000000000000002" + + // ICMPv6 RS + "85 00 6973" + + "00000000" + + "01 01 807ABF6F48F3"; + + final String expected = + "TX 80:7a:bf:6f:48:f3 > 33:33:00:00:00:02 ipv6" + + " fe80::827a:bfff:fe6f:48f3 > ff02::2 icmp6" + + " rs slla 80:7a:bf:6f:48:f3"; + + assertEquals(expected, getSummary(packet)); + } + + @Test + public void testParseICMPv6RA() { + final String packet = + // Ethernet + "807ABF6F48F3 100E7E263FC1 86DD" + + // IPv6 + "600000000068 3A FF" + + "FE80000000000000FA000004FD000001" + + "FE80000000000000827ABFFFFE6F48F3" + + // ICMPv6 RA + "86 00 8141" + + "40 00 0E10" + + "00000000" + + "00000000" + + "01 01 00005E000265" + + "05 01 0000000005DC" + + "19 05 000000000E10" + + " 20014860486000000000000000008844" + + " 20014860486000000000000000008888" + + "03 04 40 C0" + + " 00278D00" + + " 00093A80" + + " 00000000" + + " 2401FA000004FD000000000000000000"; + + final String expected = + "RX 10:0e:7e:26:3f:c1 > 80:7a:bf:6f:48:f3 ipv6" + + " fe80::fa00:4:fd00:1 > fe80::827a:bfff:fe6f:48f3 icmp6" + + " ra slla 00:00:5e:00:02:65 mtu 1500"; + + assertEquals(expected, getSummary(packet)); + } + + @Test + public void testParseICMPv6NS() { + final String packet = + // Ethernet + "807ABF6F48F3 100E7E263FC1 86DD" + + // IPv6 + "6C0000000020 3A FF" + + "FE80000000000000FA000004FD000001" + + "FF0200000000000000000001FF01C146" + + // ICMPv6 NS + "87 00 8AD4" + + "00000000" + + "2401FA000004FD0015EA6A5C7B01C146" + + "01 01 00005E000265"; + + final String expected = + "RX 10:0e:7e:26:3f:c1 > 80:7a:bf:6f:48:f3 ipv6" + + " fe80::fa00:4:fd00:1 > ff02::1:ff01:c146 icmp6" + + " ns 2401:fa00:4:fd00:15ea:6a5c:7b01:c146 slla 00:00:5e:00:02:65"; + + assertEquals(expected, getSummary(packet)); + } + + @Test + public void testInvalidICMPv6NDLength() { + final String packet = + // Ethernet + "807ABF6F48F3 100E7E263FC1 86DD" + + // IPv6 + "600000000068 3A FF" + + "FE80000000000000FA000004FD000001" + + "FE80000000000000827ABFFFFE6F48F3" + + // ICMPv6 RA + "86 00 8141" + + "40 00 0E10" + + "00000000" + + "00000000" + + "01 01 00005E000265" + + "00 00 0102030405D6"; + + final String expected = + "RX 10:0e:7e:26:3f:c1 > 80:7a:bf:6f:48:f3 ipv6" + + " fe80::fa00:4:fd00:1 > fe80::827a:bfff:fe6f:48f3 icmp6" + + " ra slla 00:00:5e:00:02:65 <malformed>"; + + assertEquals(expected, getSummary(packet)); + } + + @Test + public void testParseICMPv6NA() { + final String packet = + // Ethernet + "00005E000265 807ABF6F48F3 86DD" + + "600000000020 3A FF" + + "2401FA000004FD0015EA6A5C7B01C146" + + "FE80000000000000FA000004FD000001" + + "88 00 E8126" + + "0000000" + + "2401FA000004FD0015EA6A5C7B01C146" + + "02 01 807ABF6F48F3"; + + final String expected = + "TX 80:7a:bf:6f:48:f3 > 00:00:5e:00:02:65 ipv6" + + " 2401:fa00:4:fd00:15ea:6a5c:7b01:c146 > fe80::fa00:4:fd00:1 icmp6" + + " na 2401:fa00:4:fd00:15ea:6a5c:7b01:c146 tlla 80:7a:bf:6f:48:f3"; + + assertEquals(expected, getSummary(packet)); + } + + @Test + public void testParseARPRequest() { + final String packet = + // Ethernet + "FFFFFFFFFFFF 807ABF6F48F3 0806" + + // ARP + "0001 0800 06 04" + + // Request + "0001" + + "807ABF6F48F3 64706ADB" + + "000000000000 64706FFD"; + + final String expected = + "TX 80:7a:bf:6f:48:f3 > ff:ff:ff:ff:ff:ff arp" + + " who-has 100.112.111.253"; + + assertEquals(expected, getSummary(packet)); + } + + @Test + public void testParseARPReply() { + final String packet = + // Ethernet + "807ABF6F48F3 288A1CA8DFC1 0806" + + // ARP + "0001 0800 06 04" + + // Reply + "0002" + + "288A1CA8DFC1 64706FFD"+ + "807ABF6F48F3 64706ADB" + + // Ethernet padding to packet min size. + "0000000000000000000000000000"; + + final String expected = + "RX 28:8a:1c:a8:df:c1 > 80:7a:bf:6f:48:f3 arp" + + " reply 100.112.111.253 28:8a:1c:a8:df:c1"; + + assertEquals(expected, getSummary(packet)); + } + + @Test + public void testParseDHCPv4Discover() { + final String packet = + // Ethernet + "FFFFFFFFFFFF 807ABF6F48F3 0800" + + // IPv4 + "451001580000400040113986" + + "00000000" + + "FFFFFFFF" + + // UDP + "0044 0043" + + "0144 5559" + + // DHCPv4 + "01 01 06 00" + + "79F7ACA4" + + "0000 0000" + + "00000000" + + "00000000" + + "00000000" + + "00000000" + + "807ABF6F48F300000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "63 82 53 63" + + "35 01 01" + + "3D 07 01807ABF6F48F3" + + "39 02 05DC" + + "3C 12 616E64726F69642D646863702D372E312E32" + + "0C 18 616E64726F69642D36623030366333313333393835343139" + + "37 0A 01 03 06 0F 1A 1C 33 3A 3B 2B" + + "FF" + + "00"; + + final String expectedPrefix = + "TX 80:7a:bf:6f:48:f3 > ff:ff:ff:ff:ff:ff ipv4" + + " 0.0.0.0 > 255.255.255.255 udp" + + " 68 > 67 dhcp4" + + " 80:7a:bf:6f:48:f3 DISCOVER"; + + assertTrue(getSummary(packet).startsWith(expectedPrefix)); + } + + @Test + public void testParseDHCPv4Offer() { + final String packet = + // Ethernet + "807ABF6F48F3 288A1CA8DFC1 0800" + + // IPv4 + "4500013D4D2C0000401188CB" + + "64706FFD" + + "64706ADB" + + // UDP + "0043 0044" + + "0129 371D" + + // DHCPv4 + "02 01 06 01" + + "79F7ACA4" + + "0000 0000" + + "00000000" + + "64706ADB" + + "00000000" + + "00000000" + + "807ABF6F48F300000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "63 82 53 63" + + "35 01 02" + + "36 04 AC188A0B" + + "33 04 00000708" + + "01 04 FFFFF000" + + "03 04 64706FFE" + + "06 08 08080808" + + " 08080404" + + "FF0001076165313A363636FF"; + + final String expectedPrefix = + "RX 28:8a:1c:a8:df:c1 > 80:7a:bf:6f:48:f3 ipv4" + + " 100.112.111.253 > 100.112.106.219 udp" + + " 67 > 68 dhcp4" + + " 80:7a:bf:6f:48:f3 OFFER"; + + assertTrue(getSummary(packet).startsWith(expectedPrefix)); + } + + @Test + public void testParseDHCPv4Request() { + final String packet = + // Ethernet + "FFFFFFFFFFFF 807ABF6F48F3 0800" + + // IPv4 + "45100164000040004011397A" + + "00000000" + + "FFFFFFFF" + + // UDP + "0044 0043" + + "0150 E5C7" + + // DHCPv4 + "01 01 06 00" + + "79F7ACA4" + + "0001 0000" + + "00000000" + + "00000000" + + "00000000" + + "00000000" + + "807ABF6F48F300000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "63 82 53 63" + + "35 01 03" + + "3D 07 01807ABF6F48F3" + + "32 04 64706ADB" + + "36 04 AC188A0B" + + "39 02 05DC" + + "3C 12 616E64726F69642D646863702D372E312E32" + + "0C 18 616E64726F69642D36623030366333313333393835343139" + + "37 0A 01 03 06 0F 1A 1C 33 3A 3B 2B" + + "FF" + + "00"; + + final String expectedPrefix = + "TX 80:7a:bf:6f:48:f3 > ff:ff:ff:ff:ff:ff ipv4" + + " 0.0.0.0 > 255.255.255.255 udp" + + " 68 > 67 dhcp4" + + " 80:7a:bf:6f:48:f3 REQUEST"; + + assertTrue(getSummary(packet).startsWith(expectedPrefix)); + } + + @Test + public void testParseDHCPv4Ack() { + final String packet = + // Ethernet + "807ABF6F48F3 288A1CA8DFC1 0800" + + // IPv4 + "4500013D4D3B0000401188BC" + + "64706FFD" + + "64706ADB" + + // UDP + "0043 0044" + + "0129 341C" + + // DHCPv4 + "02 01 06 01" + + "79F7ACA4" + + "0001 0000" + + "00000000" + + "64706ADB" + + "00000000" + + "00000000" + + "807ABF6F48F300000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "63 82 53 63" + + "35 01 05" + + "36 04 AC188A0B" + + "33 04 00000708" + + "01 04 FFFFF000" + + "03 04 64706FFE" + + "06 08 08080808" + + " 08080404" + + "FF0001076165313A363636FF"; + + final String expectedPrefix = + "RX 28:8a:1c:a8:df:c1 > 80:7a:bf:6f:48:f3 ipv4" + + " 100.112.111.253 > 100.112.106.219 udp" + + " 67 > 68 dhcp4" + + " 80:7a:bf:6f:48:f3 ACK"; + + assertTrue(getSummary(packet).startsWith(expectedPrefix)); + } +} diff --git a/tests/src/android/net/util/PacketReaderTest.java b/tests/src/android/net/util/PacketReaderTest.java new file mode 100644 index 0000000..dced743 --- /dev/null +++ b/tests/src/android/net/util/PacketReaderTest.java @@ -0,0 +1,209 @@ +/* + * 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.net.util.PacketReader.DEFAULT_RECV_BUF_SIZE; +import static android.system.OsConstants.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.os.Handler; +import android.os.HandlerThread; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.system.ErrnoException; +import android.system.Os; +import android.system.StructTimeval; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.runner.RunWith; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import libcore.io.IoBridge; + +/** + * Tests for PacketReader. + * + * @hide + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class PacketReaderTest { + static final InetAddress LOOPBACK6 = Inet6Address.getLoopbackAddress(); + static final StructTimeval TIMEO = StructTimeval.fromMillis(500); + + protected CountDownLatch mLatch; + protected FileDescriptor mLocalSocket; + protected InetSocketAddress mLocalSockName; + protected byte[] mLastRecvBuf; + protected boolean mStopped; + protected HandlerThread mHandlerThread; + protected PacketReader mReceiver; + + class UdpLoopbackReader extends PacketReader { + public UdpLoopbackReader(Handler h) { + super(h); + } + + @Override + protected FileDescriptor createFd() { + FileDescriptor s = null; + try { + s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + Os.bind(s, LOOPBACK6, 0); + mLocalSockName = (InetSocketAddress) Os.getsockname(s); + Os.setsockoptTimeval(s, SOL_SOCKET, SO_SNDTIMEO, TIMEO); + } catch (ErrnoException|SocketException e) { + closeFd(s); + fail(); + return null; + } + + mLocalSocket = s; + return s; + } + + @Override + protected void handlePacket(byte[] recvbuf, int length) { + mLastRecvBuf = Arrays.copyOf(recvbuf, length); + mLatch.countDown(); + } + + @Override + protected void onStart() { + mStopped = false; + mLatch.countDown(); + } + + @Override + protected void onStop() { + mStopped = true; + mLatch.countDown(); + } + }; + + @Before + public void setUp() { + resetLatch(); + mLocalSocket = null; + mLocalSockName = null; + mLastRecvBuf = null; + mStopped = false; + + mHandlerThread = new HandlerThread(PacketReaderTest.class.getSimpleName()); + mHandlerThread.start(); + } + + @After + public void tearDown() throws Exception { + if (mReceiver != null) { + mHandlerThread.getThreadHandler().post(() -> { mReceiver.stop(); }); + waitForActivity(); + } + mReceiver = null; + mHandlerThread.quit(); + mHandlerThread = null; + } + + void resetLatch() { mLatch = new CountDownLatch(1); } + + void waitForActivity() throws Exception { + try { + mLatch.await(1000, TimeUnit.MILLISECONDS); + } finally { + resetLatch(); + } + } + + void sendPacket(byte[] contents) throws Exception { + final DatagramSocket sender = new DatagramSocket(); + sender.connect(mLocalSockName); + sender.send(new DatagramPacket(contents, contents.length)); + sender.close(); + } + + @Test + public void testBasicWorking() throws Exception { + final Handler h = mHandlerThread.getThreadHandler(); + mReceiver = new UdpLoopbackReader(h); + + h.post(() -> { mReceiver.start(); }); + waitForActivity(); + assertTrue(mLocalSockName != null); + assertEquals(LOOPBACK6, mLocalSockName.getAddress()); + assertTrue(0 < mLocalSockName.getPort()); + assertTrue(mLocalSocket != null); + assertFalse(mStopped); + + final byte[] one = "one 1".getBytes("UTF-8"); + sendPacket(one); + waitForActivity(); + assertEquals(1, mReceiver.numPacketsReceived()); + assertTrue(Arrays.equals(one, mLastRecvBuf)); + assertFalse(mStopped); + + final byte[] two = "two 2".getBytes("UTF-8"); + sendPacket(two); + waitForActivity(); + assertEquals(2, mReceiver.numPacketsReceived()); + assertTrue(Arrays.equals(two, mLastRecvBuf)); + assertFalse(mStopped); + + mReceiver.stop(); + waitForActivity(); + assertEquals(2, mReceiver.numPacketsReceived()); + assertTrue(Arrays.equals(two, mLastRecvBuf)); + assertTrue(mStopped); + mReceiver = null; + } + + class NullPacketReader extends PacketReader { + public NullPacketReader(Handler h, int recvbufsize) { + super(h, recvbufsize); + } + + @Override + public FileDescriptor createFd() { return null; } + } + + @Test + public void testMinimalRecvBufSize() throws Exception { + final Handler h = mHandlerThread.getThreadHandler(); + + for (int i : new int[]{-1, 0, 1, DEFAULT_RECV_BUF_SIZE-1}) { + final PacketReader b = new NullPacketReader(h, i); + assertEquals(DEFAULT_RECV_BUF_SIZE, b.recvBufSize()); + } + } +} |