Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 757d9ba5 authored by Xiao Ma's avatar Xiao Ma Committed by Automerger Merge Worker
Browse files

Merge "Support MirrorLink DHCPDECLINE." am: d9fefb8d am: f03295bf

Change-Id: I3af1d089de8214a37d92fb23490550eee9c2a280
parents 1e2e377a f03295bf
Loading
Loading
Loading
Loading
+20 −2
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import android.util.ArrayMap;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import java.net.Inet4Address;
import java.util.ArrayList;
@@ -158,7 +159,7 @@ class DhcpLeaseRepository {
    /**
     * From a map keyed by {@link Inet4Address}, remove entries where the key is invalid (as
     * specified by {@link #isValidAddress(Inet4Address)}), or is a reserved address.
     * @return true iff at least one entry was removed.
     * @return true if and only if at least one entry was removed.
     */
    private <T> boolean cleanMap(Map<Inet4Address, T> map) {
        final Iterator<Entry<Inet4Address, T>> it = map.entrySet().iterator();
@@ -397,7 +398,8 @@ class DhcpLeaseRepository {
        mEventCallbacks.finishBroadcast();
    }

    public void markLeaseDeclined(@NonNull Inet4Address addr) {
    @VisibleForTesting
    void markLeaseDeclined(@NonNull Inet4Address addr) {
        if (mDeclinedAddrs.containsKey(addr) || !isValidAddress(addr)) {
            mLog.logf("Not marking %s as declined: already declined or not assignable",
                    inet4AddrToString(addr));
@@ -409,6 +411,22 @@ class DhcpLeaseRepository {
        maybeUpdateEarliestExpiration(expTime);
    }

    /**
     * Mark a committed lease matching the passed in clientId and hardware address parameters to be
     * declined, and delete it from the repository.
     *
     * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC}
     * @param hwAddr client's mac address
     * @param Addr IPv4 address to be declined
     * @return true if a lease matching parameters was removed from committed repository.
     */
    public boolean markAndReleaseDeclinedLease(@Nullable byte[] clientId,
            @NonNull MacAddress hwAddr, @NonNull Inet4Address addr) {
        if (!releaseLease(clientId, hwAddr, addr)) return false;
        markLeaseDeclined(addr);
        return true;
    }

    /**
     * Get the list of currently valid committed leases in the repository.
     */
+158 −83
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import static java.lang.Integer.toUnsignedLong;

import android.content.Context;
import android.net.INetworkStackStatusCallback;
import android.net.IpPrefix;
import android.net.MacAddress;
import android.net.TrafficStats;
import android.net.util.NetworkStackUtils;
@@ -96,7 +97,8 @@ public class DhcpServer extends StateMachine {
    private static final int CMD_START_DHCP_SERVER = 1;
    private static final int CMD_STOP_DHCP_SERVER = 2;
    private static final int CMD_UPDATE_PARAMS = 3;
    private static final int CMD_SUSPEND_DHCP_SERVER = 4;
    @VisibleForTesting
    protected static final int CMD_RECEIVE_PACKET = 4;

    @NonNull
    private final Context mContext;
@@ -126,6 +128,8 @@ public class DhcpServer extends StateMachine {
    private final StoppedState mStoppedState = new StoppedState();
    private final StartedState mStartedState = new StartedState();
    private final RunningState mRunningState = new RunningState();
    private final WaitBeforeRetrievalState mWaitBeforeRetrievalState =
            new WaitBeforeRetrievalState();

    /**
     * Clock to be used by DhcpServer to track time for lease expiration.
@@ -262,6 +266,7 @@ public class DhcpServer extends StateMachine {
        addState(mStoppedState);
        addState(mStartedState);
            addState(mRunningState, mStartedState);
            addState(mWaitBeforeRetrievalState, mStartedState);
        // CHECKSTYLE:ON IndentationCheck

        setInitialState(mStoppedState);
@@ -372,6 +377,17 @@ public class DhcpServer extends StateMachine {
        }
    }

    private void handleUpdateServingParams(@NonNull DhcpServingParams params,
            @Nullable INetworkStackStatusCallback cb) {
        mServingParams = params;
        mLeaseRepo.updateParams(
                DhcpServingParams.makeIpPrefix(params.serverAddr),
                params.excludedAddrs,
                params.dhcpLeaseTimeSecs * 1000,
                params.singleClientAddr);
        maybeNotifyStatus(cb, STATUS_SUCCESS);
    }

    class StoppedState extends State {
        private INetworkStackStatusCallback mOnStopCallback;

@@ -422,6 +438,8 @@ public class DhcpServer extends StateMachine {
                mLeaseRepo.addLeaseCallbacks(mEventCallbacks);
            }
            maybeNotifyStatus(mOnStartCallback, STATUS_SUCCESS);
            // Clear INetworkStackStatusCallback binder token, so that it's freed
            // on the other side.
            mOnStartCallback = null;
        }

@@ -431,14 +449,7 @@ public class DhcpServer extends StateMachine {
                case CMD_UPDATE_PARAMS:
                    final Pair<DhcpServingParams, INetworkStackStatusCallback> pair =
                            (Pair<DhcpServingParams, INetworkStackStatusCallback>) msg.obj;
                    final DhcpServingParams params = pair.first;
                    mServingParams = params;
                    mLeaseRepo.updateParams(
                            DhcpServingParams.makeIpPrefix(params.serverAddr),
                            params.excludedAddrs,
                            params.dhcpLeaseTimeSecs * 1000,
                            params.singleClientAddr);
                    maybeNotifyStatus(pair.second, STATUS_SUCCESS);
                    handleUpdateServingParams(pair.first, pair.second);
                    return HANDLED;

                case CMD_START_DHCP_SERVER:
@@ -466,26 +477,19 @@ public class DhcpServer extends StateMachine {
        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_SUSPEND_DHCP_SERVER:
                    // TODO: transition to the state which waits for IpServer to reconfigure the
                    // new selected prefix.
                case CMD_RECEIVE_PACKET:
                    processPacket((DhcpPacket) msg.obj);
                    return HANDLED;

                default:
                    // Fall through to StartedState.
                    return NOT_HANDLED;
            }
        }
    }

    @VisibleForTesting
    void processPacket(@NonNull DhcpPacket packet, int srcPort) {
        final String packetType = packet.getClass().getSimpleName();
        if (srcPort != DHCP_CLIENT) {
            mLog.logf("Ignored packet of type %s sent from client port %d", packetType, srcPort);
            return;
        }
        private void processPacket(@NonNull DhcpPacket packet) {
            mLog.log("Received packet of type " + packet.getClass().getSimpleName());

        mLog.log("Received packet of type " + packetType);
            final Inet4Address sid = packet.mServerIdentifier;
            if (sid != null && !sid.equals(mServingParams.serverAddr.getAddress())) {
                mLog.log("Packet ignored due to wrong server identifier: " + sid);
@@ -499,6 +503,8 @@ public class DhcpServer extends StateMachine {
                    processRequest((DhcpRequestPacket) packet);
                } else if (packet instanceof DhcpReleasePacket) {
                    processRelease((DhcpReleasePacket) packet);
                } else if (packet instanceof DhcpDeclinePacket) {
                    processDecline((DhcpDeclinePacket) packet);
                } else {
                    mLog.e("Unknown packet type: " + packet.getClass().getSimpleName());
                }
@@ -519,8 +525,8 @@ public class DhcpServer extends StateMachine {
            final MacAddress clientMac = getMacAddr(packet);
            try {
                if (mDhcpRapidCommitEnabled && packet.mRapidCommit) {
                lease = mLeaseRepo.getCommittedLease(packet.getExplicitClientIdOrNull(), clientMac,
                        packet.mRelayIp, packet.mHostName);
                    lease = mLeaseRepo.getCommittedLease(packet.getExplicitClientIdOrNull(),
                            clientMac, packet.mRelayIp, packet.mHostName);
                    transmitAck(packet, lease, clientMac);
                } else {
                    lease = mLeaseRepo.getOffer(packet.getExplicitClientIdOrNull(), clientMac,
@@ -534,7 +540,8 @@ public class DhcpServer extends StateMachine {
            }
        }

    private void processRequest(@NonNull DhcpRequestPacket packet) throws MalformedPacketException {
        private void processRequest(@NonNull DhcpRequestPacket packet)
                throws MalformedPacketException {
            // If set, packet SID matches with this server's ID as checked in processPacket().
            final boolean sidSet = packet.mServerIdentifier != null;
            final DhcpLease lease;
@@ -558,10 +565,72 @@ public class DhcpServer extends StateMachine {
                throws MalformedPacketException {
            final byte[] clientId = packet.getExplicitClientIdOrNull();
            final MacAddress macAddr = getMacAddr(packet);
        // Don't care about success (there is no ACK/NAK); logging is already done in the repository
            // Don't care about success (there is no ACK/NAK); logging is already done
            // in the repository.
            mLeaseRepo.releaseLease(clientId, macAddr, packet.mClientIp);
        }

        private void processDecline(@NonNull DhcpDeclinePacket packet)
                throws MalformedPacketException {
            final byte[] clientId = packet.getExplicitClientIdOrNull();
            final MacAddress macAddr = getMacAddr(packet);
            int committedLeasesCount = mLeaseRepo.getCommittedLeases().size();

            // If peer's clientID and macAddr doesn't match with any issued lease, nothing to do.
            if (!mLeaseRepo.markAndReleaseDeclinedLease(clientId, macAddr, packet.mRequestedIp)) {
                return;
            }

            // Check whether the boolean flag which requests a new prefix is enabled, and if
            // it's enabled, make sure the issued lease count should be only one, otherwise,
            // changing a different prefix will cause other exist host(s) configured with the
            // current prefix lose appropriate route.
            if (!mServingParams.changePrefixOnDecline || committedLeasesCount > 1) return;

            if (mEventCallbacks == null) {
                mLog.e("changePrefixOnDecline enabled but caller didn't pass a valid"
                        + "IDhcpEventCallbacks callback.");
                return;
            }

            try {
                mEventCallbacks.onNewPrefixRequest(
                        DhcpServingParams.makeIpPrefix(mServingParams.serverAddr));
                transitionTo(mWaitBeforeRetrievalState);
            } catch (RemoteException e) {
                mLog.e("could not request a new prefix to caller", e);
            }
        }
    }

    class WaitBeforeRetrievalState extends State {
        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_UPDATE_PARAMS:
                    final Pair<DhcpServingParams, INetworkStackStatusCallback> pair =
                            (Pair<DhcpServingParams, INetworkStackStatusCallback>) msg.obj;
                    final IpPrefix currentPrefix =
                            DhcpServingParams.makeIpPrefix(mServingParams.serverAddr);
                    final IpPrefix newPrefix =
                            DhcpServingParams.makeIpPrefix(pair.first.serverAddr);
                    handleUpdateServingParams(pair.first, pair.second);
                    if (currentPrefix != null && !currentPrefix.equals(newPrefix)) {
                        transitionTo(mRunningState);
                    }
                    return HANDLED;

                case CMD_RECEIVE_PACKET:
                    deferMessage(msg);
                    return HANDLED;

                default:
                    // Fall through to StartedState.
                    return NOT_HANDLED;
            }
        }
    }

    private Inet4Address getAckOrOfferDst(@NonNull DhcpPacket request, @NonNull DhcpLease lease,
            boolean broadcastFlag) {
        // Unless relayed or broadcast, send to client IP if already configured on the client, or to
@@ -748,7 +817,13 @@ public class DhcpServer extends StateMachine {
        @Override
        protected void onReceive(@NonNull DhcpPacket packet, @NonNull Inet4Address srcAddr,
                int srcPort) {
            processPacket(packet, srcPort);
            if (srcPort != DHCP_CLIENT) {
                final String packetType = packet.getClass().getSimpleName();
                mLog.logf("Ignored packet of type %s sent from client port %d",
                        packetType, srcPort);
                return;
            }
            sendMessage(CMD_RECEIVE_PACKET, packet);
        }

        @Override
+121 −23
Original line number Diff line number Diff line
@@ -17,12 +17,13 @@
package android.net.dhcp;

import static android.net.InetAddresses.parseNumericAddress;
import static android.net.dhcp.DhcpPacket.DHCP_CLIENT;
import static android.net.dhcp.DhcpPacket.DHCP_HOST_NAME;
import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP;
import static android.net.dhcp.DhcpPacket.INADDR_ANY;
import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST;
import static android.net.dhcp.DhcpServer.CMD_RECEIVE_PACKET;
import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
import static android.net.shared.Inet4AddressUtils.inet4AddressToIntHTH;
import static android.net.util.NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION;

import static junit.framework.Assert.assertEquals;
@@ -31,7 +32,6 @@ import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doNothing;
@@ -44,12 +44,14 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.INetworkStackStatusCallback;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.MacAddress;
import android.net.dhcp.DhcpLeaseRepository.InvalidAddressException;
import android.net.dhcp.DhcpLeaseRepository.OutOfAddressesException;
import android.net.dhcp.DhcpServer.Clock;
import android.net.dhcp.DhcpServer.Dependencies;
import android.net.shared.Inet4AddressUtils;
import android.net.util.SharedLog;
import android.testing.AndroidTestingRunner;

@@ -69,6 +71,8 @@ import org.mockito.MockitoAnnotations;
import java.net.Inet4Address;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@@ -156,6 +160,7 @@ public class DhcpServerTest {
                .setServerAddr(TEST_SERVER_LINKADDR)
                .setLinkMtu(TEST_MTU)
                .setExcludedAddrs(TEST_EXCLUDED_ADDRS)
                .setChangePrefixOnDecline(false)
                .build();
    }

@@ -215,7 +220,8 @@ public class DhcpServerTest {
        final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID,
                (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
                false /* broadcast */, INADDR_ANY /* srcIp */, false /* rapidCommit */);
        mServer.processPacket(discover, DHCP_CLIENT);
        mServer.sendMessage(CMD_RECEIVE_PACKET, discover);
        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);

        assertResponseSentTo(TEST_CLIENT_ADDR);
        final DhcpOfferPacket packet = assertOffer(getPacket());
@@ -233,7 +239,8 @@ public class DhcpServerTest {
        final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID,
                (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
                false /* broadcast */, INADDR_ANY /* srcIp */, true /* rapidCommit */);
        mServer.processPacket(discover, DHCP_CLIENT);
        mServer.sendMessage(CMD_RECEIVE_PACKET, discover);
        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);

        assertResponseSentTo(TEST_CLIENT_ADDR);
        final DhcpAckPacket packet = assertAck(getPacket());
@@ -251,7 +258,8 @@ public class DhcpServerTest {
        final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID,
                (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
                false /* broadcast */, INADDR_ANY /* srcIp */, false /* rapidCommit */);
        mServer.processPacket(discover, DHCP_CLIENT);
        mServer.sendMessage(CMD_RECEIVE_PACKET, discover);
        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);

        assertResponseSentTo(INADDR_BROADCAST);
        final DhcpNakPacket packet = assertNak(getPacket());
@@ -279,7 +287,8 @@ public class DhcpServerTest {
        final DhcpRequestPacket request = makeRequestSelectingPacket();
        request.mHostName = TEST_HOSTNAME;
        request.mRequestedParams = new byte[] { DHCP_HOST_NAME };
        mServer.processPacket(request, DHCP_CLIENT);
        mServer.sendMessage(CMD_RECEIVE_PACKET, request);
        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);

        assertResponseSentTo(TEST_CLIENT_ADDR);
        final DhcpAckPacket packet = assertAck(getPacket());
@@ -296,25 +305,14 @@ public class DhcpServerTest {
                .thenThrow(new InvalidAddressException("Test error"));

        final DhcpRequestPacket request = makeRequestSelectingPacket();
        mServer.processPacket(request, DHCP_CLIENT);
        mServer.sendMessage(CMD_RECEIVE_PACKET, request);
        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);

        assertResponseSentTo(INADDR_BROADCAST);
        final DhcpNakPacket packet = assertNak(getPacket());
        assertMatchesClient(packet);
    }

    @Test
    public void testRequest_Selecting_WrongClientPort() throws Exception {
        startServer();

        final DhcpRequestPacket request = makeRequestSelectingPacket();
        mServer.processPacket(request, 50000);

        verify(mRepository, never())
                .requestLease(any(), any(), any(), any(), any(), anyBoolean(), any());
        verify(mDeps, never()).sendPacket(any(), any(), any());
    }

    @Test
    public void testRelease() throws Exception {
        startServer();
@@ -322,28 +320,128 @@ public class DhcpServerTest {
        final DhcpReleasePacket release = new DhcpReleasePacket(TEST_TRANSACTION_ID,
                TEST_SERVER_ADDR, TEST_CLIENT_ADDR,
                INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES);
        mServer.processPacket(release, DHCP_CLIENT);
        mServer.sendMessage(CMD_RECEIVE_PACKET, release);
        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);

        verify(mRepository, times(1))
                .releaseLease(isNull(), eq(TEST_CLIENT_MAC), eq(TEST_CLIENT_ADDR));
    }

    @Test
    public void testDecline_LeaseDoesNotExist() throws Exception {
        when(mRepository.markAndReleaseDeclinedLease(isNull(), eq(TEST_CLIENT_MAC),
                eq(TEST_CLIENT_ADDR))).thenReturn(false);

        startServer();
        runOnReceivedDeclinePacket();
        verify(mEventCallbacks, never()).onNewPrefixRequest(any());
    }

    private void runOnReceivedDeclinePacket() throws Exception {
        when(mRepository.getCommittedLeases()).thenReturn(
                Arrays.asList(new DhcpLease(null, TEST_CLIENT_MAC, TEST_CLIENT_ADDR,
                        TEST_PREFIX_LENGTH, TEST_LEASE_EXPTIME_SECS * 1000L + TEST_CLOCK_TIME,
                        TEST_HOSTNAME)));
        final DhcpDeclinePacket decline = new DhcpDeclinePacket(TEST_TRANSACTION_ID,
                (short) 0 /* secs */, INADDR_ANY /* clientIp */, INADDR_ANY /* yourIp */,
                INADDR_ANY /* nextIp */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
                TEST_CLIENT_ADDR /* requestedIp */, TEST_SERVER_ADDR /* serverIdentifier */);
        mServer.sendMessage(CMD_RECEIVE_PACKET, decline);
        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);

        verify(mRepository).markAndReleaseDeclinedLease(isNull(), eq(TEST_CLIENT_MAC),
                eq(TEST_CLIENT_ADDR));
    }

    private int[] toIntArray(@NonNull Collection<Inet4Address> addrs) {
        return addrs.stream().mapToInt(Inet4AddressUtils::inet4AddressToIntHTH).toArray();
    }

    private void updateServingParams(Set<Inet4Address> defaultRouters,
            Set<Inet4Address> dnsServers, Set<Inet4Address> excludedAddrs, LinkAddress serverAddr,
            boolean changePrefixOnDecline) throws Exception {
        final DhcpServingParamsParcel params = new DhcpServingParamsParcel();
        params.serverAddr = inet4AddressToIntHTH((Inet4Address) serverAddr.getAddress());
        params.serverAddrPrefixLength = serverAddr.getPrefixLength();
        params.defaultRouters = toIntArray(defaultRouters);
        params.dnsServers = toIntArray(dnsServers);
        params.excludedAddrs = toIntArray(excludedAddrs);
        params.dhcpLeaseTimeSecs = TEST_LEASE_EXPTIME_SECS;
        params.linkMtu = TEST_MTU;
        params.metered = true;
        params.changePrefixOnDecline = changePrefixOnDecline;

        mServer.updateParams(params, mAssertSuccessCallback);
        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
    }

    @Test
    public void testChangePrefixOnDecline() throws Exception {
        when(mRepository.markAndReleaseDeclinedLease(isNull(), eq(TEST_CLIENT_MAC),
                eq(TEST_CLIENT_ADDR))).thenReturn(true);

        mServer.start(mAssertSuccessCallback, mEventCallbacks);
        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
        verify(mRepository).addLeaseCallbacks(eq(mEventCallbacks));

        // Enable changePrefixOnDecline
        updateServingParams(TEST_DEFAULT_ROUTERS, TEST_DNS_SERVERS, TEST_EXCLUDED_ADDRS,
                TEST_SERVER_LINKADDR, true /* changePrefixOnDecline */);

        runOnReceivedDeclinePacket();
        final IpPrefix servingPrefix = DhcpServingParams.makeIpPrefix(TEST_SERVER_LINKADDR);
        verify(mEventCallbacks).onNewPrefixRequest(eq(servingPrefix));

        final Inet4Address serverAddr = parseAddr("192.168.51.129");
        final LinkAddress srvLinkAddr = new LinkAddress(serverAddr, 24);
        final Set<Inet4Address> srvAddr = new HashSet<>(Collections.singletonList(serverAddr));
        final Set<Inet4Address> excludedAddrs = new HashSet<>(
                Arrays.asList(parseAddr("192.168.51.200"), parseAddr("192.168.51.201")));

        // Simulate IpServer updates the serving params with a new prefix.
        updateServingParams(srvAddr, srvAddr, excludedAddrs, srvLinkAddr,
                true /* changePrefixOnDecline */);

        final Inet4Address clientAddr = parseAddr("192.168.51.42");
        final DhcpLease lease = new DhcpLease(null, TEST_CLIENT_MAC,
                clientAddr, 24 /*prefixLen*/, TEST_LEASE_EXPTIME_SECS * 1000L + TEST_CLOCK_TIME,
                null /* hostname */);
        when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC),
                eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */))
                .thenReturn(lease);

        // Test discover packet
        final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID,
                (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
                false /* broadcast */, INADDR_ANY /* srcIp */, false /* rapidCommit */);
        mServer.sendMessage(CMD_RECEIVE_PACKET, discover);
        HandlerUtilsKt.waitForIdle(mServer.getHandler(), TEST_TIMEOUT_MS);
        assertResponseSentTo(clientAddr);
        final DhcpOfferPacket packet = assertOffer(getPacket());
        assertMatchesLease(packet, serverAddr, clientAddr, null);
    }

    /* TODO: add more tests once packet construction is refactored, including:
     *  - usage of giaddr
     *  - usage of broadcast bit
     *  - other request states (init-reboot/renewing/rebinding)
     */

    private void assertMatchesTestLease(@NonNull DhcpPacket packet, @Nullable String hostname) {
    private void assertMatchesLease(@NonNull DhcpPacket packet, @NonNull Inet4Address srvAddr,
            @NonNull Inet4Address clientAddr, @Nullable String hostname) {
        assertMatchesClient(packet);
        assertFalse(packet.hasExplicitClientId());
        assertEquals(TEST_SERVER_ADDR, packet.mServerIdentifier);
        assertEquals(TEST_CLIENT_ADDR, packet.mYourIp);
        assertEquals(srvAddr, packet.mServerIdentifier);
        assertEquals(clientAddr, packet.mYourIp);
        assertNotNull(packet.mLeaseTime);
        assertEquals(TEST_LEASE_EXPTIME_SECS, (int) packet.mLeaseTime);
        assertEquals(hostname, packet.mHostName);
    }

    private void assertMatchesTestLease(@NonNull DhcpPacket packet, @Nullable String hostname) {
        assertMatchesLease(packet, TEST_SERVER_ADDR, TEST_CLIENT_ADDR, hostname);
    }

    private void assertMatchesTestLease(@NonNull DhcpPacket packet) {
        assertMatchesTestLease(packet, null);
    }