/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.net.dhcp;

import static android.net.dhcp.DhcpPacket.DHCP_CLIENT;
import static android.net.dhcp.DhcpPacket.DHCP_HOST_NAME;
import static android.net.dhcp.DhcpPacket.DHCP_SERVER;
import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP;
import static android.net.dhcp.IDhcpServer.STATUS_INVALID_ARGUMENT;
import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR;
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.IPPROTO_UDP;
import static android.system.OsConstants.SOCK_DGRAM;
import static android.system.OsConstants.SOCK_NONBLOCK;
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_BROADCAST;
import static android.system.OsConstants.SO_REUSEADDR;

import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address;
import static com.android.net.module.util.NetworkStackConstants.INFINITE_LEASE;
import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ALL;
import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
import static com.android.net.module.util.NetworkStackConstants.TAG_SYSTEM_DHCP_SERVER;
import static com.android.networkstack.util.NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION;
import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission;

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.SocketUtils;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.Os;
import android.text.TextUtils;
import android.util.Pair;

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

import com.android.internal.util.HexDump;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.util.NetworkStackUtils;

import java.io.FileDescriptor;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;

/**
 * A DHCPv4 server.
 *
 * <p>This server listens for and responds to packets on a single interface. It considers itself
 * authoritative for all leases on the subnet, which means that DHCP requests for unknown leases of
 * unknown hosts receive a reply instead of being ignored.
 *
 * <p>The server relies on StateMachine's handler (including send/receive operations): all internal
 * operations are done in StateMachine's looper. Public methods are thread-safe and will schedule
 * operations on that looper asynchronously.
 * @hide
 */
public class DhcpServer extends StateMachine {
    private static final String REPO_TAG = "Repository";

    // Lease time to transmit to client instead of a negative time in case a lease expired before
    // the server could send it (if the server process is suspended for example).
    private static final int EXPIRED_FALLBACK_LEASE_TIME_SECS = 120;

    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;
    @VisibleForTesting
    protected static final int CMD_RECEIVE_PACKET = 4;
    private static final int CMD_TERMINATE_AFTER_STOP = 5;

    @NonNull
    private final Context mContext;
    @NonNull
    private final String mIfName;
    @NonNull
    private final DhcpLeaseRepository mLeaseRepo;
    @NonNull
    private final SharedLog mLog;
    @NonNull
    private final Dependencies mDeps;
    @NonNull
    private final Clock mClock;
    @NonNull
    private DhcpServingParams mServingParams;

    @Nullable
    private DhcpPacketListener mPacketListener;
    @Nullable
    private FileDescriptor mSocket;
    @Nullable
    private IDhcpEventCallbacks mEventCallbacks;

    private final boolean mDhcpRapidCommitEnabled;

    // States.
    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.
     *
     * <p>The clock should track time as may be measured by clients obtaining a lease. It does not
     * need to be monotonous across restarts of the server as long as leases are cleared when the
     * server is stopped.
     */
    public static class Clock {
        /**
         * @see SystemClock#elapsedRealtime()
         */
        public long elapsedRealtime() {
            return SystemClock.elapsedRealtime();
        }
    }

    /**
     * Dependencies for the DhcpServer. Useful to be mocked in tests.
     */
    public interface Dependencies {
        /**
         * Send a packet to the specified datagram socket.
         *
         * @param fd File descriptor of the socket.
         * @param buffer Data to be sent.
         * @param dst Destination address of the packet.
         */
        void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer,
                @NonNull InetAddress dst) throws ErrnoException, IOException;

