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

Commit 6e2818b1 authored by Xiao Ma's avatar Xiao Ma
Browse files

Implement DHCP IPv6-Only preferred option.

An IPv6-capable host includes this option in the Parameter Request
List option and willing to forgo obtaining an IPv4 address for a
specific assigned period(V6ONLY_WAIT). DHCP server responds with
DHCPOFFER or DHCPACK with this option if network can provide IPv6
connectivity.

Currently whether or not including IPv6-only preferred option in the
PRL is controlled by experiment flag.

Bug: 159670487
Test: atest NetworkStackTests NetworkStackIntegrationTests
Change-Id: I89653271197164a7b7f638c43cb3bf4d28817b9f
parent fd4ed24e
Loading
Loading
Loading
Loading
+5 −4
Original line number Diff line number Diff line
@@ -46,10 +46,11 @@ public class DhcpAckPacket extends DhcpPacket {
            dnsServers += dnsServer.toString() + " ";
        }

        return s + " ACK: your new IP " + mYourIp +
                ", netmask " + mSubnetMask +
                ", gateways " + mGateways + dnsServers +
                ", lease time " + mLeaseTime;
        return s + " ACK: your new IP " + mYourIp
                + ", netmask " + mSubnetMask
                + ", gateways " + mGateways + dnsServers
                + ", lease time " + mLeaseTime
                + (mIpv6OnlyWaitTime != null ? ", V6ONLY_WAIT " + mIpv6OnlyWaitTime : "");
    }

    /**
+86 −13
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import static android.net.dhcp.DhcpPacket.DHCP_BROADCAST_ADDRESS;
import static android.net.dhcp.DhcpPacket.DHCP_CAPTIVE_PORTAL;
import static android.net.dhcp.DhcpPacket.DHCP_DNS_SERVER;
import static android.net.dhcp.DhcpPacket.DHCP_DOMAIN_NAME;
import static android.net.dhcp.DhcpPacket.DHCP_IPV6_ONLY_PREFERRED;
import static android.net.dhcp.DhcpPacket.DHCP_LEASE_TIME;
import static android.net.dhcp.DhcpPacket.DHCP_MTU;
import static android.net.dhcp.DhcpPacket.DHCP_REBINDING_TIME;
@@ -31,6 +32,7 @@ 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_IPV6_ONLY_PREFERRED_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;
@@ -104,6 +106,7 @@ import com.android.networkstack.apishim.common.ShimUtils;
import com.android.networkstack.arp.ArpPacket;
import com.android.networkstack.metrics.IpProvisioningMetrics;

import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.Inet4Address;
@@ -244,6 +247,7 @@ public class DhcpClient extends StateMachine {
    /* Message.arg1 arguments to CMD_POST_DHCP_ACTION notification */
    public static final int DHCP_SUCCESS = 1;
    public static final int DHCP_FAILURE = 2;
    public static final int DHCP_IPV6_ONLY = 3;

    // Internal messages.
    private static final int PRIVATE_BASE         = IpClient.DHCPCLIENT_CMD_BASE + 100;
