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

Commit 00197139 authored by Xiao Ma's avatar Xiao Ma Committed by Gerrit Code Review
Browse files

Merge "Support gratuitous unsolicited Neighbor Advertisement."

parents 3d03021c 002dd5ac
Loading
Loading
Loading
Loading
+103 −0
Original line number Diff line number Diff line
@@ -18,8 +18,14 @@ package android.net.ip;

import static android.net.RouteInfo.RTN_UNICAST;
import static android.net.dhcp.DhcpResultsParcelableUtil.toStableParcelable;
import static android.net.util.NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.system.OsConstants.AF_PACKET;
import static android.system.OsConstants.ETH_P_IPV6;
import static android.system.OsConstants.SOCK_NONBLOCK;
import static android.system.OsConstants.SOCK_RAW;

import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
import static com.android.net.module.util.NetworkStackConstants.VENDOR_SPECIFIC_IE_ID;
import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission;

@@ -52,6 +58,7 @@ import android.net.shared.ProvisioningConfiguration;
import android.net.shared.ProvisioningConfiguration.ScanResultInfo;
import android.net.shared.ProvisioningConfiguration.ScanResultInfo.InformationElement;
import android.net.util.InterfaceParams;
import android.net.util.NetworkStackUtils;
import android.net.util.SharedLog;
import android.os.Build;
import android.os.ConditionVariable;
@@ -61,6 +68,8 @@ import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.stats.connectivity.DisconnectCode;
import android.system.ErrnoException;
import android.system.Os;
import android.text.TextUtils;
import android.util.LocalLog;
import android.util.Log;
@@ -79,16 +88,21 @@ import com.android.internal.util.StateMachine;
import com.android.internal.util.WakeupMessage;
import com.android.net.module.util.DeviceConfigUtils;
import com.android.networkstack.apishim.NetworkInformationShimImpl;
import com.android.networkstack.apishim.SocketUtilsShimImpl;
import com.android.networkstack.apishim.common.NetworkInformationShim;
import com.android.networkstack.apishim.common.ShimUtils;
import com.android.networkstack.metrics.IpProvisioningMetrics;
import com.android.networkstack.packets.NeighborAdvertisement;
import com.android.server.NetworkObserverRegistry;
import com.android.server.NetworkStackService.NetworkStackServiceManager;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URL;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
@@ -479,6 +493,8 @@ public class IpClient extends StateMachine {
    private final MessageHandlingLogger mMsgStateLogger;
    private final IpConnectivityLog mMetricsLog;
    private final InterfaceController mInterfaceCtrl;
    // Set of IPv6 addresses for which unsolicited gratuitous NA packets have been sent.
    private final Set<Inet6Address> mGratuitousNaTargetAddresses = new HashSet<>();

    // Ignore nonzero RDNSS option lifetimes below this value. 0 = disabled.
    private final int mMinRdnssLifetimeSec;
@@ -564,6 +580,16 @@ public class IpClient extends StateMachine {
        public IpConnectivityLog getIpConnectivityLog() {
            return new IpConnectivityLog();
        }

        /**
         * Return whether a feature guarded by a feature flag is enabled.
         * @see NetworkStackUtils#isFeatureEnabled(Context, String, String)
         */
        public boolean isFeatureEnabled(final Context context, final String name,
                boolean defaultEnabled) {
            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name,
                    defaultEnabled);
        }
    }

    public IpClient(Context context, String ifName, IIpClientCallbacks callback,
@@ -647,6 +673,21 @@ public class IpClient extends StateMachine {
                logMsg(msg);
            }

            @Override
            public void onInterfaceAddressRemoved(LinkAddress address, String iface) {
                super.onInterfaceAddressRemoved(address, iface);
                if (!mInterfaceName.equals(iface)) return;
                if (!address.isIpv6()) return;
                final Inet6Address targetIp = (Inet6Address) address.getAddress();
                if (mGratuitousNaTargetAddresses.contains(targetIp)) {
                    mGratuitousNaTargetAddresses.remove(targetIp);

                    final String msg = "Global IPv6 address: " + targetIp
                            + " has removed from the set of gratuitous NA target address.";
                    logMsg(msg);
                }
            }

            private void logMsg(String msg) {
                Log.d(mTag, msg);
                getHandler().post(() -> mLog.log("OBSERVED " + msg));
@@ -793,6 +834,11 @@ public class IpClient extends StateMachine {
        mLinkObserver.shutdown();
    }

    private boolean isGratuitousNaEnabled() {
        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GRATUITOUS_NA_VERSION,
                false /* defaultEnabled */);
    }

    @Override
    protected void onQuitting() {
        mCallback.onQuit();
@@ -1377,6 +1423,57 @@ public class IpClient extends StateMachine {
        }
    }

    private void sendGratuitousNA(final Inet6Address srcIp, final Inet6Address target) {
        final int flags = 0; // R=0, S=0, O=0
        final Inet6Address dstIp = IPV6_ADDR_ALL_ROUTERS_MULTICAST;
        // Ethernet multicast destination address: 33:33:00:00:00:02.
        final MacAddress dstMac = NetworkStackUtils.ipv6MulticastToEthernetMulticast(dstIp);
        final ByteBuffer packet = NeighborAdvertisement.build(mInterfaceParams.macAddr, dstMac,
                srcIp, dstIp, flags, target);
        FileDescriptor sock = null;
        try {
            sock = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0 /* protocol */);
            final SocketAddress sockAddress =
                    SocketUtilsShimImpl.newInstance().makePacketSocketAddress(ETH_P_IPV6,
                            mInterfaceParams.index, dstMac.toByteArray());
            Os.sendto(sock, packet.array(), 0 /* byteOffset */, packet.limit() /* byteCount */,
                    0 /* flags */, sockAddress);
        } catch (SocketException | ErrnoException e) {
            logError("Fail to send Gratuitous Neighbor Advertisement", e);
        } finally {
            NetworkStackUtils.closeSocketQuietly(sock);
        }
    }

    private Inet6Address getIpv6LinkLocalAddress(final LinkProperties newLp) {
        Inet6Address src = null;
        for (LinkAddress la : newLp.getLinkAddresses()) {
            if (!la.isIpv6()) continue;
            final Inet6Address ip = (Inet6Address) la.getAddress();
            if (ip.isLinkLocalAddress()) return ip;
        }
        return null;
    }

    private void maybeSendGratuitousNAs(final LinkProperties newLp) {
        if (!newLp.hasGlobalIpv6Address() || !isGratuitousNaEnabled()) return;

        final Inet6Address src = getIpv6LinkLocalAddress(newLp);
        if (src == null) return;
        for (LinkAddress la : newLp.getLinkAddresses()) {
            if (!la.isIpv6() || !la.isGlobalPreferred()) continue;
            final Inet6Address targetIp = (Inet6Address) la.getAddress();
            // Already sent gratuitous NA with this target global IPv6 address.
            if (mGratuitousNaTargetAddresses.contains(targetIp)) continue;
            if (DBG) {
                Log.d(mTag, "send Gratuitous NA from " + src + ", target Address is "
                            + targetIp);
            }
            sendGratuitousNA(src, targetIp);
            mGratuitousNaTargetAddresses.add(targetIp);
        }
    }

    // Returns false if we have lost provisioning, true otherwise.
    private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
        final LinkProperties newLp = assembleLinkProperties();