        /**
         * Create a DhcpLeaseRepository for the server.
         * @param servingParams Parameters used to serve DHCP requests.
         * @param log Log to be used by the repository.
         * @param clock Clock that the repository must use to track time.
         */
        DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams,
                @NonNull SharedLog log, @NonNull Clock clock);

        /**
         * Create a packet listener that will send packets to be processed.
         */
        DhcpPacketListener makePacketListener(@NonNull Handler handler);

        /**
         * Create a clock that the server will use to track time.
         */
        Clock makeClock();

        /**
         * Add an entry to the ARP cache table.
         * @param fd Datagram socket file descriptor that must use the new entry.
         */
        void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr,
                @NonNull String ifname, @NonNull FileDescriptor fd) throws IOException;

        /**
         * Check whether or not one specific experimental feature for connectivity namespace is
         * enabled.
         * @param context The global context information about an app environment.
         * @param name Specific experimental flag name.
         */
        boolean isFeatureEnabled(@NonNull Context context, @NonNull String name);

        /**
         * Check whether one specific experimental feature for connectivity namespace is not
         * disabled.
         * @param context The global context information about an app environment.
         * @param name Specific experimental flag name.
         */
        boolean isFeatureNotChickenedOut(@NonNull Context context, @NonNull String name);
    }

    private class DependenciesImpl implements Dependencies {
        @Override
        public void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer,
                @NonNull InetAddress dst) throws ErrnoException, IOException {
            Os.sendto(fd, buffer, 0, dst, DhcpPacket.DHCP_CLIENT);
        }

        @Override
        public DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams,
                @NonNull SharedLog log, @NonNull Clock clock) {
            return new DhcpLeaseRepository(
                    DhcpServingParams.makeIpPrefix(servingParams.serverAddr),
                    servingParams.excludedAddrs, servingParams.dhcpLeaseTimeSecs * 1000,
                    servingParams.singleClientAddr, servingParams.leasesSubnetPrefixLength,
                    log.forSubComponent(REPO_TAG), clock);
        }

        @Override
        public DhcpPacketListener makePacketListener(@NonNull Handler handler) {
            return new PacketListener(handler);
        }

        @Override
        public Clock makeClock() {
            return new Clock();
        }

        @Override
        public void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr,
                @NonNull String ifname, @NonNull FileDescriptor fd) throws IOException {
            NetworkStackUtils.addArpEntry(ipv4Addr, ethAddr, ifname, fd);
        }

        @Override
        public boolean isFeatureEnabled(@NonNull Context context, @NonNull String name) {
            return DeviceConfigUtils.isNetworkStackFeatureEnabled(context, name);
        }

        @Override
        public boolean isFeatureNotChickenedOut(final Context context, final String name) {
            return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(context, name);
        }
    }

    private static class MalformedPacketException extends Exception {
        MalformedPacketException(String message, Throwable t) {
            super(message, t);
        }
    }

    public DhcpServer(@NonNull Context context, @NonNull String ifName,
            @NonNull DhcpServingParams params, @NonNull SharedLog log) {
        this(context, ifName, params, log, null);
    }

    @VisibleForTesting
    DhcpServer(@NonNull Context context, @NonNull String ifName, @NonNull DhcpServingParams params,
            @NonNull SharedLog log, @Nullable Dependencies deps) {
        super(DhcpServer.class.getSimpleName() + "." + ifName);

        if (deps == null) {
            deps = new DependenciesImpl();
        }
        mContext = context;
        mIfName = ifName;
        mServingParams = params;
        mLog = log;
        mDeps = deps;
        mClock = deps.makeClock();
        mLeaseRepo = deps.makeLeaseRepository(mServingParams, mLog, mClock);
        mDhcpRapidCommitEnabled =
                deps.isFeatureNotChickenedOut(context, DHCP_RAPID_COMMIT_VERSION);

        // CHECKSTYLE:OFF IndentationCheck
        addState(mStoppedState);
        addState(mStartedState);
            addState(mRunningState, mStartedState);
            addState(mWaitBeforeRetrievalState, mStartedState);
        // CHECKSTYLE:ON IndentationCheck

        setInitialState(mStoppedState);

        super.start();
    }

    /**
     * Make a IDhcpServer connector to communicate with this DhcpServer.
     */
    public IDhcpServer makeConnector() {
        return new DhcpServerConnector();
    }

    private class DhcpServerConnector extends IDhcpServer.Stub {
        @Override
        public void start(@Nullable INetworkStackStatusCallback cb) {
            enforceNetworkStackCallingPermission();
            DhcpServer.this.start(cb);
        }

        @Override
        public void startWithCallbacks(@Nullable INetworkStackStatusCallback statusCb,
                @Nullable IDhcpEventCallbacks eventCb) {
            enforceNetworkStackCallingPermission();
            DhcpServer.this.start(statusCb, eventCb);
        }

        @Override
        public void updateParams(@Nullable DhcpServingParamsParcel params,
                @Nullable INetworkStackStatusCallback cb) {
            enforceNetworkStackCallingPermission();
            DhcpServer.this.updateParams(params, cb);
        }

        @Override
        public void stop(@Nullable INetworkStackStatusCallback cb) {
            enforceNetworkStackCallingPermission();
            DhcpServer.this.stop(cb);
        }

        @Override
        public int getInterfaceVersion() {
            return this.VERSION;
        }

        @Override
        public String getInterfaceHash() {
            return this.HASH;
        }
    }

    /**
     * Start listening for and responding to packets.
     *
     * <p>It is not legal to call this method more than once; in particular the server cannot be
     * restarted after being stopped.
     */
    void start(@Nullable INetworkStackStatusCallback cb) {
        start(cb, null);
    }

    /**
     * Start listening for and responding to packets, with optional callbacks for lease events.
     *
     * <p>It is not legal to call this method more than once; in particular the server cannot be
     * restarted after being stopped.
     */
    void start(@Nullable INetworkStackStatusCallback statusCb,
            @Nullable IDhcpEventCallbacks eventCb) {
        sendMessage(CMD_START_DHCP_SERVER, new Pair<>(statusCb, eventCb));
    }

    /**
     * Update serving parameters. All subsequently received requests will be handled with the new
     * parameters, and current leases that are incompatible with the new parameters are dropped.
     */
    void updateParams(@Nullable DhcpServingParamsParcel params,
            @Nullable INetworkStackStatusCallback cb) {
        final DhcpServingParams parsedParams;
        try {
            // throws InvalidParameterException with null params
            parsedParams = DhcpServingParams.fromParcelableObject(params);
        } catch (DhcpServingParams.InvalidParameterException e) {
            mLog.e("Invalid parameters sent to DhcpServer", e);
            maybeNotifyStatus(cb, STATUS_INVALID_ARGUMENT);
            return;
        }
        sendMessage(CMD_UPDATE_PARAMS, new Pair<>(parsedParams, cb));
    }

    /**
     * Stop listening for packets.
     *
     * <p>As the server is stopped asynchronously, some packets may still be processed shortly after
     * calling this method. The server will also be cleaned up and can't be started again, even if
     * it was already stopped.
     */
    void stop(@Nullable INetworkStackStatusCallback cb) {
        sendMessage(CMD_STOP_DHCP_SERVER, cb);
        sendMessage(CMD_TERMINATE_AFTER_STOP);
    }

    private void maybeNotifyStatus(@Nullable INetworkStackStatusCallback cb, int statusCode) {
        if (cb == null) return;
        try {
            cb.onStatusAvailable(statusCode);
        } catch (RemoteException e) {
            mLog.e("Could not send status back to caller", e);
        }
    }

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

    class StoppedState extends State {
        private INetworkStackStatusCallback mOnStopCallback;

        @Override
        public void enter() {
            maybeNotifyStatus(mOnStopCallback, STATUS_SUCCESS);
            mOnStopCallback = null;
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_START_DHCP_SERVER:
                    final Pair<INetworkStackStatusCallback, IDhcpEventCallbacks> obj =
                            (Pair<INetworkStackStatusCallback, IDhcpEventCallbacks>) msg.obj;
                    mStartedState.mOnStartCallback = obj.first;
                    mEventCallbacks = obj.second;
                    transitionTo(mRunningState);
                    return HANDLED;
                case CMD_TERMINATE_AFTER_STOP:
                    quit();
                    return HANDLED;
                default:
                    return NOT_HANDLED;
            }
        }
    }

    class StartedState extends State {
        private INetworkStackStatusCallback mOnStartCallback;

        @Override
        public void enter() {
            if (mPacketListener != null) {
                mLog.e("Starting DHCP server more than once is not supported.");
                maybeNotifyStatus(mOnStartCallback, STATUS_UNKNOWN_ERROR);
                mOnStartCallback = null;
                return;
            }
            mPacketListener = mDeps.makePacketListener(getHandler());

            if (!mPacketListener.start()) {
                mLog.e("Fail to start DHCP Packet Listener, rollback to StoppedState");
                deferMessage(obtainMessage(CMD_STOP_DHCP_SERVER, null));
                maybeNotifyStatus(mOnStartCallback, STATUS_UNKNOWN_ERROR);
                mOnStartCallback = null;
                return;
            }

            if (mEventCallbacks != null) {
                mLeaseRepo.addLeaseCallbacks(mEventCallbacks);
            }
            maybeNotifyStatus(mOnStartCallback, STATUS_SUCCESS);
            // Clear INetworkStackStatusCallback binder token, so that it's freed
            // on the other side.
            mOnStartCallback = null;
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_UPDATE_PARAMS:
                    final Pair<DhcpServingParams, INetworkStackStatusCallback> pair =
                            (Pair<DhcpServingParams, INetworkStackStatusCallback>) msg.obj;
                    handleUpdateServingParams(pair.first, pair.second);
                    return HANDLED;

                case CMD_START_DHCP_SERVER:
                    mLog.e("ALERT: START received in StartedState. Please fix caller.");
                    return HANDLED;

                case CMD_STOP_DHCP_SERVER:
                    mStoppedState.mOnStopCallback = (INetworkStackStatusCallback) msg.obj;
                    transitionTo(mStoppedState);
                    return HANDLED;

                default:
                    return NOT_HANDLED;
            }
        }

        @Override
        public void exit() {
            mPacketListener.stop();
            mLog.logf("DHCP Packet Listener stopped");
        }
    }

    class RunningState extends State {
        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_RECEIVE_PACKET:
                    processPacket((DhcpPacket) msg.obj);
                    return HANDLED;

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

        private void processPacket(@NonNull DhcpPacket packet) {
            mLog.log("Received packet of type " + packet.getClass().getSimpleName());

            final Inet4Address sid = packet.mServerIdentifier;
            if (sid != null && !sid.equals(mServingParams.serverAddr.getAddress())) {
                mLog.log("Packet ignored due to wrong server identifier: " + sid);
                return;
            }

            try {
                if (packet instanceof DhcpDiscoverPacket) {
                    processDiscover((DhcpDiscoverPacket) packet);
                } else if (packet instanceof DhcpRequestPacket) {
                    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());
                }
            } catch (MalformedPacketException e) {
                // Not an internal error: only logging exception message, not stacktrace
                mLog.e("Ignored malformed packet: " + e.getMessage());
            }
        }

        private void logIgnoredPacketInvalidSubnet(DhcpLeaseRepository.InvalidSubnetException e) {
            // Not an internal error: only logging exception message, not stacktrace
            mLog.e("Ignored packet from invalid subnet: " + e.getMessage());
        }

        private void processDiscover(@NonNull DhcpDiscoverPacket packet)
                throws MalformedPacketException {
            final DhcpLease lease;
            final MacAddress clientMac = getMacAddr(packet);
            try {
                if (mDhcpRapidCommitEnabled && packet.mRapidCommit) {
                    lease = mLeaseRepo.getCommittedLease(packet.getExplicitClientIdOrNull(),
                            clientMac, packet.mRelayIp, packet.mHostName);
                    transmitAck(packet, lease, clientMac);
                } else {
                    lease = mLeaseRepo.getOffer(packet.getExplicitClientIdOrNull(), clientMac,
                            packet.mRelayIp, packet.mRequestedIp, packet.mHostName);
                    transmitOffer(packet, lease, clientMac);
                }
            } catch (DhcpLeaseRepository.OutOfAddressesException e) {
                transmitNak(packet, "Out of addresses to offer");
            } catch (DhcpLeaseRepository.InvalidSubnetException e) {
                logIgnoredPacketInvalidSubnet(e);
            }
        }

        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;
            final MacAddress clientMac = getMacAddr(packet);
            try {
                lease = mLeaseRepo.requestLease(packet.getExplicitClientIdOrNull(), clientMac,
                        packet.mClientIp, packet.mRelayIp, packet.mRequestedIp, sidSet,
                        packet.mHostName);
            } catch (DhcpLeaseRepository.InvalidAddressException e) {
                transmitNak(packet, "Invalid requested address");
                return;
            } catch (DhcpLeaseRepository.InvalidSubnetException e) {
                logIgnoredPacketInvalidSubnet(e);
                return;
            }

            transmitAck(packet, lease, clientMac);
        }

        private void processRelease(@NonNull DhcpReleasePacket packet)
                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.
            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
        // the lease address if the client has no configured address
        if (!isEmpty(request.mRelayIp)) {
            return request.mRelayIp;
        } else if (broadcastFlag) {
            return IPV4_ADDR_ALL;
        } else if (!isEmpty(request.mClientIp)) {
            return request.mClientIp;
        } else {
            return lease.getNetAddr();
        }
    }

    /**
     * Determine whether the broadcast flag should be set in the BOOTP packet flags. This does not
     * apply to NAK responses, which should always have it set.
     */
    private static boolean getBroadcastFlag(@NonNull DhcpPacket request, @NonNull DhcpLease lease) {
        // No broadcast flag if the client already has a configured IP to unicast to. RFC2131 #4.1
        // has some contradictions regarding broadcast behavior if a client already has an IP
        // configured and sends a request with both ciaddr (renew/rebind) and the broadcast flag
        // set. Sending a unicast response to ciaddr matches previous behavior and is more
        // efficient.
        // If the client has no configured IP, broadcast if requested by the client or if the lease
        // address cannot be used to send a unicast reply either.
        return isEmpty(request.mClientIp) && (request.mBroadcast || isEmpty(lease.getNetAddr()));
    }

    /**
     * Get the hostname from a lease if non-empty and requested in the incoming request.
     * @param request The incoming request.
     * @return The hostname, or null if not requested or empty.
     */
    @Nullable
    private static String getHostnameIfRequested(@NonNull DhcpPacket request,
            @NonNull DhcpLease lease) {
        return request.hasRequestedParam(DHCP_HOST_NAME) && !TextUtils.isEmpty(lease.getHostname())
                ? lease.getHostname()
                : null;
    }

    private boolean transmitOffer(@NonNull DhcpPacket request, @NonNull DhcpLease lease,
            @NonNull MacAddress clientMac) {
        final boolean broadcastFlag = getBroadcastFlag(request, lease);
        final int timeout = getLeaseTimeout(lease);
        final Inet4Address prefixMask =
                getPrefixMaskAsInet4Address(mServingParams.serverAddr.getPrefixLength());
        final Inet4Address broadcastAddr = getBroadcastAddress(
                mServingParams.getServerInet4Addr(), mServingParams.serverAddr.getPrefixLength());
        final String hostname = getHostnameIfRequested(request, lease);
        final ByteBuffer offerPacket = DhcpPacket.buildOfferPacket(
                ENCAP_BOOTP, request.mTransId, broadcastFlag, mServingParams.getServerInet4Addr(),
                request.mRelayIp, lease.getNetAddr(), request.mClientMac, timeout, prefixMask,
                broadcastAddr, new ArrayList<>(mServingParams.defaultRouters),
                new ArrayList<>(mServingParams.dnsServers),
                mServingParams.getServerInet4Addr(), null /* domainName */, hostname,
                mServingParams.metered, (short) mServingParams.linkMtu,
                // TODO (b/144402437): advertise the URL if known
                null /* captivePortalApiUrl */);

        return transmitOfferOrAckPacket(offerPacket, DhcpOfferPacket.class.getSimpleName(), request,
                lease, clientMac, broadcastFlag);
    }

    private boolean transmitAck(@NonNull DhcpPacket packet, @NonNull DhcpLease lease,
            @NonNull MacAddress clientMac) {
        // TODO: replace DhcpPacket's build methods with real builders and use common code with
        // transmitOffer above
        final boolean broadcastFlag = getBroadcastFlag(packet, lease);
        final int timeout = getLeaseTimeout(lease);
        final String hostname = getHostnameIfRequested(packet, lease);
        final ByteBuffer ackPacket = DhcpPacket.buildAckPacket(ENCAP_BOOTP, packet.mTransId,
                broadcastFlag, mServingParams.getServerInet4Addr(), packet.mRelayIp,
                lease.getNetAddr(), packet.mClientIp, packet.mClientMac, timeout,
                mServingParams.getPrefixMaskAsAddress(), mServingParams.getBroadcastAddress(),
                new ArrayList<>(mServingParams.defaultRouters),
                new ArrayList<>(mServingParams.dnsServers),
                mServingParams.getServerInet4Addr(), null /* domainName */, hostname,
                mServingParams.metered, (short) mServingParams.linkMtu,
                // TODO (b/144402437): advertise the URL if known
                packet.mRapidCommit && mDhcpRapidCommitEnabled, null /* captivePortalApiUrl */);

        return transmitOfferOrAckPacket(ackPacket, DhcpAckPacket.class.getSimpleName(), packet,
                lease, clientMac, broadcastFlag);
    }

    private boolean transmitNak(DhcpPacket request, String message) {
        mLog.w("Transmitting NAK: " + message);
        // Always set broadcast flag for NAK: client may not have a correct IP
        final ByteBuffer nakPacket = DhcpPacket.buildNakPacket(
                ENCAP_BOOTP, request.mTransId, mServingParams.getServerInet4Addr(),
                request.mRelayIp, request.mClientMac, true /* broadcast */, message);

        final Inet4Address dst = isEmpty(request.mRelayIp)
                ? IPV4_ADDR_ALL
                : request.mRelayIp;
        return transmitPacket(nakPacket, DhcpNakPacket.class.getSimpleName(), dst);
    }

    private boolean transmitOfferOrAckPacket(@NonNull ByteBuffer buf, @NonNull String packetTypeTag,
            @NonNull DhcpPacket request, @NonNull DhcpLease lease, @NonNull MacAddress clientMac,
            boolean broadcastFlag) {
        mLog.logf("Transmitting %s with lease %s", packetTypeTag, lease);
        // Client may not yet respond to ARP for the lease address, which may be the destination
        // address. Add an entry to the ARP cache to save future ARP probes and make sure the
        // packet reaches its destination.
        if (!addArpEntry(clientMac, lease.getNetAddr())) {
            // Logging for error already done
            return false;
        }
        final Inet4Address dst = getAckOrOfferDst(request, lease, broadcastFlag);
        return transmitPacket(buf, packetTypeTag, dst);
    }

    private boolean transmitPacket(@NonNull ByteBuffer buf, @NonNull String packetTypeTag,
            @NonNull Inet4Address dst) {
        try {
            mDeps.sendPacket(mSocket, buf, dst);
        } catch (ErrnoException | IOException e) {
            mLog.e("Can't send packet " + packetTypeTag, e);
            return false;
        }
        return true;
    }

    private boolean addArpEntry(@NonNull MacAddress macAddr, @NonNull Inet4Address inetAddr) {
        try {
            mDeps.addArpEntry(inetAddr, macAddr, mIfName, mSocket);
            return true;
        } catch (IOException e) {
            mLog.e("Error adding client to ARP table", e);
            return false;
        }
    }

    /**
     * Get the remaining lease time in seconds, starting from {@link Clock#elapsedRealtime()}.
     *
     * <p>This is an unsigned 32-bit integer, so it cannot be read as a standard (signed) Java int.
     * The return value is only intended to be used to populate the lease time field in a DHCP
     * response, considering that lease time is an unsigned 32-bit integer field in DHCP packets.
     *
     * <p>Lease expiration times are tracked internally with millisecond precision: this method
     * returns a rounded down value.
     */
    private int getLeaseTimeout(@NonNull DhcpLease lease) {
        final long remainingTimeSecs = (lease.getExpTime() - mClock.elapsedRealtime()) / 1000;
        if (remainingTimeSecs < 0) {
            mLog.e("Processing expired lease " + lease);
            return EXPIRED_FALLBACK_LEASE_TIME_SECS;
        }

        if (remainingTimeSecs >= toUnsignedLong(INFINITE_LEASE)) {
            return INFINITE_LEASE;
        }

        return (int) remainingTimeSecs;
    }

    /**
     * Get the client MAC address from a packet.
     *
     * @throws MalformedPacketException The address in the packet uses an unsupported format.
     */
    @NonNull
    private MacAddress getMacAddr(@NonNull DhcpPacket packet) throws MalformedPacketException {
        try {
            return MacAddress.fromBytes(packet.getClientMac());
        } catch (IllegalArgumentException e) {
            final String message = "Invalid MAC address in packet: "
                    + HexDump.dumpHexString(packet.getClientMac());
            throw new MalformedPacketException(message, e);
        }
    }

    private static boolean isEmpty(@Nullable Inet4Address address) {
        return address == null || IPV4_ADDR_ANY.equals(address);
    }

    private class PacketListener extends DhcpPacketListener {
        PacketListener(Handler handler) {
            super(handler);
        }

        @Override
        protected void onReceive(@NonNull DhcpPacket packet, @NonNull Inet4Address srcAddr,
                int 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
        protected void logError(@NonNull String msg, Exception e) {
            mLog.e("Error receiving packet: " + msg, e);
        }

        @Override
        protected void logParseError(@NonNull byte[] packet, int length,
                @NonNull DhcpPacket.ParseException e) {
            mLog.e("Error parsing packet", e);
        }

        @Override
        protected FileDescriptor createFd() {
            // TODO: have and use an API to set a socket tag without going through the thread tag
            final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_DHCP_SERVER);
            try {
                mSocket = Os.socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_UDP);
                SocketUtils.bindSocketToInterface(mSocket, mIfName);
                Os.setsockoptInt(mSocket, SOL_SOCKET, SO_REUSEADDR, 1);
                Os.setsockoptInt(mSocket, SOL_SOCKET, SO_BROADCAST, 1);
                Os.bind(mSocket, IPV4_ADDR_ANY, DHCP_SERVER);

                return mSocket;
            } catch (IOException | ErrnoException e) {
                mLog.e("Error creating UDP socket", e);
                return null;
            } finally {
                TrafficStats.setThreadStatsTag(oldTag);
            }
        }
    }
}
