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

Commit 7e9ee219 authored by Xiao Ma's avatar Xiao Ma Committed by Automerger Merge Worker
Browse files

Merge "Support gratuitous unsolicited Neighbor Advertisement." am: 00197139

Original change: https://android-review.googlesource.com/c/platform/packages/modules/NetworkStack/+/1535654

Change-Id: Ib59fe224c3890224b144fbfa840ac0062dbd10e5
parents a99c09fe 00197139
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
    }
}