@@ -1384,6 +1481,11 @@ public class IpClient extends StateMachine {
            return true;
        }

        // Check if new assigned IPv6 GUA is available in the LinkProperties now. If so, initiate
        // gratuitous multicast unsolicited Neighbor Advertisements as soon as possible to inform
        // first-hop routers that the new GUA host is goning to use.
        maybeSendGratuitousNAs(newLp);

        // Either success IPv4 or IPv6 provisioning triggers new LinkProperties update,
        // wait for the provisioning completion and record the latency.
        mIpProvisioningMetrics.setIPv4ProvisionedLatencyOnFirstTime(newLp.isIpv4Provisioned());
@@ -1662,6 +1764,7 @@ public class IpClient extends StateMachine {
        public void enter() {
            stopAllIP();
            mHasDisabledIPv6OnProvLoss = false;
            mGratuitousNaTargetAddresses.clear();

            mLinkObserver.clearInterfaceParams();
            resetLinkProperties();
+25 −0
Original line number Diff line number Diff line
@@ -17,12 +17,16 @@
package android.net.util;

import android.content.Context;
import android.net.MacAddress;

import androidx.annotation.NonNull;

import com.android.net.module.util.DeviceConfigUtils;

import java.io.FileDescriptor;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.SocketException;

/**
@@ -232,6 +236,12 @@ public class NetworkStackUtils {
     */
    public static final String VALIDATION_METRICS_VERSION = "validation_metrics_version";

    /**
     * Experiment flag to enable sending gratuitous multicast unsolicited Neighbor Advertisements
     * to propagate new assigned IPv6 GUA as quickly as possible.
     */
    public static final String IPCLIENT_GRATUITOUS_NA_VERSION = "ipclient_gratuitous_na_version";

    static {
        System.loadLibrary("networkstackutilsjni");
    }
@@ -246,6 +256,21 @@ public class NetworkStackUtils {
        }
    }

    /**
     * Convert IPv6 multicast address to ethernet multicast address in network order.
     */
    public static MacAddress ipv6MulticastToEthernetMulticast(@NonNull final Inet6Address addr) {
        final byte[] etherMulticast = new byte[6];
        final byte[] ipv6Multicast = addr.getAddress();
        etherMulticast[0] = (byte) 0x33;
        etherMulticast[1] = (byte) 0x33;
        etherMulticast[2] = ipv6Multicast[12];
        etherMulticast[3] = ipv6Multicast[13];
        etherMulticast[4] = ipv6Multicast[14];
        etherMulticast[5] = ipv6Multicast[15];
        return MacAddress.fromBytes(etherMulticast);
    }

    /**
     * Attaches a socket filter that accepts DHCP packets to the given socket.
     */
+79 −1
Original line number Diff line number Diff line
@@ -39,11 +39,15 @@ import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_OFFSET;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
import static com.android.net.module.util.NetworkStackConstants.IPV6_PROTOCOL_OFFSET;
import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;

import static junit.framework.Assert.fail;

@@ -141,6 +145,7 @@ import com.android.networkstack.apishim.ConstantsShim;
import com.android.networkstack.apishim.common.ShimUtils;
import com.android.networkstack.arp.ArpPacket;
import com.android.networkstack.metrics.IpProvisioningMetrics;
import com.android.networkstack.packets.NeighborAdvertisement;
import com.android.server.NetworkObserver;
import com.android.server.NetworkObserverRegistry;
import com.android.server.NetworkStackService.NetworkStackServiceManager;
@@ -390,6 +395,7 @@ public abstract class IpClientIntegrationTestCommon {
            return mDhcpClient;
        }

        @Override
        public boolean isFeatureEnabled(final Context context, final String name,
                final boolean defaultEnabled) {
            return IpClientIntegrationTestCommon.this.isFeatureEnabled(name, defaultEnabled);
@@ -688,6 +694,14 @@ public abstract class IpClientIntegrationTestCommon {
        }
    }

    private NeighborAdvertisement parseNeighborAdvertisementOrNull(final byte[] packet) {
        try {
            return NeighborAdvertisement.parse(packet, packet.length);
        } catch (NeighborAdvertisement.ParseException e) {
            return null;
        }
    }

    private static ByteBuffer buildDhcpOfferPacket(final DhcpPacket packet,
            final Inet4Address clientAddress, final Integer leaseTimeSec, final short mtu,
            final String captivePortalUrl, final Integer ipv6OnlyWaitTime) {
@@ -1418,6 +1432,24 @@ public abstract class IpClientIntegrationTestCommon {
                        == (byte) ICMPV6_ROUTER_SOLICITATION;
    }

    private boolean isNeighborAdvertisement(final byte[] packetBytes) {
        ByteBuffer packet = ByteBuffer.wrap(packetBytes);
        return packet.getShort(ETHER_TYPE_OFFSET) == (short) ETH_P_IPV6
                && packet.get(ETHER_HEADER_LEN + IPV6_PROTOCOL_OFFSET) == (byte) IPPROTO_ICMPV6
                && packet.get(ETHER_HEADER_LEN + IPV6_HEADER_LEN)
                        == (byte) ICMPV6_NEIGHBOR_ADVERTISEMENT;
    }

    private NeighborAdvertisement getNextNeighborAdvertisement() throws ParseException {
        final byte[] packet = mPacketReader.popPacket(PACKET_TIMEOUT_MS,
                this::isNeighborAdvertisement);
        if (packet == null) return null;

        final NeighborAdvertisement na = parseNeighborAdvertisementOrNull(packet);
        assertNotNull("Invalid neighbour advertisement received", na);
        return na;
    }

    private void waitForRouterSolicitation() throws ParseException {
        assertNotNull("No router solicitation received on interface within timeout",
                mPacketReader.popPacket(PACKET_TIMEOUT_MS, this::isRouterSolicitation));
@@ -1448,7 +1480,7 @@ public abstract class IpClientIntegrationTestCommon {
    private static ByteBuffer buildPioOption(int valid, int preferred, String prefixString)
            throws Exception {
        return PrefixInformationOption.build(new IpPrefix(prefixString),
                (byte) 0b11000000 /* L = 1, A = 1 */, valid, preferred);
                (byte) (PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS), valid, preferred);
    }

    private static ByteBuffer buildRdnssOption(int lifetime, String... servers) throws Exception {
@@ -2744,4 +2776,50 @@ public abstract class IpClientIntegrationTestCommon {
        assertArrayEquals(packet.mUserClass, TEST_OEM_USER_CLASS_INFO);
        assertFalse(packet.hasRequestedParam((byte) 42 /* NTP_SERVER */));
    }

    private void assertGratuitousNa(final NeighborAdvertisement na) throws Exception {
        final MacAddress etherMulticast =
                NetworkStackUtils.ipv6MulticastToEthernetMulticast(IPV6_ADDR_ALL_ROUTERS_MULTICAST);
        final LinkAddress target = new LinkAddress(na.naHdr.target, 64);

        assertEquals(etherMulticast, na.ethHdr.dstMac);
        assertEquals(ETH_P_IPV6, na.ethHdr.etherType);
        assertEquals(IPPROTO_ICMPV6, na.ipv6Hdr.nextHeader);
        assertEquals(0xff, na.ipv6Hdr.hopLimit);
        assertTrue(na.ipv6Hdr.srcIp.isLinkLocalAddress());
        assertEquals(IPV6_ADDR_ALL_ROUTERS_MULTICAST, na.ipv6Hdr.dstIp);
        assertEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, na.icmpv6Hdr.type);
        assertEquals(0, na.icmpv6Hdr.code);
        assertEquals(0, na.naHdr.flags);
        assertTrue(target.isGlobalPreferred());
    }

    @Test
    public void testGratuitousNa() throws Exception {
        final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIpReachabilityMonitor()
                .withoutIPv4()
                .build();

        setFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION,
                true /* isGratuitousNaEnabled */);
        assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION, false));
        startIpClientProvisioning(config);

        final InOrder inOrder = inOrder(mCb);
        final String dnsServer = "2001:4860:4860::64";
        final ByteBuffer pio = buildPioOption(3600, 1800, "2001:db8:1::/64");
        final ByteBuffer rdnss = buildRdnssOption(3600, dnsServer);
        final ByteBuffer ra = buildRaPacket(pio, rdnss);

        doIpv6OnlyProvisioning(inOrder, ra);

        final List<NeighborAdvertisement> naList = new ArrayList<>();
        NeighborAdvertisement packet;
        while ((packet = getNextNeighborAdvertisement()) != null) {
            assertGratuitousNa(packet);
            naList.add(packet);
        }
        assertEquals(2, naList.size()); // privacy address and stable privacy address
    }
}