@@ -287,13 +291,18 @@ public class DhcpClient extends StateMachine {

    @NonNull
    private byte[] getRequestedParams() {
        // Set an initial size large enough for all optional parameters that we might request.
        final int numOptionalParams = 2;
        final ByteArrayOutputStream params =
                new ByteArrayOutputStream(DEFAULT_REQUESTED_PARAMS.length + numOptionalParams);
        params.write(DEFAULT_REQUESTED_PARAMS, 0, DEFAULT_REQUESTED_PARAMS.length);
        if (isCapportApiEnabled()) {
            final byte[] params = Arrays.copyOf(DEFAULT_REQUESTED_PARAMS,
                    DEFAULT_REQUESTED_PARAMS.length + 1);
            params[params.length - 1] = DHCP_CAPTIVE_PORTAL;
            return params;
            params.write(DHCP_CAPTIVE_PORTAL);
        }
        return DEFAULT_REQUESTED_PARAMS;
        if (isIPv6OnlyPreferredModeEnabled()) {
            params.write(DHCP_IPV6_ONLY_PREFERRED);
        }
        return params.toByteArray();
    }

    private static boolean isCapportApiEnabled() {
@@ -338,10 +347,10 @@ public class DhcpClient extends StateMachine {
    private int mConflictCount;
    private long mLastAssignedIpv4AddressExpiry;
    private Dependencies mDependencies;
    @NonNull
    private final NetworkStackIpMemoryStore mIpMemoryStore;
    @Nullable
    private DhcpPacketHandler mDhcpPacketHandler;
    @NonNull
    private final NetworkStackIpMemoryStore mIpMemoryStore;
    @Nullable
    private final String mHostname;

@@ -349,6 +358,10 @@ public class DhcpClient extends StateMachine {
    private long mLastInitEnterTime;
    private long mLastBoundExitTime;

    // 32-bit unsigned integer used to indicate the number of milliseconds the DHCP client should
    // disable DHCPv4.
    private long mIpv6OnlyWaitTimeMs;

    // States.
    private State mStoppedState = new StoppedState();
    private State mDhcpState = new DhcpState();
@@ -370,6 +383,7 @@ public class DhcpClient extends StateMachine {
            new WaitBeforeObtainingConfigurationState(mObtainingConfigurationState);
    private State mIpAddressConflictDetectingState = new IpAddressConflictDetectingState();
    private State mDhcpDecliningState = new DhcpDecliningState();
    private State mIpv6OnlyWaitState = new Ipv6OnlyWaitState();

    private WakeupMessage makeWakeupMessage(String cmdName, int cmd) {
        cmdName = DhcpClient.class.getSimpleName() + "." + mIfaceName + "." + cmdName;
@@ -470,6 +484,7 @@ public class DhcpClient extends StateMachine {
            addState(mDhcpSelectingState, mDhcpState);
            addState(mDhcpRequestingState, mDhcpState);
            addState(mIpAddressConflictDetectingState, mDhcpState);
            addState(mIpv6OnlyWaitState, mDhcpState);
            addState(mDhcpHaveLeaseState, mDhcpState);
                addState(mConfiguringInterfaceState, mDhcpHaveLeaseState);
                addState(mDhcpBoundState, mDhcpHaveLeaseState);
@@ -544,6 +559,14 @@ public class DhcpClient extends StateMachine {
                false /* defaultEnabled */);
    }

    /**
     * check whether or not to support IPv6-only preferred option.
     */
    public boolean isIPv6OnlyPreferredModeEnabled() {
        return mDependencies.isFeatureEnabled(mContext, DHCP_IPV6_ONLY_PREFERRED_VERSION,
                false /* defaultEnabled */);
    }

    private void recordMetricEnabledFeatures() {
        if (isDhcpLeaseCacheEnabled()) mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_INITREBOOT);
        if (isDhcpRapidCommitEnabled()) mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_RAPIDCOMMIT);
@@ -605,6 +628,13 @@ public class DhcpClient extends StateMachine {
        }
    }

    private byte[] getOptionsToSkip() {
        final ByteArrayOutputStream optionsToSkip = new ByteArrayOutputStream(2);
        if (!isCapportApiEnabled()) optionsToSkip.write(DHCP_CAPTIVE_PORTAL);
        if (!isIPv6OnlyPreferredModeEnabled()) optionsToSkip.write(DHCP_IPV6_ONLY_PREFERRED);
        return optionsToSkip.toByteArray();
    }

    private class DhcpPacketHandler extends PacketReader {
        private FileDescriptor mPacketSock;

@@ -615,10 +645,8 @@ public class DhcpClient extends StateMachine {
        @Override
        protected void handlePacket(byte[] recvbuf, int length) {
            try {
                final byte[] optionsToSkip =
                        isCapportApiEnabled() ? new byte[0] : new byte[] { DHCP_CAPTIVE_PORTAL };
                final DhcpPacket packet = DhcpPacket.decodeFullPacket(recvbuf, length,
                        DhcpPacket.ENCAP_L2, optionsToSkip);
                        DhcpPacket.ENCAP_L2, getOptionsToSkip());
                if (DBG) Log.d(TAG, "Received packet: " + packet);
                sendMessage(CMD_RECEIVED_PACKET, packet);
            } catch (DhcpPacket.ParseException e) {
@@ -944,10 +972,11 @@ public class DhcpClient extends StateMachine {
        // This is part of the initial configuration because it is passed in on startup and
        // never updated.
        // TODO: decide what to do about L2 key changes while the client is connected.
        @Nullable
        public final String l2Key;
        public final boolean isPreconnectionEnabled;

        public Configuration(String l2Key, boolean isPreconnectionEnabled) {
        public Configuration(@Nullable final String l2Key, final boolean isPreconnectionEnabled) {
            this.l2Key = l2Key;
            this.isPreconnectionEnabled = isPreconnectionEnabled;
        }
@@ -1052,7 +1081,7 @@ public class DhcpClient extends StateMachine {
    }

    abstract class TimeoutState extends LoggingState {
        protected int mTimeout = 0;
        protected long mTimeout = 0;

        @Override
        public void enter() {
@@ -1223,6 +1252,15 @@ public class DhcpClient extends StateMachine {
        }
    }

    private boolean maybeTransitionToIpv6OnlyWaitState(@NonNull final DhcpPacket packet) {
        if (!isIPv6OnlyPreferredModeEnabled()) return false;
        if (packet.getIpv6OnlyWaitTimeMillis() == DhcpPacket.V6ONLY_PREFERRED_ABSENCE) return false;

        mIpv6OnlyWaitTimeMs = packet.getIpv6OnlyWaitTimeMillis();
        transitionTo(mIpv6OnlyWaitState);
        return true;
    }

    private void receiveOfferOrAckPacket(final DhcpPacket packet, final boolean acceptRapidCommit) {
        if (!isValidPacket(packet)) return;

@@ -1230,6 +1268,9 @@ public class DhcpClient extends StateMachine {
        // 2. received the DHCPACK packet from DHCP Servers that support Rapid
        //    Commit option, process it by following RFC4039.
        if (packet instanceof DhcpOfferPacket) {
            if (maybeTransitionToIpv6OnlyWaitState(packet)) {
                return;
            }
            mOffer = packet.toDhcpResults();
            if (mOffer != null) {
                Log.d(TAG, "Got pending lease: " + mOffer);
@@ -1362,7 +1403,10 @@ public class DhcpClient extends StateMachine {
        protected void receivePacket(DhcpPacket packet) {
            if (!isValidPacket(packet)) return;
            if ((packet instanceof DhcpAckPacket)) {
                DhcpResults results = packet.toDhcpResults();
                if (maybeTransitionToIpv6OnlyWaitState(packet)) {
                    return;
                }
                final DhcpResults results = packet.toDhcpResults();
                if (results != null) {
                    confirmDhcpLease(packet, results);
                    transitionTo(isDhcpIpConflictDetectEnabled()
@@ -1762,6 +1806,9 @@ public class DhcpClient extends StateMachine {
        protected void receivePacket(DhcpPacket packet) {
            if (!isValidPacket(packet)) return;
            if ((packet instanceof DhcpAckPacket)) {
                if (maybeTransitionToIpv6OnlyWaitState(packet)) {
                    return;
                }
                final DhcpResults results = packet.toDhcpResults();
                if (results != null) {
                    if (!mDhcpLease.ipAddress.equals(results.ipAddress)) {
@@ -1905,6 +1952,32 @@ public class DhcpClient extends StateMachine {
        }
    }

    // This state is used for IPv6-only preferred mode defined in the draft-ietf-dhc-v6only.
    // For IPv6-only capable host, it will forgo obtaining an IPv4 address for V6ONLY_WAIT
    // period if the network indicates that it can provide IPv6 connectivity by replying
    // with a valid IPv6-only preferred option in the DHCPOFFER or DHCPACK.
    class Ipv6OnlyWaitState extends TimeoutState {
        @Override
        public void enter() {
            mTimeout = mIpv6OnlyWaitTimeMs;
            super.enter();

            // Restore power save and suspend optimization if it was disabled before.
            if (mRegisteredForPreDhcpNotification) {
                mController.sendMessage(CMD_POST_DHCP_ACTION, DHCP_IPV6_ONLY, 0, null);
            }
        }

        @Override
        public void exit() {
            mIpv6OnlyWaitTimeMs = 0;
        }

        protected void timeout() {
            startInitRebootOrInit();
        }
    }

    private void logState(String name, int durationMs) {
        final DhcpClientEvent event = new DhcpClientEvent.Builder()
                .setMsg(name)
+6 −3
Original line number Diff line number Diff line
@@ -47,9 +47,12 @@ public class DhcpOfferPacket extends DhcpPacket {
            }
        }

        return s + " OFFER, ip " + mYourIp + ", mask " + mSubnetMask +
                dnsServers + ", gateways " + mGateways +
                " lease time " + mLeaseTime + ", domain " + mDomainName;
        return s + " OFFER, ip " + mYourIp
                + ", mask " + mSubnetMask + dnsServers
                + ", gateways " + mGateways
                + ", lease time " + mLeaseTime
                + ", domain " + mDomainName
                + (mIpv6OnlyWaitTime != null ? ", V6ONLY_WAIT " + mIpv6OnlyWaitTime : "");
    }

    /**
+69 −4
Original line number Diff line number Diff line
@@ -88,6 +88,10 @@ public abstract class DhcpPacket {
    public static final int HWADDR_LEN = 16;
    public static final int MAX_OPTION_LEN = 255;

    // The lower boundary for V6ONLY_WAIT.
    public static final long MIN_V6ONLY_WAIT_MS = 300_000;
    public static final long V6ONLY_PREFERRED_ABSENCE = -1L;

    /**
     * The minimum and maximum MTU that we are prepared to use. We set the minimum to the minimum
     * IPv6 MTU because the IPv6 stack enters unusual codepaths when the link MTU drops below 1280,
@@ -307,6 +311,16 @@ public abstract class DhcpPacket {
    public static final byte DHCP_RAPID_COMMIT = 80;
    protected boolean mRapidCommit;

    /**
     * DHCP IPv6-Only Preferred Option(draft-ietf-dhc-v6only).
     * Indicate that a host supports an IPv6-only mode and willing to forgo obtaining an IPv4
     * address for V6ONLY_WAIT period if the network provides IPv6 connectivity. V6ONLY_WAIT
     * is 32-bit unsigned integer, so the Integer value cannot be used as-is.
     */
    public static final byte DHCP_IPV6_ONLY_PREFERRED = (byte) 108;
    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    public Integer mIpv6OnlyWaitTime;

    public static final byte DHCP_CAPTIVE_PORTAL = (byte) 114;
    protected String mCaptivePortalUrl;

@@ -789,6 +803,9 @@ public abstract class DhcpPacket {
        if (mMtu != null && Short.toUnsignedInt(mMtu) >= IPV4_MIN_MTU) {
            addTlv(buf, DHCP_MTU, mMtu);
        }
        if (mIpv6OnlyWaitTime != null) {
            addTlv(buf, DHCP_IPV6_ONLY_PREFERRED, (int) Integer.toUnsignedLong(mIpv6OnlyWaitTime));
        }
        addTlv(buf, DHCP_CAPTIVE_PORTAL, mCaptivePortalUrl);
    }

@@ -942,6 +959,7 @@ public abstract class DhcpPacket {
        Integer leaseTime = null;
        Integer T1 = null;
        Integer T2 = null;
        Integer ipv6OnlyWaitTime = null;

        // dhcp options
        byte dhcpType = (byte) 0xFF;
@@ -1204,6 +1222,10 @@ public abstract class DhcpPacket {
                            expectedLen = optionLen;
                            captivePortalUrl = readAsciiString(packet, optionLen, true);
                            break;
                        case DHCP_IPV6_ONLY_PREFERRED:
                            expectedLen = 4;
                            ipv6OnlyWaitTime = Integer.valueOf(packet.getInt());
                            break;
                        default:
                            expectedLen = skipOption(packet, optionLen);
                    }
@@ -1292,6 +1314,7 @@ public abstract class DhcpPacket {
        newPacket.mVendorId = vendorId;
        newPacket.mVendorInfo = vendorInfo;
        newPacket.mCaptivePortalUrl = captivePortalUrl;
        newPacket.mIpv6OnlyWaitTime = ipv6OnlyWaitTime;
        if ((optionOverload & OPTION_OVERLOAD_SNAME) == 0) {
            newPacket.mServerHostName = serverHostName;
        } else {
@@ -1384,6 +1407,14 @@ public abstract class DhcpPacket {
        }
    }

    /**
     * Returns the IPv6-only wait time, in milliseconds, or -1 if the option is not present.
     */
    public long getIpv6OnlyWaitTimeMillis() {
        if (mIpv6OnlyWaitTime == null) return V6ONLY_PREFERRED_ABSENCE;
        return Math.max(MIN_V6ONLY_WAIT_MS, Integer.toUnsignedLong(mIpv6OnlyWaitTime) * 1000);
    }

    /**
     * Builds a DHCP-DISCOVER packet from the required specified
     * parameters.
@@ -1399,15 +1430,14 @@ public abstract class DhcpPacket {
    }

    /**
     * Builds a DHCP-OFFER packet from the required specified
     * parameters.
     * Builds a DHCP-OFFER packet from the required specified parameters.
     */
    public static ByteBuffer buildOfferPacket(int encap, int transactionId,
            boolean broadcast, Inet4Address serverIpAddr, Inet4Address relayIp,
            Inet4Address yourIp, byte[] mac, Integer timeout, Inet4Address netMask,
            Inet4Address bcAddr, List<Inet4Address> gateways, List<Inet4Address> dnsServers,
            Inet4Address dhcpServerIdentifier, String domainName, String hostname, boolean metered,
            short mtu, String captivePortalUrl) {
            short mtu, String captivePortalUrl, Integer ipv6OnlyWaitTime) {
        DhcpPacket pkt = new DhcpOfferPacket(
                transactionId, (short) 0, broadcast, serverIpAddr, relayIp,
                INADDR_ANY /* clientIp */, yourIp, mac);
@@ -1424,9 +1454,26 @@ public abstract class DhcpPacket {
        if (metered) {
            pkt.mVendorInfo = VENDOR_INFO_ANDROID_METERED;
        }
        if (ipv6OnlyWaitTime != null) {
            pkt.mIpv6OnlyWaitTime = ipv6OnlyWaitTime;
        }
        return pkt.buildPacket(encap, DHCP_CLIENT, DHCP_SERVER);
    }

    /**
     * Builds a DHCP-OFFER packet from the required specified parameters.
     */
    public static ByteBuffer buildOfferPacket(int encap, int transactionId,
            boolean broadcast, Inet4Address serverIpAddr, Inet4Address relayIp,
            Inet4Address yourIp, byte[] mac, Integer timeout, Inet4Address netMask,
            Inet4Address bcAddr, List<Inet4Address> gateways, List<Inet4Address> dnsServers,
            Inet4Address dhcpServerIdentifier, String domainName, String hostname, boolean metered,
            short mtu, String captivePortalUrl) {
        return buildOfferPacket(encap, transactionId, broadcast, serverIpAddr, relayIp, yourIp,
                mac, timeout, netMask, bcAddr, gateways, dnsServers, dhcpServerIdentifier,
                domainName, hostname, metered, mtu, captivePortalUrl, null /* V6ONLY_WAIT */);
    }

    /**
     * Builds a DHCP-ACK packet from the required specified parameters.
     */
@@ -1435,7 +1482,7 @@ public abstract class DhcpPacket {
            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 rapidCommit, String captivePortalUrl) {
            short mtu, boolean rapidCommit, String captivePortalUrl, Integer ipv6OnlyWaitTime) {
        DhcpPacket pkt = new DhcpAckPacket(
                transactionId, (short) 0, broadcast, serverIpAddr, relayIp, requestClientIp, yourIp,
                mac, rapidCommit);
@@ -1452,9 +1499,27 @@ public abstract class DhcpPacket {
        if (metered) {
            pkt.mVendorInfo = VENDOR_INFO_ANDROID_METERED;
        }
        if (ipv6OnlyWaitTime != null) {
            pkt.mIpv6OnlyWaitTime = ipv6OnlyWaitTime;
        }
        return pkt.buildPacket(encap, DHCP_CLIENT, DHCP_SERVER);
    }

    /**
     * Builds a DHCP-ACK packet from the required specified parameters.
     */
    public static ByteBuffer buildAckPacket(int encap, int transactionId,
            boolean broadcast, Inet4Address serverIpAddr, Inet4Address relayIp, Inet4Address yourIp,
            Inet4Address requestClientIp, byte[] mac, Integer timeout, Inet4Address netMask,
            Inet4Address bcAddr, List<Inet4Address> gateways, List<Inet4Address> dnsServers,
            Inet4Address dhcpServerIdentifier, String domainName, String hostname, boolean metered,
            short mtu, boolean rapidCommit, String captivePortalUrl) {
        return buildAckPacket(encap, transactionId, broadcast, serverIpAddr, relayIp, yourIp,
                requestClientIp, mac, timeout, netMask, bcAddr, gateways, dnsServers,
                dhcpServerIdentifier, domainName, hostname, metered, mtu, rapidCommit,
                captivePortalUrl, null /* V6ONLY_WAIT */);
    }

    /**
     * Builds a DHCP-NAK packet from the required specified parameters.
     */
+4 −1
Original line number Diff line number Diff line
@@ -2169,7 +2169,8 @@ public class IpClient extends StateMachine {
                //     a) initial address acquisition succeeds,
                //     b) renew succeeds or is NAK'd,
                //     c) rebind succeeds or is NAK'd, or
                //     c) the lease expires,
                //     d) the lease expires, or
                //     e) the IPv6-only preferred option is enabled and entering Ipv6OnlyWaitState.
                //
                // but never when initial address acquisition fails. The latter
                // condition is now governed by the provisioning timeout.
@@ -2183,6 +2184,8 @@ public class IpClient extends StateMachine {
                        case DhcpClient.DHCP_FAILURE:
                            handleIPv4Failure();
                            break;
                        case DhcpClient.DHCP_IPV6_ONLY:
                            break;
                        default:
                            logError("Unknown CMD_POST_DHCP_ACTION status: %s", msg.arg1);
                    }
Loading