diff options
-rw-r--r-- | src/android/net/dhcp/DhcpClient.java | 410 | ||||
-rw-r--r-- | src/android/net/dhcp/DhcpDeclinePacket.java | 8 | ||||
-rw-r--r-- | src/android/net/dhcp/DhcpPacket.java | 48 | ||||
-rw-r--r-- | src/android/net/util/NetworkStackUtils.java | 15 | ||||
-rw-r--r-- | src/com/android/networkstack/arp/ArpPacket.java | 171 | ||||
-rw-r--r-- | src/com/android/server/util/NetworkStackConstants.java | 23 | ||||
-rw-r--r-- | tests/integration/src/android/net/ip/IpClientIntegrationTest.java | 249 | ||||
-rw-r--r-- | tests/unit/src/com/android/networkstack/arp/ArpPacketTest.java | 199 |
8 files changed, 1051 insertions, 72 deletions
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java index 68f7ab0..e7d53ee 100644 --- a/src/android/net/dhcp/DhcpClient.java +++ b/src/android/net/dhcp/DhcpClient.java @@ -29,11 +29,15 @@ 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.dhcp.DhcpPacket.INFINITE_LEASE; +import static android.net.util.NetworkStackUtils.DHCP_INIT_REBOOT_VERSION; +import static android.net.util.NetworkStackUtils.DHCP_IP_CONFLICT_DETECT_VERSION; +import static android.net.util.NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION; import static android.net.util.NetworkStackUtils.closeSocketQuietly; import static android.net.util.SocketUtils.makePacketSocketAddress; import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static android.system.OsConstants.AF_INET; import static android.system.OsConstants.AF_PACKET; +import static android.system.OsConstants.ETH_P_ARP; import static android.system.OsConstants.ETH_P_IP; import static android.system.OsConstants.IPPROTO_UDP; import static android.system.OsConstants.SOCK_DGRAM; @@ -44,7 +48,11 @@ import static android.system.OsConstants.SO_BROADCAST; import static android.system.OsConstants.SO_RCVBUF; import static android.system.OsConstants.SO_REUSEADDR; +import static com.android.server.util.NetworkStackConstants.ARP_REQUEST; +import static com.android.server.util.NetworkStackConstants.ETHER_ADDR_LEN; import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_ANY; +import static com.android.server.util.NetworkStackConstants.IPV4_CONFLICT_ANNOUNCE_NUM; +import static com.android.server.util.NetworkStackConstants.IPV4_CONFLICT_PROBE_NUM; import android.content.Context; import android.net.DhcpResults; @@ -66,6 +74,7 @@ import android.net.util.PacketReader; import android.net.util.SocketUtils; import android.os.Handler; import android.os.Message; +import android.os.PowerManager; import android.os.SystemClock; import android.system.ErrnoException; import android.system.Os; @@ -85,6 +94,7 @@ import com.android.internal.util.TrafficStatsConstants; import com.android.internal.util.WakeupMessage; import com.android.networkstack.R; import com.android.networkstack.apishim.SocketUtilsShimImpl; +import com.android.networkstack.arp.ArpPacket; import java.io.FileDescriptor; import java.io.IOException; @@ -122,6 +132,7 @@ public class DhcpClient extends StateMachine { private static final String TAG = "DhcpClient"; private static final boolean DBG = true; + private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE); private static final boolean STATE_DBG = Log.isLoggable(TAG, Log.DEBUG); private static final boolean MSG_DBG = Log.isLoggable(TAG, Log.DEBUG); private static final boolean PACKET_DBG = Log.isLoggable(TAG, Log.DEBUG); @@ -138,6 +149,43 @@ public class DhcpClient extends StateMachine { private static final int MAX_TIMEOUT_MS = 128 * SECONDS; private static final int IPMEMORYSTORE_TIMEOUT_MS = 1 * SECONDS; + // The waiting time to restart the DHCP configuration process after broadcasting a + // DHCPDECLINE message, (RFC2131 3.1.5 describes client SHOULD wait a minimum of 10 + // seconds to avoid excessive traffic, but it's too long). + @VisibleForTesting + public static final String DHCP_RESTART_CONFIG_DELAY = "dhcp_restart_configuration_delay"; + private static final int DEFAULT_DHCP_RESTART_CONFIG_DELAY_SEC = 1 * SECONDS; + + // Initial random delay before sending first ARP probe. + @VisibleForTesting + public static final String ARP_FIRST_PROBE_DELAY_MS = "arp_first_probe_delay"; + private static final int DEFAULT_ARP_FIRST_PROBE_DELAY_MS = 100; + + // Minimum delay until retransmitting the probe. The probe will be retransmitted after a + // random number of milliseconds in the range ARP_PROBE_MIN_MS and ARP_PROBE_MAX_MS. + @VisibleForTesting + public static final String ARP_PROBE_MIN_MS = "arp_probe_min"; + private static final int DEFAULT_ARP_PROBE_MIN_MS = 100; + + // Maximum delay until retransmitting the probe. + @VisibleForTesting + public static final String ARP_PROBE_MAX_MS = "arp_probe_max"; + private static final int DEFAULT_ARP_PROBE_MAX_MS = 300; + + // Initial random delay before sending first ARP Announcement after completing Probe packet + // transmission. + @VisibleForTesting + public static final String ARP_FIRST_ANNOUNCE_DELAY_MS = "arp_first_announce_delay"; + private static final int DEFAULT_ARP_FIRST_ANNOUNCE_DELAY_MS = 100; + + // Time between retransmitting ARP Announcement packets. + @VisibleForTesting + public static final String ARP_ANNOUNCE_INTERVAL_MS = "arp_announce_interval"; + private static final int DEFAULT_ARP_ANNOUNCE_INTERVAL_MS = 100; + + // Max conflict count before configuring interface with declined IP address anyway. + private static final int MAX_CONFLICTS_COUNT = 2; + // 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 @@ -190,6 +238,9 @@ public class DhcpClient extends StateMachine { private static final int EVENT_CONFIGURATION_TIMEOUT = PRIVATE_BASE + 7; private static final int EVENT_CONFIGURATION_OBTAINED = PRIVATE_BASE + 8; private static final int EVENT_CONFIGURATION_INVALID = PRIVATE_BASE + 9; + private static final int EVENT_IP_CONFLICT = PRIVATE_BASE + 10; + private static final int CMD_ARP_PROBE = PRIVATE_BASE + 11; + private static final int CMD_ARP_ANNOUNCEMENT = PRIVATE_BASE + 12; // constant to represent this DHCP lease has been expired. @VisibleForTesting @@ -247,6 +298,7 @@ public class DhcpClient extends StateMachine { private DhcpResults mOffer; private Configuration mConfiguration; private Inet4Address mLastAssignedIpv4Address; + private int mConflictCount; private long mLastAssignedIpv4AddressExpiry; private Dependencies mDependencies; @NonNull @@ -277,6 +329,8 @@ public class DhcpClient extends StateMachine { private State mWaitBeforeRenewalState = new WaitBeforeRenewalState(mDhcpRenewingState); private State mWaitBeforeObtainingConfigurationState = new WaitBeforeObtainingConfigurationState(mObtainingConfigurationState); + private State mIpAddressConflictDetectingState = new IpAddressConflictDetectingState(); + private State mDhcpDecliningState = new DhcpDecliningState(); private WakeupMessage makeWakeupMessage(String cmdName, int cmd) { cmdName = DhcpClient.class.getSimpleName() + "." + mIfaceName + "." + cmdName; @@ -302,11 +356,51 @@ public class DhcpClient extends StateMachine { } /** - * Get the value of DHCP related experiment flags. + * Return whether a feature guarded by a feature flag is enabled. + * @see NetworkStackUtils#isFeatureEnabled(Context, String, String) */ - public boolean getBooleanDeviceConfig(final String nameSpace, final String flagName) { - return NetworkStackUtils.getDeviceConfigPropertyBoolean(nameSpace, flagName, - false /* default value */); + public boolean isFeatureEnabled(final Context context, final String name) { + return NetworkStackUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name); + } + + /** + * Get the Integer value of relevant DeviceConfig properties of Connectivity namespace. + */ + public int getIntDeviceConfig(final String name) { + final int defaultValue; + switch (name) { + case DHCP_RESTART_CONFIG_DELAY: + defaultValue = DEFAULT_DHCP_RESTART_CONFIG_DELAY_SEC; + break; + case ARP_FIRST_PROBE_DELAY_MS: + defaultValue = DEFAULT_ARP_FIRST_PROBE_DELAY_MS; + break; + case ARP_PROBE_MIN_MS: + defaultValue = DEFAULT_ARP_PROBE_MIN_MS; + break; + case ARP_PROBE_MAX_MS: + defaultValue = DEFAULT_ARP_PROBE_MAX_MS; + break; + case ARP_FIRST_ANNOUNCE_DELAY_MS: + defaultValue = DEFAULT_ARP_FIRST_ANNOUNCE_DELAY_MS; + break; + case ARP_ANNOUNCE_INTERVAL_MS: + defaultValue = DEFAULT_ARP_ANNOUNCE_INTERVAL_MS; + break; + default: + Log.e(TAG, "Invalid experiment flag: " + name); + return 0; + } + return NetworkStackUtils.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY, name, + defaultValue); + } + + /** + * Get a new wake lock to force CPU keeping awake when transmitting packets or waiting + * for timeout. + */ + public PowerManager.WakeLock getWakeLock(final PowerManager powerManager) { + return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); } } @@ -331,12 +425,14 @@ public class DhcpClient extends StateMachine { addState(mObtainingConfigurationState, mDhcpState); addState(mDhcpSelectingState, mDhcpState); addState(mDhcpRequestingState, mDhcpState); + addState(mIpAddressConflictDetectingState, mDhcpState); addState(mDhcpHaveLeaseState, mDhcpState); addState(mConfiguringInterfaceState, mDhcpHaveLeaseState); addState(mDhcpBoundState, mDhcpHaveLeaseState); addState(mWaitBeforeRenewalState, mDhcpHaveLeaseState); addState(mDhcpRenewingState, mDhcpHaveLeaseState); addState(mDhcpRebindingState, mDhcpHaveLeaseState); + addState(mDhcpDecliningState, mDhcpHaveLeaseState); addState(mDhcpInitRebootState, mDhcpState); addState(mDhcpRebootingState, mDhcpState); // CHECKSTYLE:ON IndentationCheck @@ -370,19 +466,23 @@ public class DhcpClient extends StateMachine { /** * check whether or not to support caching the last lease info and INIT-REBOOT state. - * */ public boolean isDhcpLeaseCacheEnabled() { - return mDependencies.getBooleanDeviceConfig(NAMESPACE_CONNECTIVITY, - NetworkStackUtils.DHCP_INIT_REBOOT_ENABLED); + return mDependencies.isFeatureEnabled(mContext, DHCP_INIT_REBOOT_VERSION); } /** * check whether or not to support DHCP Rapid Commit option. */ public boolean isDhcpRapidCommitEnabled() { - return mDependencies.getBooleanDeviceConfig(NAMESPACE_CONNECTIVITY, - NetworkStackUtils.DHCP_RAPID_COMMIT_ENABLED); + return mDependencies.isFeatureEnabled(mContext, DHCP_RAPID_COMMIT_VERSION); + } + + /** + * check whether or not to support IP address conflict detection and DHCPDECLINE. + */ + public boolean isDhcpIpConflictDetectEnabled() { + return mDependencies.isFeatureEnabled(mContext, DHCP_IP_CONFLICT_DETECT_VERSION); } private void confirmDhcpLease(DhcpPacket packet, DhcpResults results) { @@ -500,7 +600,8 @@ public class DhcpClient extends StateMachine { public int transmitPacket(final ByteBuffer buf, final SocketAddress socketAddress) throws ErrnoException, SocketException { - return Os.sendto(mPacketSock, buf.array(), 0, buf.limit(), 0, socketAddress); + return Os.sendto(mPacketSock, buf.array(), 0 /* byteOffset */, + buf.limit() /* byteCount */, 0 /* flags */, socketAddress); } } @@ -537,20 +638,20 @@ public class DhcpClient extends StateMachine { } private boolean sendDiscoverPacket() { - ByteBuffer packet = DhcpPacket.buildDiscoverPacket( + final ByteBuffer packet = DhcpPacket.buildDiscoverPacket( DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr, DO_UNICAST, REQUESTED_PARAMS, isDhcpRapidCommitEnabled()); return transmitPacket(packet, "DHCPDISCOVER", DhcpPacket.ENCAP_L2, INADDR_BROADCAST); } private boolean sendRequestPacket( - Inet4Address clientAddress, Inet4Address requestedAddress, - Inet4Address serverAddress, Inet4Address to) { + final Inet4Address clientAddress, final Inet4Address requestedAddress, + final Inet4Address serverAddress, final 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( + final ByteBuffer packet = DhcpPacket.buildRequestPacket( encap, mTransactionId, getSecs(), clientAddress, DO_UNICAST, mHwAddr, requestedAddress, serverAddress, REQUESTED_PARAMS, null); @@ -561,6 +662,14 @@ public class DhcpClient extends StateMachine { return transmitPacket(packet, description, encap, to); } + private boolean sendDeclinePacket(final Inet4Address requestedAddress, + final Inet4Address serverIdentifier) { + // Requested IP address and Server Identifier options are mandatory for DHCPDECLINE. + final ByteBuffer packet = DhcpPacket.buildDeclinePacket(DhcpPacket.ENCAP_L2, + mTransactionId, mHwAddr, requestedAddress, serverIdentifier); + return transmitPacket(packet, "DHCPDECLINE", DhcpPacket.ENCAP_L2, INADDR_BROADCAST); + } + private void scheduleLeaseTimers() { if (mDhcpLeaseExpiry == 0) { Log.d(TAG, "Infinite lease, no timer scheduling needed"); @@ -814,6 +923,7 @@ public class DhcpClient extends StateMachine { @Override public void enter() { clearDhcpState(); + mConflictCount = 0; if (initInterface() && initUdpSocket()) { mDhcpPacketHandler = new DhcpPacketHandler(getHandler()); if (mDhcpPacketHandler.start()) return; @@ -1033,6 +1143,7 @@ public class DhcpClient extends StateMachine { @Override public void exit() { + super.exit(); removeMessages(EVENT_CONFIGURATION_INVALID); removeMessages(EVENT_CONFIGURATION_TIMEOUT); removeMessages(EVENT_CONFIGURATION_OBTAINED); @@ -1189,7 +1300,8 @@ public class DhcpClient extends StateMachine { DhcpResults results = packet.toDhcpResults(); if (results != null) { confirmDhcpLease(packet, results); - transitionTo(mConfiguringInterfaceState); + transitionTo(isDhcpIpConflictDetectEnabled() + ? mIpAddressConflictDetectingState : mConfiguringInterfaceState); } } else if (packet instanceof DhcpNakPacket) { // TODO: Wait a while before returning into INIT state. @@ -1254,6 +1366,238 @@ public class DhcpClient extends StateMachine { } } + private class IpConflictDetector extends PacketReader { + private FileDescriptor mArpSock; + private final Inet4Address mTargetIp; + + IpConflictDetector(@NonNull Handler handler, @NonNull Inet4Address ipAddress) { + super(handler); + mTargetIp = ipAddress; + } + + @Override + protected void handlePacket(byte[] recvbuf, int length) { + try { + final ArpPacket packet = ArpPacket.parseArpPacket(recvbuf, length); + if (hasIpAddressConflict(packet, mTargetIp)) { + sendMessage(EVENT_IP_CONFLICT); + } + } catch (ArpPacket.ParseException e) { + logError("Can't parse ARP packet", e); + } + } + + @Override + protected FileDescriptor createFd() { + try { + // TODO: attach ARP packet only filter. + mArpSock = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0 /* protocol */); + SocketAddress addr = makePacketSocketAddress(ETH_P_ARP, mIface.index); + Os.bind(mArpSock, addr); + return mArpSock; + } catch (SocketException | ErrnoException e) { + logError("Error creating ARP socket", e); + closeFd(mArpSock); + mArpSock = null; + return null; + } + } + + @Override + protected void logError(@NonNull String msg, @NonNull Exception e) { + Log.e(TAG, msg, e); + } + + public boolean transmitPacket(@NonNull Inet4Address targetIp, + @NonNull Inet4Address senderIp, final byte[] hwAddr, + @NonNull SocketAddress sockAddr) { + // RFC5227 3. describes both ARP Probes and Announcements use ARP Request packet. + final ByteBuffer packet = ArpPacket.buildArpPacket(DhcpPacket.ETHER_BROADCAST, hwAddr, + targetIp.getAddress(), new byte[ETHER_ADDR_LEN], senderIp.getAddress(), + (short) ARP_REQUEST); + try { + Os.sendto(mArpSock, packet.array(), 0 /* byteOffset */, + packet.limit() /* byteCount */, 0 /* flags */, sockAddr); + return true; + } catch (ErrnoException | SocketException e) { + logError("Can't send ARP packet", e); + return false; + } + } + } + + private boolean isArpProbe(@NonNull ArpPacket packet) { + return (packet.opCode == ARP_REQUEST && packet.senderIp.equals(INADDR_ANY) + && !packet.targetIp.equals(INADDR_ANY)); + } + + // RFC5227 2.1.1 says, during probing period: + // 1. the host receives any ARP packet (Request *or* Reply) on the interface where the + // probe is being performed, where the packet's 'sender IP address' is the address + // being probed for, then the host MUST treat this address as conflict. + // 2. the host receives any ARP Probe where the packet's 'target IP address' is the + // address being probed for, and the packet's 'sender hardware address' is not the + // hardware address of any of the host's interfaces, then the host SHOULD similarly + // treat this as an address conflict. + private boolean checkArpSenderIpOrTargetIp(@NonNull ArpPacket packet, + @NonNull Inet4Address targetIp) { + return ((!packet.senderIp.equals(INADDR_ANY) && packet.senderIp.equals(targetIp)) + || (isArpProbe(packet) && packet.targetIp.equals(targetIp))); + } + + private boolean hasIpAddressConflict(@NonNull ArpPacket packet, + @NonNull Inet4Address targetIp) { + if (checkArpSenderIpOrTargetIp(packet, targetIp) + && !Arrays.equals(packet.senderHwAddress.toByteArray(), mHwAddr)) { + if (DBG) { + final String senderIpString = packet.senderIp.getHostAddress(); + final String targetIpString = packet.targetIp.getHostAddress(); + final MacAddress senderMacAddress = packet.senderHwAddress; + final MacAddress hostMacAddress = MacAddress.fromBytes(mHwAddr); + Log.d(TAG, "IP address conflict detected:" + + (packet.opCode == ARP_REQUEST ? "ARP Request" : "ARP Reply") + + " ARP sender MAC: " + senderMacAddress.toString() + + " host MAC: " + hostMacAddress.toString() + + " ARP sender IP: " + senderIpString + + " ARP target IP: " + targetIpString + + " host target IP: " + targetIp.getHostAddress()); + } + return true; + } + return false; + } + + class IpAddressConflictDetectingState extends LoggingState { + private int mArpProbeCount; + private int mArpAnnounceCount; + private Inet4Address mTargetIp; + private IpConflictDetector mIpConflictDetector; + private PowerManager.WakeLock mTimeoutWakeLock; + + private int mArpFirstProbeDelayMs; + private int mArpProbeMaxDelayMs; + private int mArpProbeMinDelayMs; + private int mArpFirstAnnounceDelayMs; + private int mArpAnnounceIntervalMs; + + @Override + public void enter() { + super.enter(); + + mArpProbeCount = 0; + mArpAnnounceCount = 0; + + // IP address conflict detection occurs after receiving DHCPACK + // message every time, i.e. we already get an available lease from + // DHCP server, that ensures mDhcpLease should be NonNull, see + // {@link DhcpRequestingState#receivePacket} for details. + mTargetIp = (Inet4Address) mDhcpLease.ipAddress.getAddress(); + mIpConflictDetector = new IpConflictDetector(getHandler(), mTargetIp); + + // IpConflictDetector might fail to create the raw socket. + if (!mIpConflictDetector.start()) { + Log.e(TAG, "Fail to start IP Conflict Detector"); + transitionTo(mConfiguringInterfaceState); + return; + } + + // Read the customized parameters from DeviceConfig. + mArpFirstProbeDelayMs = mDependencies.getIntDeviceConfig(ARP_FIRST_PROBE_DELAY_MS); + mArpProbeMaxDelayMs = mDependencies.getIntDeviceConfig(ARP_PROBE_MAX_MS); + mArpProbeMinDelayMs = mDependencies.getIntDeviceConfig(ARP_PROBE_MIN_MS); + mArpAnnounceIntervalMs = mDependencies.getIntDeviceConfig(ARP_ANNOUNCE_INTERVAL_MS); + mArpFirstAnnounceDelayMs = mDependencies.getIntDeviceConfig( + ARP_FIRST_ANNOUNCE_DELAY_MS); + + if (VDBG) { + Log.d(TAG, "ARP First Probe delay: " + mArpFirstProbeDelayMs + + " ARP Probe Max delay: " + mArpProbeMaxDelayMs + + " ARP Probe Min delay: " + mArpProbeMinDelayMs + + " ARP First Announce delay: " + mArpFirstAnnounceDelayMs + + " ARP Announce interval: " + mArpAnnounceIntervalMs); + } + + // Note that when we get here, we're still processing the WakeupMessage that caused + // us to transition into this state, and thus the AlarmManager is still holding its + // wakelock. That wakelock might expire as soon as this method returns. + final PowerManager powerManager = mContext.getSystemService(PowerManager.class); + mTimeoutWakeLock = mDependencies.getWakeLock(powerManager); + mTimeoutWakeLock.acquire(); + + // RFC5227 2.1.1 describes waiting for a random time interval between 0 and + // PROBE_WAIT seconds before sending probe packets PROBE_NUM times, this delay + // helps avoid hosts send initial probe packet simultaneously upon power on. + // Probe packet transmission interval spaces randomly and uniformly between + // PROBE_MIN and PROBE_MAX. + sendMessageDelayed(CMD_ARP_PROBE, mRandom.nextInt(mArpFirstProbeDelayMs)); + } + + @Override + public boolean processMessage(Message message) { + super.processMessage(message); + switch (message.what) { + case CMD_ARP_PROBE: + // According to RFC5227, wait ANNOUNCE_WAIT seconds after + // the last ARP Probe, and no conflicting ARP Reply or ARP + // Probe has been received within this period, then host can + // determine the desired IP address may be used safely. + sendArpProbe(); + if (++mArpProbeCount < IPV4_CONFLICT_PROBE_NUM) { + scheduleProbe(); + } else { + scheduleAnnounce(mArpFirstAnnounceDelayMs); + } + return HANDLED; + case CMD_ARP_ANNOUNCEMENT: + sendArpAnnounce(); + if (++mArpAnnounceCount < IPV4_CONFLICT_ANNOUNCE_NUM) { + scheduleAnnounce(mArpAnnounceIntervalMs); + } else { + transitionTo(mConfiguringInterfaceState); + } + return HANDLED; + case EVENT_IP_CONFLICT: + transitionTo(mDhcpDecliningState); + return HANDLED; + default: + return NOT_HANDLED; + } + } + + // Because the timing parameters used in IP Address detection mechanism are in + // milliseconds, WakeupMessage would be too imprecise for small timeouts. + private void scheduleProbe() { + long timeout = mRandom.nextInt(mArpProbeMaxDelayMs - mArpProbeMinDelayMs) + + mArpProbeMinDelayMs; + sendMessageDelayed(CMD_ARP_PROBE, timeout); + } + + private void scheduleAnnounce(final int timeout) { + sendMessageDelayed(CMD_ARP_ANNOUNCEMENT, timeout); + } + + @Override + public void exit() { + super.exit(); + mTimeoutWakeLock.release(); + mIpConflictDetector.stop(); + if (DBG) Log.d(TAG, "IP Conflict Detector stopped"); + removeMessages(CMD_ARP_PROBE); + removeMessages(CMD_ARP_ANNOUNCEMENT); + removeMessages(EVENT_IP_CONFLICT); + } + + private boolean sendArpProbe() { + return mIpConflictDetector.transmitPacket(mTargetIp /* target IP */, + INADDR_ANY /* sender IP */, mHwAddr, mInterfaceBroadcastAddr); + } + + private boolean sendArpAnnounce() { + return mIpConflictDetector.transmitPacket(mTargetIp /* target IP */, + mTargetIp /* sender IP */, mHwAddr, mInterfaceBroadcastAddr); + } + } + class DhcpBoundState extends LoggingState { @Override public void enter() { @@ -1421,10 +1765,10 @@ public class DhcpClient extends StateMachine { @Override protected boolean sendPacket() { return sendRequestPacket( - INADDR_ANY, // ciaddr - mLastAssignedIpv4Address, // DHCP_REQUESTED_IP - null, // DHCP_SERVER_IDENTIFIER - INADDR_BROADCAST); // packet destination address + INADDR_ANY, // ciaddr + mLastAssignedIpv4Address, // DHCP_REQUESTED_IP + null, // DHCP_SERVER_IDENTIFIER + INADDR_BROADCAST); // packet destination address } @Override @@ -1437,6 +1781,34 @@ public class DhcpClient extends StateMachine { class DhcpRebootingState extends LoggingState { } + class DhcpDecliningState extends TimeoutState { + @Override + public void enter() { + // If the host experiences MAX_CONFLICTS or more address conflicts on the + // interface, configure interface with this IP address anyway. + if (++mConflictCount > MAX_CONFLICTS_COUNT) { + transitionTo(mConfiguringInterfaceState); + return; + } + + mTimeout = mDependencies.getIntDeviceConfig(DHCP_RESTART_CONFIG_DELAY); + super.enter(); + sendPacket(); + } + + // No need to override processMessage here since this state is + // functionally identical to its superclass TimeoutState. + protected void timeout() { + transitionTo(mDhcpInitState); + } + + private boolean sendPacket() { + return sendDeclinePacket( + (Inet4Address) mDhcpLease.ipAddress.getAddress(), // requested IP + (Inet4Address) mDhcpLease.serverAddress); // serverIdentifier + } + } + private void logState(String name, int durationMs) { final DhcpClientEvent event = new DhcpClientEvent.Builder() .setMsg(name) diff --git a/src/android/net/dhcp/DhcpDeclinePacket.java b/src/android/net/dhcp/DhcpDeclinePacket.java index 2f4f0f6..14a27db 100644 --- a/src/android/net/dhcp/DhcpDeclinePacket.java +++ b/src/android/net/dhcp/DhcpDeclinePacket.java @@ -27,9 +27,11 @@ public 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) { + Inet4Address nextIp, Inet4Address relayIp, byte[] clientMac, + Inet4Address requestedIpAddress, Inet4Address serverIdentifier) { super(transId, secs, clientIp, yourIp, nextIp, relayIp, clientMac, false); + mRequestedIp = requestedIpAddress; + mServerIdentifier = serverIdentifier; } public String toString() { @@ -55,6 +57,8 @@ public class DhcpDeclinePacket extends DhcpPacket { void finishPacket(ByteBuffer buffer) { addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_DECLINE); addTlv(buffer, DHCP_CLIENT_IDENTIFIER, getClientId()); + addTlv(buffer, DHCP_REQUESTED_IP, mRequestedIp); + addTlv(buffer, DHCP_SERVER_IDENTIFIER, mServerIdentifier); // 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/DhcpPacket.java b/src/android/net/dhcp/DhcpPacket.java index efce6a5..5ab769b 100644 --- a/src/android/net/dhcp/DhcpPacket.java +++ b/src/android/net/dhcp/DhcpPacket.java @@ -210,7 +210,8 @@ public abstract class DhcpPacket { * DHCP Optional Type: DHCP Requested IP Address */ protected static final byte DHCP_REQUESTED_IP = 50; - protected Inet4Address mRequestedIp; + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + public Inet4Address mRequestedIp; /** * DHCP Optional Type: DHCP Lease Time @@ -236,7 +237,8 @@ public abstract class DhcpPacket { * DHCP Optional Type: DHCP Server Identifier */ protected static final byte DHCP_SERVER_IDENTIFIER = 54; - protected Inet4Address mServerIdentifier; + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + public Inet4Address mServerIdentifier; /** * DHCP Optional Type: DHCP Parameter List @@ -1189,8 +1191,8 @@ public abstract class DhcpPacket { break; case DHCP_MESSAGE_TYPE_DECLINE: newPacket = new DhcpDeclinePacket( - transactionId, secs, clientIp, yourIp, nextIp, relayIp, - clientMac); + transactionId, secs, clientIp, yourIp, nextIp, relayIp, clientMac, requestedIp, + serverIdentifier); break; case DHCP_MESSAGE_TYPE_ACK: newPacket = new DhcpAckPacket( @@ -1339,11 +1341,11 @@ public abstract class DhcpPacket { * 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) { + 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); @@ -1366,11 +1368,11 @@ public abstract class DhcpPacket { * 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) { + 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); @@ -1405,9 +1407,9 @@ public abstract class DhcpPacket { * 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) { + 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; @@ -1417,4 +1419,16 @@ public abstract class DhcpPacket { ByteBuffer result = pkt.buildPacket(encap, DHCP_SERVER, DHCP_CLIENT); return result; } + + /** + * Builds a DHCP-DECLINE packet from the required specified parameters. + */ + public static ByteBuffer buildDeclinePacket(int encap, int transactionId, byte[] clientMac, + Inet4Address requestedIpAddress, Inet4Address serverIdentifier) { + DhcpPacket pkt = new DhcpDeclinePacket(transactionId, (short) 0 /* secs */, + INADDR_ANY /* clientIp */, INADDR_ANY /* yourIp */, INADDR_ANY /* nextIp */, + INADDR_ANY /* relayIp */, clientMac, requestedIpAddress, serverIdentifier); + ByteBuffer result = pkt.buildPacket(encap, DHCP_SERVER, DHCP_CLIENT); + return result; + } } diff --git a/src/android/net/util/NetworkStackUtils.java b/src/android/net/util/NetworkStackUtils.java index b2deefd..993f3cb 100644 --- a/src/android/net/util/NetworkStackUtils.java +++ b/src/android/net/util/NetworkStackUtils.java @@ -121,6 +121,21 @@ public class NetworkStackUtils { */ public static final String DHCP_RAPID_COMMIT_ENABLED = "dhcp_rapid_commit_enabled"; + /** + * Minimum module version at which to enable the DHCP INIT-REBOOT state. + */ + public static final String DHCP_INIT_REBOOT_VERSION = "dhcp_init_reboot_version"; + + /** + * Minimum module version at which to enable the DHCP Rapid Commit option. + */ + public static final String DHCP_RAPID_COMMIT_VERSION = "dhcp_rapid_commit_version"; + + /** + * Minimum module version at which to enable the IP address conflict detection feature. + */ + public static final String DHCP_IP_CONFLICT_DETECT_VERSION = "dhcp_ip_conflict_detect_version"; + static { System.loadLibrary("networkstackutilsjni"); } diff --git a/src/com/android/networkstack/arp/ArpPacket.java b/src/com/android/networkstack/arp/ArpPacket.java new file mode 100644 index 0000000..b84ff2b --- /dev/null +++ b/src/com/android/networkstack/arp/ArpPacket.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.arp; + +import static android.system.OsConstants.ETH_P_ARP; +import static android.system.OsConstants.ETH_P_IP; + +import static com.android.server.util.NetworkStackConstants.ARP_ETHER_IPV4_LEN; +import static com.android.server.util.NetworkStackConstants.ARP_HWTYPE_ETHER; +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.ETHER_ADDR_LEN; +import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_LEN; + +import android.net.MacAddress; + +import com.android.internal.annotations.VisibleForTesting; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +/** + * Defines basic data and operations needed to build and parse packets for the + * ARP protocol. + * + * @hide + */ +public class ArpPacket { + private static final String TAG = "ArpPacket"; + + public final short opCode; + public final Inet4Address senderIp; + public final Inet4Address targetIp; + public final MacAddress senderHwAddress; + public final MacAddress targetHwAddress; + + ArpPacket(short opCode, MacAddress senderHwAddress, Inet4Address senderIp, + MacAddress targetHwAddress, Inet4Address targetIp) { + this.opCode = opCode; + this.senderHwAddress = senderHwAddress; + this.senderIp = senderIp; + this.targetHwAddress = targetHwAddress; + this.targetIp = targetIp; + } + + /** + * Build an ARP packet from the required specified parameters. + */ + @VisibleForTesting + public static ByteBuffer buildArpPacket(final byte[] dstMac, final byte[] srcMac, + final byte[] targetIp, final byte[] targetHwAddress, byte[] senderIp, + final short opCode) { + final ByteBuffer buf = ByteBuffer.allocate(ARP_ETHER_IPV4_LEN); + + // Ether header + buf.put(dstMac); + buf.put(srcMac); + buf.putShort((short) ETH_P_ARP); + + // ARP header + buf.putShort((short) ARP_HWTYPE_ETHER); // hrd + buf.putShort((short) ETH_P_IP); // pro + buf.put((byte) ETHER_ADDR_LEN); // hln + buf.put((byte) IPV4_ADDR_LEN); // pln + buf.putShort(opCode); // op + buf.put(srcMac); // sha + buf.put(senderIp); // spa + buf.put(targetHwAddress); // tha + buf.put(targetIp); // tpa + buf.flip(); + return buf; + } + + /** + * Parse an ARP packet from an ByteBuffer object. + */ + @VisibleForTesting + public static ArpPacket parseArpPacket(final byte[] recvbuf, final int length) + throws ParseException { + try { + if (length < ARP_ETHER_IPV4_LEN || recvbuf.length < length) { + throw new ParseException("Invalid packet length: " + length); + } + + final ByteBuffer buffer = ByteBuffer.wrap(recvbuf, 0, length); + byte[] l2dst = new byte[ETHER_ADDR_LEN]; + byte[] l2src = new byte[ETHER_ADDR_LEN]; + buffer.get(l2dst); + buffer.get(l2src); + + final short etherType = buffer.getShort(); + if (etherType != ETH_P_ARP) { + throw new ParseException("Incorrect Ether Type: " + etherType); + } + + final short hwType = buffer.getShort(); + if (hwType != ARP_HWTYPE_ETHER) { + throw new ParseException("Incorrect HW Type: " + hwType); + } + + final short protoType = buffer.getShort(); + if (protoType != ETH_P_IP) { + throw new ParseException("Incorrect Protocol Type: " + protoType); + } + + final byte hwAddrLength = buffer.get(); + if (hwAddrLength != ETHER_ADDR_LEN) { + throw new ParseException("Incorrect HW address length: " + hwAddrLength); + } + + final byte ipAddrLength = buffer.get(); + if (ipAddrLength != IPV4_ADDR_LEN) { + throw new ParseException("Incorrect Protocol address length: " + ipAddrLength); + } + + final short opCode = buffer.getShort(); + if (opCode != ARP_REQUEST && opCode != ARP_REPLY) { + throw new ParseException("Incorrect opCode: " + opCode); + } + + byte[] senderHwAddress = new byte[ETHER_ADDR_LEN]; + byte[] senderIp = new byte[IPV4_ADDR_LEN]; + buffer.get(senderHwAddress); + buffer.get(senderIp); + + byte[] targetHwAddress = new byte[ETHER_ADDR_LEN]; + byte[] targetIp = new byte[IPV4_ADDR_LEN]; + buffer.get(targetHwAddress); + buffer.get(targetIp); + + return new ArpPacket(opCode, MacAddress.fromBytes(senderHwAddress), + (Inet4Address) InetAddress.getByAddress(senderIp), + MacAddress.fromBytes(targetHwAddress), + (Inet4Address) InetAddress.getByAddress(targetIp)); + } catch (IndexOutOfBoundsException e) { + throw new ParseException("Invalid index when wrapping a byte array into a buffer"); + } catch (BufferUnderflowException e) { + throw new ParseException("Invalid buffer position"); + } catch (IllegalArgumentException e) { + throw new ParseException("Invalid MAC address representation"); + } catch (UnknownHostException e) { + throw new ParseException("Invalid IP address of Host"); + } + } + + /** + * Thrown when parsing ARP packet failed. + */ + public static class ParseException extends Exception { + ParseException(String message) { + super(message); + } + } +} diff --git a/src/com/android/server/util/NetworkStackConstants.java b/src/com/android/server/util/NetworkStackConstants.java index c011b6a..36bac27 100644 --- a/src/com/android/server/util/NetworkStackConstants.java +++ b/src/com/android/server/util/NetworkStackConstants.java @@ -26,16 +26,6 @@ import java.net.Inet4Address; public final class NetworkStackConstants { /** - * IPv4 constants. - * - * See also: - * - https://tools.ietf.org/html/rfc791 - */ - public static final int IPV4_ADDR_BITS = 32; - public static final int IPV4_MIN_MTU = 68; - public static final int IPV4_MAX_MTU = 65_535; - - /** * Ethernet constants. * * See also: @@ -64,6 +54,7 @@ public final class NetworkStackConstants { * - 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_ETHER_IPV4_LEN = ARP_PAYLOAD_LEN + ETHER_HEADER_LEN; public static final int ARP_REQUEST = 1; public static final int ARP_REPLY = 2; public static final int ARP_HWTYPE_RESERVED_LO = 0; @@ -71,11 +62,23 @@ public final class NetworkStackConstants { public static final int ARP_HWTYPE_RESERVED_HI = 0xffff; /** + * IPv4 Address Conflict Detection constants. + * + * See also: + * - https://tools.ietf.org/html/rfc5227 + */ + public static final int IPV4_CONFLICT_PROBE_NUM = 3; + public static final int IPV4_CONFLICT_ANNOUNCE_NUM = 2; + + /** * IPv4 constants. * * See also: * - https://tools.ietf.org/html/rfc791 */ + public static final int IPV4_ADDR_BITS = 32; + public static final int IPV4_MIN_MTU = 68; + public static final int IPV4_MAX_MTU = 65_535; 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; diff --git a/tests/integration/src/android/net/ip/IpClientIntegrationTest.java b/tests/integration/src/android/net/ip/IpClientIntegrationTest.java index 1d2bc86..c5f99c5 100644 --- a/tests/integration/src/android/net/ip/IpClientIntegrationTest.java +++ b/tests/integration/src/android/net/ip/IpClientIntegrationTest.java @@ -31,6 +31,9 @@ import static android.system.OsConstants.IPPROTO_ICMPV6; import static android.system.OsConstants.IPPROTO_TCP; import static com.android.internal.util.BitUtils.uint16; +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.ETHER_ADDR_LEN; import static com.android.server.util.NetworkStackConstants.ETHER_HEADER_LEN; import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_IPV6; import static com.android.server.util.NetworkStackConstants.ETHER_TYPE_OFFSET; @@ -81,6 +84,7 @@ import android.net.NetworkStackIpMemoryStore; import android.net.TestNetworkInterface; import android.net.TestNetworkManager; import android.net.dhcp.DhcpClient; +import android.net.dhcp.DhcpDeclinePacket; import android.net.dhcp.DhcpDiscoverPacket; import android.net.dhcp.DhcpPacket; import android.net.dhcp.DhcpPacket.ParseException; @@ -89,6 +93,7 @@ import android.net.ipmemorystore.NetworkAttributes; import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener; import android.net.ipmemorystore.Status; import android.net.shared.ProvisioningConfiguration; +import android.net.util.InterfaceParams; import android.net.util.IpUtils; import android.net.util.NetworkStackUtils; import android.net.util.PacketReader; @@ -96,6 +101,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.ParcelFileDescriptor; +import android.os.PowerManager; import android.os.RemoteException; import android.system.ErrnoException; import android.system.Os; @@ -105,6 +111,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.networkstack.arp.ArpPacket; import com.android.server.NetworkObserverRegistry; import com.android.server.NetworkStackService.NetworkStackServiceManager; import com.android.server.connectivity.ipmemorystore.IpMemoryStoreService; @@ -126,6 +133,7 @@ import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -155,6 +163,7 @@ public class IpClientIntegrationTest { @Mock private NetworkStackServiceManager mNetworkStackServiceManager; @Mock private NetworkStackIpMemoryStore mIpMemoryStore; @Mock private IpMemoryStoreService mIpMemoryStoreService; + @Mock private PowerManager.WakeLock mTimeoutWakeLock; @Spy private INetd mNetd; @@ -164,6 +173,7 @@ public class IpClientIntegrationTest { private TapPacketReader mPacketReader; private IpClient mIpc; private Dependencies mDependencies; + private byte[] mClientMac; // Ethernet header private static final int ETH_HEADER_LEN = 14; @@ -183,7 +193,6 @@ public class IpClientIntegrationTest { private static final int DHCP_MESSAGE_OP_CODE_OFFSET = DHCP_HEADER_OFFSET + 0; private static final int DHCP_TRANSACTION_ID_OFFSET = DHCP_HEADER_OFFSET + 4; private static final int DHCP_OPTION_MAGIC_COOKIE_OFFSET = DHCP_HEADER_OFFSET + 236; - private static final int DHCP_OPTION_MESSAGE_TYPE_OFFSET = DHCP_OPTION_MAGIC_COOKIE_OFFSET + 4; private static final Inet4Address SERVER_ADDR = (Inet4Address) InetAddresses.parseNumericAddress("192.168.1.100"); @@ -198,6 +207,7 @@ public class IpClientIntegrationTest { private static final String HOSTNAME = "testhostname"; private static final int TEST_DEFAULT_MTU = 1500; private static final int TEST_MIN_MTU = 1280; + private static final byte[] SERVER_MAC = new byte[] { 0x00, 0x1A, 0x11, 0x22, 0x33, 0x44 }; private static class TapPacketReader extends PacketReader { private final ParcelFileDescriptor mTapFd; @@ -242,6 +252,7 @@ public class IpClientIntegrationTest { private class Dependencies extends IpClient.Dependencies { private boolean mIsDhcpLeaseCacheEnabled; private boolean mIsDhcpRapidCommitEnabled; + private boolean mIsDhcpIpConflictDetectEnabled; // Can't use SparseIntArray, it doesn't have an easy way to know if a key is not present. private HashMap<String, Integer> mIntConfigProperties = new HashMap<>(); @@ -253,6 +264,10 @@ public class IpClientIntegrationTest { mIsDhcpRapidCommitEnabled = enable; } + public void setDhcpIpConflictDetectEnabled(final boolean enable) { + mIsDhcpIpConflictDetectEnabled = enable; + } + @Override public INetd getNetd(Context context) { return mNetd; @@ -269,18 +284,29 @@ public class IpClientIntegrationTest { NetworkStackIpMemoryStore ipMemoryStore) { return new DhcpClient.Dependencies(ipMemoryStore) { @Override - public boolean getBooleanDeviceConfig(final String nameSpace, - final String flagName) { - switch (flagName) { - case NetworkStackUtils.DHCP_RAPID_COMMIT_ENABLED: + public boolean isFeatureEnabled(final Context context, final String name) { + switch (name) { + case NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION: return mIsDhcpRapidCommitEnabled; - case NetworkStackUtils.DHCP_INIT_REBOOT_ENABLED: + case NetworkStackUtils.DHCP_INIT_REBOOT_VERSION: return mIsDhcpLeaseCacheEnabled; + case NetworkStackUtils.DHCP_IP_CONFLICT_DETECT_VERSION: + return mIsDhcpIpConflictDetectEnabled; default: - fail("Invalid experiment flag: " + flagName); + fail("Invalid experiment flag: " + name); return false; } } + + @Override + public int getIntDeviceConfig(final String name) { + return getDeviceConfigPropertyInt(name, 0 /* default value */); + } + + @Override + public PowerManager.WakeLock getWakeLock(final PowerManager powerManager) { + return mTimeoutWakeLock; + } }; } @@ -311,6 +337,12 @@ public class IpClientIntegrationTest { .thenReturn(mIpMemoryStoreService); mDependencies.setDeviceConfigProperty(IpClient.CONFIG_MIN_RDNSS_LIFETIME, 67); + mDependencies.setDeviceConfigProperty(DhcpClient.DHCP_RESTART_CONFIG_DELAY, 10); + mDependencies.setDeviceConfigProperty(DhcpClient.ARP_FIRST_PROBE_DELAY_MS, 10); + mDependencies.setDeviceConfigProperty(DhcpClient.ARP_PROBE_MIN_MS, 10); + mDependencies.setDeviceConfigProperty(DhcpClient.ARP_PROBE_MAX_MS, 20); + mDependencies.setDeviceConfigProperty(DhcpClient.ARP_FIRST_ANNOUNCE_DELAY_MS, 10); + mDependencies.setDeviceConfigProperty(DhcpClient.ARP_ANNOUNCE_INTERVAL_MS, 10); setUpTapInterface(); setUpIpClient(); @@ -348,6 +380,7 @@ public class IpClientIntegrationTest { inst.getUiAutomation().dropShellPermissionIdentity(); } mIfaceName = iface.getInterfaceName(); + mClientMac = InterfaceParams.getByName(mIfaceName).macAddr.toByteArray(); mPacketReaderThread = new HandlerThread(IpClientIntegrationTest.class.getSimpleName()); mPacketReaderThread.start(); mHandler = mPacketReaderThread.getThreadHandler(); @@ -405,6 +438,14 @@ public class IpClientIntegrationTest { return true; } + private ArpPacket parseArpPacketOrNull(final byte[] packet) { + try { + return ArpPacket.parseArpPacket(packet, packet.length); + } catch (ArpPacket.ParseException e) { + return null; + } + } + private static ByteBuffer buildDhcpOfferPacket(final DhcpPacket packet, final Integer leaseTimeSec, final short mtu) { return DhcpPacket.buildOfferPacket(DhcpPacket.ENCAP_L2, packet.getTransactionId(), @@ -444,9 +485,25 @@ public class IpClientIntegrationTest { } } + private void sendArpReply(final byte[] clientMac) throws IOException { + final ByteBuffer packet = ArpPacket.buildArpPacket(clientMac /* dst */, + SERVER_MAC /* src */, INADDR_ANY.getAddress() /* target IP */, + clientMac /* target HW address */, CLIENT_ADDR.getAddress() /* sender IP */, + (short) ARP_REPLY); + sendResponse(packet); + } + + private void sendArpProbe() throws IOException { + final ByteBuffer packet = ArpPacket.buildArpPacket(DhcpPacket.ETHER_BROADCAST /* dst */, + SERVER_MAC /* src */, CLIENT_ADDR.getAddress() /* target IP */, + new byte[ETHER_ADDR_LEN] /* target HW address */, + INADDR_ANY.getAddress() /* sender IP */, (short) ARP_REQUEST); + sendResponse(packet); + } + private void startIpClientProvisioning(final boolean isDhcpLeaseCacheEnabled, - final boolean isDhcpRapidCommitEnabled, final boolean isPreconnectionEnabled) - throws RemoteException { + final boolean isDhcpRapidCommitEnabled, final boolean isPreconnectionEnabled, + final boolean isDhcpIpConflictDetectEnabled) throws RemoteException { ProvisioningConfiguration.Builder builder = new ProvisioningConfiguration.Builder() .withoutIpReachabilityMonitor() .withoutIPv6(); @@ -454,6 +511,7 @@ public class IpClientIntegrationTest { mDependencies.setDhcpLeaseCacheEnabled(isDhcpLeaseCacheEnabled); mDependencies.setDhcpRapidCommitEnabled(isDhcpRapidCommitEnabled); + mDependencies.setDhcpIpConflictDetectEnabled(isDhcpIpConflictDetectEnabled); mIpc.setL2KeyAndGroupHint(TEST_L2KEY, TEST_GROUPHINT); mIpc.startProvisioning(builder.build()); verify(mCb).setNeighborDiscoveryOffload(true); @@ -493,9 +551,10 @@ public class IpClientIntegrationTest { // Helper method to complete DHCP 2-way or 4-way handshake private void performDhcpHandshake(final boolean isSuccessLease, final Integer leaseTimeSec, final boolean isDhcpLeaseCacheEnabled, - final boolean isDhcpRapidCommitEnabled, final int mtu) throws Exception { + final boolean isDhcpRapidCommitEnabled, final int mtu, + final boolean isDhcpIpConflictDetectEnabled) throws Exception { startIpClientProvisioning(isDhcpLeaseCacheEnabled, isDhcpRapidCommitEnabled, - false /* isPreconnectionEnabled */); + false /* isPreconnectionEnabled */, isDhcpIpConflictDetectEnabled); DhcpPacket packet; while ((packet = getNextDhcpPacket()) != null) { @@ -538,7 +597,8 @@ public class IpClientIntegrationTest { return null; }).when(mIpMemoryStore).retrieveNetworkAttributes(eq(TEST_L2KEY), any()); startIpClientProvisioning(true /* isDhcpLeaseCacheEnabled */, - false /* isDhcpRapidCommitEnabled */, false /* isPreconnectionEnabled */); + false /* isDhcpRapidCommitEnabled */, false /* isPreconnectionEnabled */, + false /* isDhcpIpConflictDetectEnabled */); return getNextDhcpPacket(); } @@ -563,7 +623,8 @@ public class IpClientIntegrationTest { if (shouldChangeMtu) mtu = TEST_MIN_MTU; performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S, - true /* isDhcpLeaseCacheEnabled */, false /* isDhcpRapidCommitEnabled */, mtu); + true /* isDhcpLeaseCacheEnabled */, false /* isDhcpRapidCommitEnabled */, + mtu, false /* isDhcpIpConflictDetectEnabled */); assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, mtu); if (shouldChangeMtu) { @@ -598,7 +659,8 @@ public class IpClientIntegrationTest { ArgumentCaptor.forClass(InterfaceConfigurationParcel.class); startIpClientProvisioning(true /* isDhcpLeaseCacheEnabled */, - isDhcpRapidCommitEnabled, true /* isDhcpPreConnectionEnabled */); + isDhcpRapidCommitEnabled, true /* isDhcpPreConnectionEnabled */, + false /* isDhcpIpConflictDetectEnabled */); verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)) .onPreconnectionStart(l2PacketList.capture()); final byte[] payload = l2PacketList.getValue().get(0).payload; @@ -635,10 +697,85 @@ public class IpClientIntegrationTest { assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU); } + private ArpPacket getNextArpPacket(final int timeout) throws Exception { + byte[] packet; + while ((packet = mPacketReader.popPacket(timeout)) != null) { + final ArpPacket arpPacket = parseArpPacketOrNull(packet); + if (arpPacket != null) return arpPacket; + } + return null; + } + + private ArpPacket getNextArpPacket() throws Exception { + final ArpPacket packet = getNextArpPacket(PACKET_TIMEOUT_MS); + assertNotNull("No expected ARP packet received on interface within timeout", packet); + return packet; + } + + private void assertArpPacket(final ArpPacket packet) { + assertEquals(packet.opCode, ARP_REQUEST); + assertEquals(packet.targetIp, CLIENT_ADDR); + assertTrue(Arrays.equals(packet.senderHwAddress.toByteArray(), mClientMac)); + } + + private void assertArpProbe(final ArpPacket packet) { + assertArpPacket(packet); + assertEquals(packet.senderIp, INADDR_ANY); + } + + private void assertArpAnnounce(final ArpPacket packet) { + assertArpPacket(packet); + assertEquals(packet.senderIp, CLIENT_ADDR); + } + + private void doIpAddressConflictDetectionTest(final boolean causeIpAddressConflict, + final boolean isDhcpRapidCommitEnabled, final boolean isDhcpIpConflictDetectEnabled, + final boolean shouldResponseArpReply) throws Exception { + final long currentTime = System.currentTimeMillis(); + + performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S, + true /* isDhcpLeaseCacheEnabled */, isDhcpRapidCommitEnabled, + TEST_DEFAULT_MTU, isDhcpIpConflictDetectEnabled); + + // If we receive an ARP packet here, it's guaranteed to be from IP conflict detection, + // because at this time the test interface does not have an IP address and therefore + // won't send ARP for anything. + if (causeIpAddressConflict) { + final ArpPacket arpProbe = getNextArpPacket(); + assertArpProbe(arpProbe); + + if (shouldResponseArpReply) { + sendArpReply(mClientMac); + } else { + sendArpProbe(); + } + final DhcpPacket packet = getNextDhcpPacket(); + assertTrue(packet instanceof DhcpDeclinePacket); + assertEquals(packet.mServerIdentifier, SERVER_ADDR); + assertEquals(packet.mRequestedIp, CLIENT_ADDR); + assertIpMemoryNeverStoreNetworkAttributes(); + } else if (isDhcpIpConflictDetectEnabled) { + int arpPacketCount = 0; + final List<ArpPacket> packetList = new ArrayList<ArpPacket>(); + // Total sent ARP packets should be 5 (3 ARP Probes + 2 ARP Announcements) + ArpPacket packet; + while ((packet = getNextArpPacket(TEST_TIMEOUT_MS)) != null) { + packetList.add(packet); + } + assertEquals(5, packetList.size()); + assertArpProbe(packetList.get(0)); + assertArpAnnounce(packetList.get(3)); + + assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, + TEST_DEFAULT_MTU); + } + } + @Test public void testDhcpInit() throws Exception { startIpClientProvisioning(false /* isDhcpLeaseCacheEnabled */, - false /* isDhcpRapidCommitEnabled */, false /* isPreconnectionEnabled */); + false /* isDhcpRapidCommitEnabled */, false /* isPreconnectionEnabled */, + false /* isDhcpIpConflictDetectEnabled */); final DhcpPacket packet = getNextDhcpPacket(); assertTrue(packet instanceof DhcpDiscoverPacket); } @@ -648,7 +785,7 @@ public class IpClientIntegrationTest { final long currentTime = System.currentTimeMillis(); performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */, false /* isDhcpRapidCommitEnabled */, - TEST_DEFAULT_MTU); + TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */); assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU); } @@ -656,7 +793,7 @@ public class IpClientIntegrationTest { public void testHandleFailureDhcpLease() throws Exception { performDhcpHandshake(false /* isSuccessLease */, TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */, false /* isDhcpRapidCommitEnabled */, - TEST_DEFAULT_MTU); + TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */); assertIpMemoryNeverStoreNetworkAttributes(); } @@ -665,7 +802,7 @@ public class IpClientIntegrationTest { final long currentTime = System.currentTimeMillis(); performDhcpHandshake(true /* isSuccessLease */, INFINITE_LEASE, true /* isDhcpLeaseCacheEnabled */, false /* isDhcpRapidCommitEnabled */, - TEST_DEFAULT_MTU); + TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */); assertIpMemoryStoreNetworkAttributes(INFINITE_LEASE, currentTime, TEST_DEFAULT_MTU); } @@ -674,7 +811,7 @@ public class IpClientIntegrationTest { final long currentTime = System.currentTimeMillis(); performDhcpHandshake(true /* isSuccessLease */, null /* no lease time */, true /* isDhcpLeaseCacheEnabled */, false /* isDhcpRapidCommitEnabled */, - TEST_DEFAULT_MTU); + TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */); assertIpMemoryStoreNetworkAttributes(null, currentTime, TEST_DEFAULT_MTU); } @@ -682,7 +819,7 @@ public class IpClientIntegrationTest { public void testHandleDisableInitRebootState() throws Exception { performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S, false /* isDhcpLeaseCacheEnabled */, false /* isDhcpRapidCommitEnabled */, - TEST_DEFAULT_MTU); + TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */); assertIpMemoryNeverStoreNetworkAttributes(); } @@ -693,7 +830,7 @@ public class IpClientIntegrationTest { final long currentTime = System.currentTimeMillis(); performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */, true /* isDhcpRapidCommitEnabled */, - TEST_DEFAULT_MTU); + TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */); assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU); } @@ -756,7 +893,8 @@ public class IpClientIntegrationTest { @Test public void testDhcpClientRapidCommitEnabled() throws Exception { startIpClientProvisioning(true /* isDhcpLeaseCacheEnabled */, - true /* isDhcpRapidCommitEnabled */, false /* isPreconnectionEnabled */); + true /* isDhcpRapidCommitEnabled */, false /* isPreconnectionEnabled */, + false /* isDhcpIpConflictDetectEnabled */); final DhcpPacket packet = getNextDhcpPacket(); assertTrue(packet instanceof DhcpDiscoverPacket); } @@ -984,7 +1122,7 @@ public class IpClientIntegrationTest { final long currentTime = System.currentTimeMillis(); performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */, false /* isDhcpRapidCommitEnabled */, - TEST_DEFAULT_MTU); + TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */); assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU); ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class); @@ -1011,7 +1149,7 @@ public class IpClientIntegrationTest { // start IpClient again and should enter Clearing State and wait for the message from kernel performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S, true /* isDhcpLeaseCacheEnabled */, false /* isDhcpRapidCommitEnabled */, - TEST_DEFAULT_MTU); + TEST_DEFAULT_MTU, false /* isDhcpIpConflictDetectEnabled */); verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture()); lp = captor.getValue(); @@ -1049,4 +1187,67 @@ public class IpClientIntegrationTest { doIpClientProvisioningWithPreconnectionTest(true /* isDhcpRapidCommitEnabled */, true /* shouldAbortPreconnection */); } + + public void testDhcpDecline_conflictByArpReply() throws Exception { + doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */, + false /* isDhcpRapidCommitEnabled */, true /* isDhcpIpConflictDetectEnabled */, + true /* shouldResponseArpReply */); + } + + @Test + public void testDhcpDecline_conflictByArpProbe() throws Exception { + doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */, + false /* isDhcpRapidCommitEnabled */, true /* isDhcpIpConflictDetectEnabled */, + false /* shouldResponseArpReply */); + } + + @Test + public void testDhcpDecline_EnableFlagWithoutIpConflict() throws Exception { + doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */, + false /* isDhcpRapidCommitEnabled */, true /* isDhcpIpConflictDetectEnabled */, + false /* shouldResponseArpReply */); + } + + @Test + public void testDhcpDecline_WithoutIpConflict() throws Exception { + doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */, + false /* isDhcpRapidCommitEnabled */, false /* isDhcpIpConflictDetectEnabled */, + false /* shouldResponseArpReply */); + } + + // So far Rapid Commit option has not been built within DHCPACK packet (implemention at + // aosp/1092270), hence, client can't deal with DHCPACK in INIT state correctly and drop + // the received DHCPACK, which causes the following verify checks fails. After checking in + // aosp/1092270, remove all below @Ignore annotations. + @Ignore + @Test + public void testDhcpDecline_WithRapidCommitWithoutIpConflict() throws Exception { + doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */, + true /* isDhcpRapidCommitEnabled */, false /* isDhcpIpConflictDetectEnabled */, + false /* shouldResponseArpReply */); + } + + @Ignore + @Test + public void testDhcpDecline_WithRapidCommitConflictByArpReply() throws Exception { + doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */, + true /* isDhcpRapidCommitEnabled */, true /* isDhcpIpConflictDetectEnabled */, + true /* shouldResponseArpReply */); + } + + @Ignore + @Test + public void testDhcpDecline_WithRapidCommitConflictByArpProbe() throws Exception { + doIpAddressConflictDetectionTest(true /* causeIpAddressConflict */, + true /* isDhcpRapidCommitEnabled */, true /* isDhcpIpConflictDetectEnabled */, + false /* shouldResponseArpReply */); + } + + @Ignore + @Test + public void testDhcpDecline_EnableFlagWithRapidCommitWithoutIpConflict() throws Exception { + doIpAddressConflictDetectionTest(false /* causeIpAddressConflict */, + true /* isDhcpRapidCommitEnabled */, true /* isDhcpIpConflictDetectEnabled */, + false /* shouldResponseArpReply */); + } } diff --git a/tests/unit/src/com/android/networkstack/arp/ArpPacketTest.java b/tests/unit/src/com/android/networkstack/arp/ArpPacketTest.java new file mode 100644 index 0000000..18f4e04 --- /dev/null +++ b/tests/unit/src/com/android/networkstack/arp/ArpPacketTest.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.arp; + +import static com.android.server.util.NetworkStackConstants.ARP_REQUEST; +import static com.android.server.util.NetworkStackConstants.ETHER_ADDR_LEN; +import static com.android.testutils.MiscAssertsKt.assertThrows; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import android.net.InetAddresses; +import android.net.MacAddress; +import android.net.dhcp.DhcpPacket; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet4Address; +import java.nio.ByteBuffer; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public final class ArpPacketTest { + + private static final Inet4Address TEST_IPV4_ADDR = + (Inet4Address) InetAddresses.parseNumericAddress("192.168.1.2"); + private static final Inet4Address INADDR_ANY = + (Inet4Address) InetAddresses.parseNumericAddress("0.0.0.0"); + private static final byte[] TEST_SENDER_MAC_ADDR = new byte[] { + 0x00, 0x1a, 0x11, 0x22, 0x33, 0x33 }; + private static final byte[] TEST_TARGET_MAC_ADDR = new byte[] { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + private static final byte[] TEST_ARP_PROBE = new byte[] { + // dst mac address + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // src mac address + (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x33, + // ether type + (byte) 0x08, (byte) 0x06, + // hardware type + (byte) 0x00, (byte) 0x01, + // protocol type + (byte) 0x08, (byte) 0x00, + // hardware address size + (byte) 0x06, + // protocol address size + (byte) 0x04, + // opcode + (byte) 0x00, (byte) 0x01, + // sender mac address + (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x33, + // sender IP address + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // target mac address + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // target IP address + (byte) 0xc0, (byte) 0xa8, (byte) 0x01, (byte) 0x02, + }; + + private static final byte[] TEST_ARP_ANNOUNCE = new byte[] { + // dst mac address + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // src mac address + (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x33, + // ether type + (byte) 0x08, (byte) 0x06, + // hardware type + (byte) 0x00, (byte) 0x01, + // protocol type + (byte) 0x08, (byte) 0x00, + // hardware address size + (byte) 0x06, + // protocol address size + (byte) 0x04, + // opcode + (byte) 0x00, (byte) 0x01, + // sender mac address + (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x33, + // sender IP address + (byte) 0xc0, (byte) 0xa8, (byte) 0x01, (byte) 0x02, + // target mac address + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + // target IP address + (byte) 0xc0, (byte) 0xa8, (byte) 0x01, (byte) 0x02, + }; + + private static final byte[] TEST_ARP_PROBE_TRUNCATED = new byte[] { + // dst mac address + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // src mac address + (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x33, + // ether type + (byte) 0x08, (byte) 0x06, + // hardware type + (byte) 0x00, (byte) 0x01, + // protocol type + (byte) 0x08, (byte) 0x00, + // hardware address size + (byte) 0x06, + // protocol address size + (byte) 0x04, + // opcode + (byte) 0x00, + }; + + private static final byte[] TEST_ARP_PROBE_TRUNCATED_MAC = new byte[] { + // dst mac address + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // src mac address + (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x33, + // ether type + (byte) 0x08, (byte) 0x06, + // hardware type + (byte) 0x00, (byte) 0x01, + // protocol type + (byte) 0x08, (byte) 0x00, + // hardware address size + (byte) 0x06, + // protocol address size + (byte) 0x04, + // opcode + (byte) 0x00, (byte) 0x01, + // sender mac address + (byte) 0x00, (byte) 0x1a, (byte) 0x11, (byte) 0x22, (byte) 0x33, + }; + + @Test + public void testBuildArpProbePacket() throws Exception { + final ByteBuffer arpProbe = ArpPacket.buildArpPacket(DhcpPacket.ETHER_BROADCAST, + TEST_SENDER_MAC_ADDR, TEST_IPV4_ADDR.getAddress(), new byte[ETHER_ADDR_LEN], + INADDR_ANY.getAddress(), (short) ARP_REQUEST); + assertArrayEquals(arpProbe.array(), TEST_ARP_PROBE); + } + + @Test + public void testBuildArpAnnouncePacket() throws Exception { + final ByteBuffer arpAnnounce = ArpPacket.buildArpPacket(DhcpPacket.ETHER_BROADCAST, + TEST_SENDER_MAC_ADDR, TEST_IPV4_ADDR.getAddress(), new byte[ETHER_ADDR_LEN], + TEST_IPV4_ADDR.getAddress(), (short) ARP_REQUEST); + assertArrayEquals(arpAnnounce.array(), TEST_ARP_ANNOUNCE); + } + + @Test + public void testParseArpProbePacket() throws Exception { + final ArpPacket packet = ArpPacket.parseArpPacket(TEST_ARP_PROBE, TEST_ARP_PROBE.length); + assertEquals(packet.opCode, ARP_REQUEST); + assertEquals(packet.senderHwAddress, MacAddress.fromBytes(TEST_SENDER_MAC_ADDR)); + assertEquals(packet.targetHwAddress, MacAddress.fromBytes(TEST_TARGET_MAC_ADDR)); + assertEquals(packet.senderIp, INADDR_ANY); + assertEquals(packet.targetIp, TEST_IPV4_ADDR); + } + + @Test + public void testParseArpAnnouncePacket() throws Exception { + final ArpPacket packet = ArpPacket.parseArpPacket(TEST_ARP_ANNOUNCE, + TEST_ARP_ANNOUNCE.length); + assertEquals(packet.opCode, ARP_REQUEST); + assertEquals(packet.senderHwAddress, MacAddress.fromBytes(TEST_SENDER_MAC_ADDR)); + assertEquals(packet.targetHwAddress, MacAddress.fromBytes(TEST_TARGET_MAC_ADDR)); + assertEquals(packet.senderIp, TEST_IPV4_ADDR); + assertEquals(packet.targetIp, TEST_IPV4_ADDR); + } + + @Test + public void testParseArpPacket_invalidByteBufferParameters() throws Exception { + assertThrows(ArpPacket.ParseException.class, () -> ArpPacket.parseArpPacket( + TEST_ARP_PROBE, 0)); + } + + @Test + public void testParseArpPacket_truncatedPacket() throws Exception { + assertThrows(ArpPacket.ParseException.class, () -> ArpPacket.parseArpPacket( + TEST_ARP_PROBE_TRUNCATED, TEST_ARP_PROBE_TRUNCATED.length)); + } + + @Test + public void testParseArpPacket_truncatedMacAddress() throws Exception { + assertThrows(ArpPacket.ParseException.class, () -> ArpPacket.parseArpPacket( + TEST_ARP_PROBE_TRUNCATED_MAC, TEST_ARP_PROBE_TRUNCATED.length)); + } +} |