Merge "Increase the service bind timeout." into main
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 9132857..cd8eac8 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -96,6 +96,7 @@
     },
     binaries: [
         "clatd",
+        "ethtool",
         "netbpfload",
         "ot-daemon",
     ],
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
index 328e3fb..dac5b63 100644
--- a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
+++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
@@ -16,8 +16,6 @@
 
 package android.net.ip;
 
-import static android.net.RouteInfo.RTN_UNICAST;
-
 import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU;
@@ -42,12 +40,13 @@
 import android.net.INetd;
 import android.net.IpPrefix;
 import android.net.MacAddress;
-import android.net.RouteInfo;
 import android.net.ip.RouterAdvertisementDaemon.RaParams;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -55,7 +54,6 @@
 
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.Ipv6Utils;
-import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv6Header;
@@ -80,7 +78,6 @@
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
 import java.util.HashSet;
-import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -332,10 +329,12 @@
         // Add a default route "fe80::/64 -> ::" to local network, otherwise, device will fail to
         // send the unicast RA out due to the ENETUNREACH error(No route to the peer's link-local
         // address is present).
-        final String iface = mTetheredParams.name;
-        final RouteInfo linkLocalRoute =
-                new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST);
-        NetdUtils.addRoutesToLocalNetwork(sNetd, iface, List.of(linkLocalRoute));
+        try {
+            sNetd.networkAddRoute(INetd.LOCAL_NET_ID, mTetheredParams.name,
+                    "fe80::/64", INetd.NEXTHOP_NONE);
+        } catch (RemoteException | ServiceSpecificException e) {
+            throw new IllegalStateException(e);
+        }
 
         final ByteBuffer rs = createRsPacket("fe80::1122:3344:5566:7788");
         mTetheredPacketReader.sendResponse(rs);
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index dd27bf9..4958040 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -190,7 +190,7 @@
     OEM_DENY_2_MATCH = (1 << 10),
     OEM_DENY_3_MATCH = (1 << 11),
 };
-// LINT.ThenChange(packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java)
+// LINT.ThenChange(../framework/src/android/net/BpfNetMapsConstants.java)
 
 enum BpfPermissionMatch {
     BPF_PERMISSION_INTERNET = 1 << 2,
diff --git a/framework/aidl-export/android/net/LocalNetworkConfig.aidl b/framework/aidl-export/android/net/LocalNetworkConfig.aidl
new file mode 100644
index 0000000..e2829a5
--- /dev/null
+++ b/framework/aidl-export/android/net/LocalNetworkConfig.aidl
@@ -0,0 +1,20 @@
+/**
+ *
+ * Copyright (C) 2023 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;
+
+@JavaOnlyStableParcelable parcelable LocalNetworkConfig;
diff --git a/framework/src/android/net/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java
index 2191682..e0527f5 100644
--- a/framework/src/android/net/BpfNetMapsConstants.java
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -60,7 +60,7 @@
     public static final long OEM_DENY_1_MATCH = (1 << 9);
     public static final long OEM_DENY_2_MATCH = (1 << 10);
     public static final long OEM_DENY_3_MATCH = (1 << 11);
-    // LINT.ThenChange(packages/modules/Connectivity/bpf_progs/netd.h)
+    // LINT.ThenChange(../../../../bpf_progs/netd.h)
 
     public static final List<Pair<Long, String>> MATCH_LIST = Arrays.asList(
             Pair.create(HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"),
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 2315521..915c20d 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -3811,11 +3811,28 @@
     @RequiresPermission(anyOf = {
             NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
             android.Manifest.permission.NETWORK_FACTORY})
-    public Network registerNetworkAgent(INetworkAgent na, NetworkInfo ni, LinkProperties lp,
-            NetworkCapabilities nc, @NonNull NetworkScore score, NetworkAgentConfig config,
-            int providerId) {
+    public Network registerNetworkAgent(@NonNull INetworkAgent na, @NonNull NetworkInfo ni,
+            @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+            @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, int providerId) {
+        return registerNetworkAgent(na, ni, lp, nc, null /* localNetworkConfig */, score, config,
+                providerId);
+    }
+
+    /**
+     * @hide
+     * Register a NetworkAgent with ConnectivityService.
+     * @return Network corresponding to NetworkAgent.
+     */
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_FACTORY})
+    public Network registerNetworkAgent(@NonNull INetworkAgent na, @NonNull NetworkInfo ni,
+            @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+            @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
+            @NonNull NetworkAgentConfig config, int providerId) {
         try {
-            return mService.registerNetworkAgent(na, ni, lp, nc, score, config, providerId);
+            return mService.registerNetworkAgent(na, ni, lp, nc, score, localNetworkConfig, config,
+                    providerId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index ebe8bca..fe27773 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -27,6 +27,7 @@
 import android.net.IQosCallback;
 import android.net.ISocketKeepaliveCallback;
 import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
 import android.net.Network;
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
@@ -146,7 +147,8 @@
     void declareNetworkRequestUnfulfillable(in NetworkRequest request);
 
     Network registerNetworkAgent(in INetworkAgent na, in NetworkInfo ni, in LinkProperties lp,
-            in NetworkCapabilities nc, in NetworkScore score, in NetworkAgentConfig config,
+            in NetworkCapabilities nc, in NetworkScore score,
+            in LocalNetworkConfig localNetworkConfig, in NetworkAgentConfig config,
             in int factorySerialNumber);
 
     NetworkRequest requestNetwork(int uid, in NetworkCapabilities networkCapabilities, int reqType,
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl
index b375b7b..61b27b5 100644
--- a/framework/src/android/net/INetworkAgentRegistry.aidl
+++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -17,6 +17,7 @@
 
 import android.net.DscpPolicy;
 import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
@@ -34,6 +35,7 @@
     void sendLinkProperties(in LinkProperties lp);
     // TODO: consider replacing this by "markConnected()" and removing
     void sendNetworkInfo(in NetworkInfo info);
+    void sendLocalNetworkConfig(in LocalNetworkConfig config);
     void sendScore(in NetworkScore score);
     void sendExplicitlySelected(boolean explicitlySelected, boolean acceptPartial);
     void sendSocketKeepaliveEvent(int slot, int reason);
diff --git a/framework/src/android/net/LocalNetworkConfig.java b/framework/src/android/net/LocalNetworkConfig.java
new file mode 100644
index 0000000..fca7fd1
--- /dev/null
+++ b/framework/src/android/net/LocalNetworkConfig.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A class to communicate configuration info about a local network through {@link NetworkAgent}.
+ * @hide
+ */
+// TODO : @SystemApi
+public final class LocalNetworkConfig implements Parcelable {
+    @Nullable
+    private final NetworkRequest mUpstreamSelector;
+
+    @NonNull
+    private final MulticastRoutingConfig mUpstreamMulticastRoutingConfig;
+
+    @NonNull
+    private final MulticastRoutingConfig mDownstreamMulticastRoutingConfig;
+
+    private LocalNetworkConfig(@Nullable final NetworkRequest upstreamSelector,
+            @Nullable final MulticastRoutingConfig upstreamConfig,
+            @Nullable final MulticastRoutingConfig downstreamConfig) {
+        mUpstreamSelector = upstreamSelector;
+        if (null != upstreamConfig) {
+            mUpstreamMulticastRoutingConfig = upstreamConfig;
+        } else {
+            mUpstreamMulticastRoutingConfig = MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+        }
+        if (null != downstreamConfig) {
+            mDownstreamMulticastRoutingConfig = downstreamConfig;
+        } else {
+            mDownstreamMulticastRoutingConfig = MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+        }
+    }
+
+    /**
+     * Get the request choosing which network traffic from this network is forwarded to and from.
+     *
+     * This may be null if the local network doesn't forward the traffic anywhere.
+     */
+    @Nullable
+    public NetworkRequest getUpstreamSelector() {
+        return mUpstreamSelector;
+    }
+
+    public @NonNull MulticastRoutingConfig getUpstreamMulticastRoutingConfig() {
+        return mUpstreamMulticastRoutingConfig;
+    }
+
+    public @NonNull MulticastRoutingConfig getDownstreamMulticastRoutingConfig() {
+        return mDownstreamMulticastRoutingConfig;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull final Parcel dest, final int flags) {
+        dest.writeParcelable(mUpstreamSelector, flags);
+        dest.writeParcelable(mUpstreamMulticastRoutingConfig, flags);
+        dest.writeParcelable(mDownstreamMulticastRoutingConfig, flags);
+    }
+
+    public static final @NonNull Creator<LocalNetworkConfig> CREATOR = new Creator<>() {
+        public LocalNetworkConfig createFromParcel(Parcel in) {
+            final NetworkRequest upstreamSelector = in.readParcelable(null);
+            final MulticastRoutingConfig upstreamConfig = in.readParcelable(null);
+            final MulticastRoutingConfig downstreamConfig = in.readParcelable(null);
+            return new LocalNetworkConfig(
+                    upstreamSelector, upstreamConfig, downstreamConfig);
+        }
+
+        @Override
+        public LocalNetworkConfig[] newArray(final int size) {
+            return new LocalNetworkConfig[size];
+        }
+    };
+
+
+    public static final class Builder {
+        @Nullable
+        NetworkRequest mUpstreamSelector;
+
+        @Nullable
+        MulticastRoutingConfig mUpstreamMulticastRoutingConfig;
+
+        @Nullable
+        MulticastRoutingConfig mDownstreamMulticastRoutingConfig;
+
+        /**
+         * Create a Builder
+         */
+        public Builder() {
+        }
+
+        /**
+         * Set to choose where this local network should forward its traffic to.
+         *
+         * The system will automatically choose the best network matching the request as an
+         * upstream, and set up forwarding between this local network and the chosen upstream.
+         * If no network matches the request, there is no upstream and the traffic is not forwarded.
+         * The caller can know when this changes by listening to link properties changes of
+         * this network with the {@link android.net.LinkProperties#getForwardedNetwork()} getter.
+         *
+         * Set this to null if the local network shouldn't be forwarded. Default is null.
+         */
+        @NonNull
+        public Builder setUpstreamSelector(@Nullable NetworkRequest upstreamSelector) {
+            mUpstreamSelector = upstreamSelector;
+            return this;
+        }
+
+        /**
+         * Set the upstream multicast routing config.
+         *
+         * If null, don't route multicast packets upstream. This is equivalent to a
+         * MulticastRoutingConfig in mode FORWARD_NONE. The default is null.
+         */
+        @NonNull
+        public Builder setUpstreamMulticastRoutingConfig(@Nullable MulticastRoutingConfig cfg) {
+            mUpstreamMulticastRoutingConfig = cfg;
+            return this;
+        }
+
+        /**
+         * Set the downstream multicast routing config.
+         *
+         * If null, don't route multicast packets downstream. This is equivalent to a
+         * MulticastRoutingConfig in mode FORWARD_NONE. The default is null.
+         */
+        @NonNull
+        public Builder setDownstreamMulticastRoutingConfig(@Nullable MulticastRoutingConfig cfg) {
+            mDownstreamMulticastRoutingConfig = cfg;
+            return this;
+        }
+
+        /**
+         * Build the LocalNetworkConfig object.
+         */
+        @NonNull
+        public LocalNetworkConfig build() {
+            return new LocalNetworkConfig(mUpstreamSelector,
+                    mUpstreamMulticastRoutingConfig,
+                    mDownstreamMulticastRoutingConfig);
+        }
+    }
+}
diff --git a/framework/src/android/net/MulticastRoutingConfig.java b/framework/src/android/net/MulticastRoutingConfig.java
new file mode 100644
index 0000000..ebd9fc5
--- /dev/null
+++ b/framework/src/android/net/MulticastRoutingConfig.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2023 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;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * A class representing a configuration for multicast routing.
+ *
+ * Internal usage to Connectivity
+ * @hide
+ */
+// TODO : @SystemApi
+public class MulticastRoutingConfig implements Parcelable {
+    private static final String TAG = MulticastRoutingConfig.class.getSimpleName();
+
+    /** Do not forward any multicast packets. */
+    public static final int FORWARD_NONE = 0;
+    /**
+     * Forward only multicast packets with destination in the list of listening addresses.
+     * Ignore the min scope.
+     */
+    public static final int FORWARD_SELECTED = 1;
+    /**
+     * Forward all multicast packets with scope greater or equal than the min scope.
+     * Ignore the list of listening addresses.
+     */
+    public static final int FORWARD_WITH_MIN_SCOPE = 2;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "FORWARD_" }, value = {
+            FORWARD_NONE,
+            FORWARD_SELECTED,
+            FORWARD_WITH_MIN_SCOPE
+    })
+    public @interface MulticastForwardingMode {}
+
+    /**
+     * Not a multicast scope, for configurations that do not use the min scope.
+     */
+    public static final int MULTICAST_SCOPE_NONE = -1;
+
+    public static final MulticastRoutingConfig CONFIG_FORWARD_NONE =
+            new MulticastRoutingConfig(FORWARD_NONE, MULTICAST_SCOPE_NONE, null);
+
+    @MulticastForwardingMode
+    private final int mForwardingMode;
+
+    private final int mMinScope;
+
+    @NonNull
+    private final Set<Inet6Address> mListeningAddresses;
+
+    private MulticastRoutingConfig(@MulticastForwardingMode final int mode, final int scope,
+            @Nullable final Set<Inet6Address> addresses) {
+        mForwardingMode = mode;
+        mMinScope = scope;
+        if (null != addresses) {
+            mListeningAddresses = Collections.unmodifiableSet(new ArraySet<>(addresses));
+        } else {
+            mListeningAddresses = Collections.emptySet();
+        }
+    }
+
+    /**
+     * Returns the forwarding mode.
+     */
+    @MulticastForwardingMode
+    public int getForwardingMode() {
+        return mForwardingMode;
+    }
+
+    /**
+     * Returns the minimal group address scope that is allowed for forwarding.
+     * If the forwarding mode is not FORWARD_WITH_MIN_SCOPE, will be MULTICAST_SCOPE_NONE.
+     */
+    public int getMinScope() {
+        return mMinScope;
+    }
+
+    /**
+     * Returns the list of group addresses listened by the outgoing interface.
+     * The list will be empty if the forwarding mode is not FORWARD_SELECTED.
+     */
+    @NonNull
+    public Set<Inet6Address> getMulticastListeningAddresses() {
+        return mListeningAddresses;
+    }
+
+    private MulticastRoutingConfig(Parcel in) {
+        mForwardingMode = in.readInt();
+        mMinScope = in.readInt();
+        final int count = in.readInt();
+        final ArraySet<Inet6Address> listeningAddresses = new ArraySet<>(count);
+        final byte[] buffer = new byte[16]; // Size of an Inet6Address
+        for (int i = 0; i < count; ++i) {
+            in.readByteArray(buffer);
+            try {
+                listeningAddresses.add((Inet6Address) Inet6Address.getByAddress(buffer));
+            } catch (UnknownHostException e) {
+                Log.wtf(TAG, "Can't read inet6address : " + Arrays.toString(buffer));
+            }
+        }
+        mListeningAddresses = Collections.unmodifiableSet(listeningAddresses);
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mForwardingMode);
+        dest.writeInt(mMinScope);
+        dest.writeInt(mListeningAddresses.size());
+        for (final Inet6Address addr : mListeningAddresses) {
+            dest.writeByteArray(addr.getAddress());
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Creator<MulticastRoutingConfig> CREATOR = new Creator<>() {
+        @Override
+        public MulticastRoutingConfig createFromParcel(Parcel in) {
+            return new MulticastRoutingConfig(in);
+        }
+
+        @Override
+        public MulticastRoutingConfig[] newArray(int size) {
+            return new MulticastRoutingConfig[size];
+        }
+    };
+
+    public static class Builder {
+        @MulticastForwardingMode
+        private final int mForwardingMode;
+        private int mMinScope;
+        private final ArraySet<Inet6Address> mListeningAddresses;
+
+        private Builder(@MulticastForwardingMode final int mode, int scope) {
+            mForwardingMode = mode;
+            mMinScope = scope;
+            mListeningAddresses = new ArraySet<>();
+        }
+
+        /**
+         * Create a builder that forwards nothing.
+         * No properties can be set on such a builder.
+         */
+        public static Builder newBuilderForwardingNone() {
+            return new Builder(FORWARD_NONE, MULTICAST_SCOPE_NONE);
+        }
+
+        /**
+         * Create a builder that forwards packets above a certain scope
+         *
+         * The scope can be changed on this builder, but not the listening addresses.
+         * @param scope the initial scope
+         */
+        public static Builder newBuilderWithMinScope(final int scope) {
+            return new Builder(FORWARD_WITH_MIN_SCOPE, scope);
+        }
+
+        /**
+         * Create a builder that forwards a specified list of listening addresses.
+         *
+         * Addresses can be added and removed from this builder, but the scope can't be set.
+         */
+        public static Builder newBuilderWithListeningAddresses() {
+            return new Builder(FORWARD_SELECTED, MULTICAST_SCOPE_NONE);
+        }
+
+        /**
+         * Sets the minimum scope for this multicast routing config.
+         * This is only meaningful (indeed, allowed) for configs in FORWARD_WITH_MIN_SCOPE mode.
+         * @return this builder
+         */
+        public Builder setMinimumScope(final int scope) {
+            if (FORWARD_WITH_MIN_SCOPE != mForwardingMode) {
+                throw new IllegalArgumentException("Can't set the scope on a builder in mode "
+                        + modeToString(mForwardingMode));
+            }
+            mMinScope = scope;
+            return this;
+        }
+
+        /**
+         * Add an address to the set of listening addresses.
+         *
+         * This is only meaningful (indeed, allowed) for configs in FORWARD_SELECTED mode.
+         * If this address was already added, this is a no-op.
+         * @return this builder
+         */
+        public Builder addListeningAddress(@NonNull final Inet6Address address) {
+            if (FORWARD_SELECTED != mForwardingMode) {
+                throw new IllegalArgumentException("Can't add an address on a builder in mode "
+                        + modeToString(mForwardingMode));
+            }
+            // TODO : should we check that this is a multicast address ?
+            mListeningAddresses.add(address);
+            return this;
+        }
+
+        /**
+         * Remove an address from the set of listening addresses.
+         *
+         * This is only meaningful (indeed, allowed) for configs in FORWARD_SELECTED mode.
+         * If this address was not added, or was already removed, this is a no-op.
+         * @return this builder
+         */
+        public Builder removeListeningAddress(@NonNull final Inet6Address address) {
+            if (FORWARD_SELECTED != mForwardingMode) {
+                throw new IllegalArgumentException("Can't remove an address on a builder in mode "
+                        + modeToString(mForwardingMode));
+            }
+            mListeningAddresses.remove(address);
+            return this;
+        }
+
+        /**
+         * Build the config.
+         */
+        public MulticastRoutingConfig build() {
+            return new MulticastRoutingConfig(mForwardingMode, mMinScope, mListeningAddresses);
+        }
+    }
+
+    private static String modeToString(@MulticastForwardingMode final int mode) {
+        switch (mode) {
+            case FORWARD_NONE: return "FORWARD_NONE";
+            case FORWARD_SELECTED: return "FORWARD_SELECTED";
+            case FORWARD_WITH_MIN_SCOPE: return "FORWARD_WITH_MIN_SCOPE";
+            default: return "unknown multicast routing mode " + mode;
+        }
+    }
+}
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 177f7e3..4e9087c 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -151,7 +151,7 @@
 
     /**
      * Sent by the NetworkAgent to ConnectivityService to pass the current
-     * NetworkCapabilties.
+     * NetworkCapabilities.
      * obj = NetworkCapabilities
      * @hide
      */
@@ -443,6 +443,14 @@
     public static final int EVENT_UNREGISTER_AFTER_REPLACEMENT = BASE + 29;
 
     /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the new value of the local
+     * network agent config.
+     * obj = {@code Pair<NetworkAgentInfo, LocalNetworkConfig>}
+     * @hide
+     */
+    public static final int EVENT_LOCAL_NETWORK_CONFIG_CHANGED = BASE + 30;
+
+    /**
      * DSCP policy was successfully added.
      */
     public static final int DSCP_POLICY_STATUS_SUCCESS = 0;
@@ -517,20 +525,47 @@
             @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
             @NonNull NetworkScore score, @NonNull NetworkAgentConfig config,
             @Nullable NetworkProvider provider) {
-        this(looper, context, logTag, nc, lp, score, config,
+        this(context, looper, logTag, nc, lp, null /* localNetworkConfig */, score, config,
+                provider);
+    }
+
+    /**
+     * Create a new network agent.
+     * @param context a {@link Context} to get system services from.
+     * @param looper the {@link Looper} on which to invoke the callbacks.
+     * @param logTag the tag for logs
+     * @param nc the initial {@link NetworkCapabilities} of this network. Update with
+     *           sendNetworkCapabilities.
+     * @param lp the initial {@link LinkProperties} of this network. Update with sendLinkProperties.
+     * @param localNetworkConfig the initial {@link LocalNetworkConfig} of this
+     *                                  network. Update with sendLocalNetworkConfig. Must be
+     *                                  non-null iff the nc have NET_CAPABILITY_LOCAL_NETWORK.
+     * @param score the initial score of this network. Update with sendNetworkScore.
+     * @param config an immutable {@link NetworkAgentConfig} for this agent.
+     * @param provider the {@link NetworkProvider} managing this agent.
+     * @hide
+     */
+    // TODO : expose
+    public NetworkAgent(@NonNull Context context, @NonNull Looper looper, @NonNull String logTag,
+            @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
+            @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
+            @NonNull NetworkAgentConfig config, @Nullable NetworkProvider provider) {
+        this(looper, context, logTag, nc, lp, localNetworkConfig, score, config,
                 provider == null ? NetworkProvider.ID_NONE : provider.getProviderId(),
                 getLegacyNetworkInfo(config));
     }
 
     private static class InitialConfiguration {
-        public final Context context;
-        public final NetworkCapabilities capabilities;
-        public final LinkProperties properties;
-        public final NetworkScore score;
-        public final NetworkAgentConfig config;
-        public final NetworkInfo info;
+        @NonNull public final Context context;
+        @NonNull public final NetworkCapabilities capabilities;
+        @NonNull public final LinkProperties properties;
+        @NonNull public final NetworkScore score;
+        @NonNull public final NetworkAgentConfig config;
+        @NonNull public final NetworkInfo info;
+        @Nullable public final LocalNetworkConfig localNetworkConfig;
         InitialConfiguration(@NonNull Context context, @NonNull NetworkCapabilities capabilities,
-                @NonNull LinkProperties properties, @NonNull NetworkScore score,
+                @NonNull LinkProperties properties,
+                @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
                 @NonNull NetworkAgentConfig config, @NonNull NetworkInfo info) {
             this.context = context;
             this.capabilities = capabilities;
@@ -538,14 +573,15 @@
             this.score = score;
             this.config = config;
             this.info = info;
+            this.localNetworkConfig = localNetworkConfig;
         }
     }
     private volatile InitialConfiguration mInitialConfiguration;
 
     private NetworkAgent(@NonNull Looper looper, @NonNull Context context, @NonNull String logTag,
             @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
-            @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, int providerId,
-            @NonNull NetworkInfo ni) {
+            @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
+            @NonNull NetworkAgentConfig config, int providerId, @NonNull NetworkInfo ni) {
         mHandler = new NetworkAgentHandler(looper);
         LOG_TAG = logTag;
         mNetworkInfo = new NetworkInfo(ni);
@@ -556,7 +592,7 @@
 
         mInitialConfiguration = new InitialConfiguration(context,
                 new NetworkCapabilities(nc, NetworkCapabilities.REDACT_NONE),
-                new LinkProperties(lp), score, config, ni);
+                new LinkProperties(lp), localNetworkConfig, score, config, ni);
     }
 
     private class NetworkAgentHandler extends Handler {
@@ -723,7 +759,8 @@
             mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
                     new NetworkInfo(mInitialConfiguration.info),
                     mInitialConfiguration.properties, mInitialConfiguration.capabilities,
-                    mInitialConfiguration.score, mInitialConfiguration.config, providerId);
+                    mInitialConfiguration.localNetworkConfig, mInitialConfiguration.score,
+                    mInitialConfiguration.config, providerId);
             mInitialConfiguration = null; // All this memory can now be GC'd
         }
         return mNetwork;
@@ -1099,6 +1136,18 @@
     }
 
     /**
+     * Must be called by the agent when the network's {@link LocalNetworkConfig} changes.
+     * @param config the new LocalNetworkConfig
+     * @hide
+     */
+    public void sendLocalNetworkConfig(@NonNull LocalNetworkConfig config) {
+        Objects.requireNonNull(config);
+        // If the agent doesn't have NET_CAPABILITY_LOCAL_NETWORK, this will be ignored by
+        // ConnectivityService with a Log.wtf.
+        queueOrSendMessage(reg -> reg.sendLocalNetworkConfig(config));
+    }
+
+    /**
      * Must be called by the agent to update the score of this network.
      *
      * @param score the new score.
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
index 5480ef7..daa8fad 100644
--- a/netbpfload/Android.bp
+++ b/netbpfload/Android.bp
@@ -45,4 +45,6 @@
     // module "netbpfload" variant "android_x86_apex30": should support
     // min_sdk_version(30) for "com.android.tethering": newer SDK(34).
     min_sdk_version: "30",
+
+    // init_rc: ["netbpfload.rc"],
 }
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index b44a0bc..d150373 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -168,7 +168,7 @@
     return 0;
 }
 
-int main(int argc, char** argv) {
+int main(int argc, char** argv, char * const envp[]) {
     (void)argc;
     android::base::InitLogging(argv, &android::base::KernelLogger);
 
@@ -257,10 +257,12 @@
         return 1;
     }
 
-    if (android::base::SetProperty("bpf.progs_loaded", "1") == false) {
-        ALOGE("Failed to set bpf.progs_loaded property");
-        return 1;
+    ALOGI("done, transferring control to platform bpfloader.");
+
+    const char * args[] = { "/system/bin/bpfloader", NULL, };
+    if (execve(args[0], (char**)args, envp)) {
+        ALOGE("FATAL: execve('/system/bin/bpfloader'): %d[%s]", errno, strerror(errno));
     }
 
-    return 0;
+    return 1;
 }
diff --git a/netbpfload/netbpfload.rc b/netbpfload/netbpfload.rc
new file mode 100644
index 0000000..20fbb9f
--- /dev/null
+++ b/netbpfload/netbpfload.rc
@@ -0,0 +1,85 @@
+# zygote-start is what officially starts netd (see //system/core/rootdir/init.rc)
+# However, on some hardware it's started from post-fs-data as well, which is just
+# a tad earlier.  There's no benefit to that though, since on 4.9+ P+ devices netd
+# will just block until bpfloader finishes and sets the bpf.progs_loaded property.
+#
+# It is important that we start netbpfload after:
+#   - /sys/fs/bpf is already mounted,
+#   - apex (incl. rollback) is initialized (so that in the future we can load bpf
+#     programs shipped as part of apex mainline modules)
+#   - logd is ready for us to log stuff
+#
+# At the same time we want to be as early as possible to reduce races and thus
+# failures (before memory is fragmented, and cpu is busy running tons of other
+# stuff) and we absolutely want to be before netd and the system boot slot is
+# considered to have booted successfully.
+#
+on load_bpf_programs
+    exec_start netbpfload
+
+service netbpfload /system/bin/netbpfload
+    capabilities CHOWN SYS_ADMIN NET_ADMIN
+    # The following group memberships are a workaround for lack of DAC_OVERRIDE
+    # and allow us to open (among other things) files that we created and are
+    # no longer root owned (due to CHOWN) but still have group read access to
+    # one of the following groups.  This is not perfect, but a more correct
+    # solution requires significantly more effort to implement.
+    group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+    user root
+    #
+    # Set RLIMIT_MEMLOCK to 1GiB for netbpfload
+    #
+    # Actually only 8MiB would be needed if netbpfload ran as its own uid.
+    #
+    # However, while the rlimit is per-thread, the accounting is system wide.
+    # So, for example, if the graphics stack has already allocated 10MiB of
+    # memlock data before netbpfload even gets a chance to run, it would fail
+    # if its memlock rlimit is only 8MiB - since there would be none left for it.
+    #
+    # netbpfload succeeding is critical to system health, since a failure will
+    # cause netd crashloop and thus system server crashloop... and the only
+    # recovery is a full kernel reboot.
+    #
+    # We've had issues where devices would sometimes (rarely) boot into
+    # a crashloop because netbpfload would occasionally lose a boot time
+    # race against the graphics stack's boot time locked memory allocation.
+    #
+    # Thus netbpfload's memlock has to be 8MB higher then the locked memory
+    # consumption of the root uid anywhere else in the system...
+    # But we don't know what that is for all possible devices...
+    #
+    # Ideally, we'd simply grant netbpfload the IPC_LOCK capability and it
+    # would simply ignore it's memlock rlimit... but it turns that this
+    # capability is not even checked by the kernel's bpf system call.
+    #
+    # As such we simply use 1GiB as a reasonable approximation of infinity.
+    #
+    rlimit memlock 1073741824 1073741824
+    oneshot
+    #
+    # How to debug bootloops caused by 'netbpfload-failed'.
+    #
+    # 1. On some lower RAM devices (like wembley) you may need to first enable developer mode
+    #    (from the Settings app UI), and change the developer option "Logger buffer sizes"
+    #    from the default (wembley: 64kB) to the maximum (1M) per log buffer.
+    #    Otherwise buffer will overflow before you manage to dump it and you'll get useless logs.
+    #
+    # 2. comment out 'reboot_on_failure reboot,netbpfload-failed' below
+    # 3. rebuild/reflash/reboot
+    # 4. as the device is booting up capture netbpfload logs via:
+    #    adb logcat -s 'NetBpfLoad:*' 'NetBpfLoader:*'
+    #
+    # something like:
+    #   $ adb reboot; sleep 1; adb wait-for-device; adb root; sleep 1; adb wait-for-device; adb logcat -s 'NetBpfLoad:*' 'NetBpfLoader:*'
+    # will take care of capturing logs as early as possible
+    #
+    # 5. look through the logs from the kernel's bpf verifier that netbpfload dumps out,
+    #    it usually makes sense to search back from the end and find the particular
+    #    bpf verifier failure that caused netbpfload to terminate early with an error code.
+    #    This will probably be something along the lines of 'too many jumps' or
+    #    'cannot prove return value is 0 or 1' or 'unsupported / unknown operation / helper',
+    #    'invalid bpf_context access', etc.
+    #
+    reboot_on_failure reboot,netbpfload-failed
+    # we're not really updatable, but want to be able to load bpf programs shipped in apexes
+    updatable
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 25e59d5..cc67550 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -517,11 +517,12 @@
                     break;
                 }
                 case MSG_NOTIFY_NETWORK_STATUS: {
-                    // If no cached states, ignore.
-                    if (mLastNetworkStateSnapshots == null) break;
-                    // TODO (b/181642673): Protect mDefaultNetworks from concurrent accessing.
-                    handleNotifyNetworkStatus(
-                            mDefaultNetworks, mLastNetworkStateSnapshots, mActiveIface);
+                    synchronized (mStatsLock) {
+                        // If no cached states, ignore.
+                        if (mLastNetworkStateSnapshots == null) break;
+                        handleNotifyNetworkStatus(
+                                mDefaultNetworks, mLastNetworkStateSnapshots, mActiveIface);
+                    }
                     break;
                 }
                 case MSG_PERFORM_POLL_REGISTER_ALERT: {
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index c19878c..2797b47 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -166,6 +166,7 @@
 import android.net.IpMemoryStore;
 import android.net.IpPrefix;
 import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
 import android.net.MatchAllNetworkSpecifier;
 import android.net.NativeNetworkConfig;
 import android.net.NativeNetworkType;
@@ -1798,7 +1799,7 @@
         mNoServiceNetwork = new NetworkAgentInfo(null,
                 new Network(INetd.UNREACHABLE_NET_ID),
                 new NetworkInfo(TYPE_NONE, 0, "", ""),
-                new LinkProperties(), new NetworkCapabilities(),
+                new LinkProperties(), new NetworkCapabilities(), null /* localNetworkConfig */,
                 new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null,
                 new NetworkAgentConfig(), this, null, null, 0, INVALID_UID,
                 mLingerDelayMs, mQosCallbackTracker, mDeps);
@@ -4169,6 +4170,11 @@
                     updateNetworkInfo(nai, info);
                     break;
                 }
+                case NetworkAgent.EVENT_LOCAL_NETWORK_CONFIG_CHANGED: {
+                    final LocalNetworkConfig config = (LocalNetworkConfig) arg.second;
+                    updateLocalNetworkConfig(nai, config);
+                    break;
+                }
                 case NetworkAgent.EVENT_NETWORK_SCORE_CHANGED: {
                     updateNetworkScore(nai, (NetworkScore) arg.second);
                     break;
@@ -8130,13 +8136,18 @@
      * @param networkCapabilities the initial capabilites of this network. They can be updated
      *         later : see {@link #updateCapabilities}.
      * @param initialScore the initial score of the network. See {@link NetworkAgentInfo#getScore}.
+     * @param localNetworkConfig config about this local network, or null if not a local network
      * @param networkAgentConfig metadata about the network. This is never updated.
      * @param providerId the ID of the provider owning this NetworkAgent.
      * @return the network created for this agent.
      */
-    public Network registerNetworkAgent(INetworkAgent na, NetworkInfo networkInfo,
-            LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
-            @NonNull NetworkScore initialScore, NetworkAgentConfig networkAgentConfig,
+    public Network registerNetworkAgent(INetworkAgent na,
+            NetworkInfo networkInfo,
+            LinkProperties linkProperties,
+            NetworkCapabilities networkCapabilities,
+            @NonNull NetworkScore initialScore,
+            @Nullable LocalNetworkConfig localNetworkConfig,
+            NetworkAgentConfig networkAgentConfig,
             int providerId) {
         Objects.requireNonNull(networkInfo, "networkInfo must not be null");
         Objects.requireNonNull(linkProperties, "linkProperties must not be null");
@@ -8154,12 +8165,20 @@
             // Before U, netd doesn't support PHYSICAL_LOCAL networks so this can't work.
             throw new IllegalArgumentException("Local agents are not supported in this version");
         }
+        final boolean hasLocalNetworkConfig = null != localNetworkConfig;
+        if (hasLocalCap != hasLocalNetworkConfig) {
+            throw new IllegalArgumentException(null != localNetworkConfig
+                    ? "Only local network agents can have a LocalNetworkConfig"
+                    : "Local network agents must have a LocalNetworkConfig"
+            );
+        }
 
         final int uid = mDeps.getCallingUid();
         final long token = Binder.clearCallingIdentity();
         try {
             return registerNetworkAgentInternal(na, networkInfo, linkProperties,
-                    networkCapabilities, initialScore, networkAgentConfig, providerId, uid);
+                    networkCapabilities, initialScore, networkAgentConfig, localNetworkConfig,
+                    providerId, uid);
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -8167,7 +8186,8 @@
 
     private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
             LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
-            NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId,
+            NetworkScore currentScore, NetworkAgentConfig networkAgentConfig,
+            @Nullable LocalNetworkConfig localNetworkConfig, int providerId,
             int uid) {
 
         // Make a copy of the passed NI, LP, NC as the caller may hold a reference to them
@@ -8175,6 +8195,7 @@
         final NetworkInfo niCopy = new NetworkInfo(networkInfo);
         final NetworkCapabilities ncCopy = new NetworkCapabilities(networkCapabilities);
         final LinkProperties lpCopy = new LinkProperties(linkProperties);
+        // No need to copy |localNetworkConfiguration| as it is immutable.
 
         // At this point the capabilities/properties are untrusted and unverified, e.g. checks that
         // the capabilities' access UIDs comply with security limitations. They will be sanitized
@@ -8182,9 +8203,9 @@
         // because some of the checks must happen on the handler thread.
         final NetworkAgentInfo nai = new NetworkAgentInfo(na,
                 new Network(mNetIdManager.reserveNetId()), niCopy, lpCopy, ncCopy,
-                currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
-                this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
-                mQosCallbackTracker, mDeps);
+                localNetworkConfig, currentScore, mContext, mTrackerHandler,
+                new NetworkAgentConfig(networkAgentConfig), this, mNetd, mDnsResolver, providerId,
+                uid, mLingerDelayMs, mQosCallbackTracker, mDeps);
 
         final String extraInfo = niCopy.getExtraInfo();
         final String name = TextUtils.isEmpty(extraInfo)
@@ -8919,6 +8940,16 @@
         updateCapabilities(nai.getScore(), nai, nai.networkCapabilities);
     }
 
+    private void updateLocalNetworkConfig(@NonNull final NetworkAgentInfo nai,
+            @NonNull final LocalNetworkConfig config) {
+        if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+            Log.wtf(TAG, "Ignoring update of a local network info on non-local network " + nai);
+            return;
+        }
+        // TODO : actually apply the diff.
+        nai.localNetworkConfig = config;
+    }
+
     /**
      * Returns the interface which requires VPN isolation (ingress interface filtering).
      *
@@ -10807,6 +10838,17 @@
                         Log.d(TAG, "Reevaluating network " + nai.network);
                         reportNetworkConnectivity(nai.network, !nai.isValidated());
                         return 0;
+                    case "bpf-get-cgroup-program-id": {
+                        // Usage : adb shell cmd connectivity bpf-get-cgroup-program-id <type>
+                        // Get cgroup bpf program Id for the given type. See BpfUtils#getProgramId
+                        // for more detail.
+                        // If type can't be parsed, this throws NumberFormatException, which
+                        // is passed back to adb who prints it.
+                        final int type = Integer.parseInt(getNextArg());
+                        final int ret = BpfUtils.getProgramId(type, BpfUtils.CGROUP_PATH);
+                        pw.println(ret);
+                        return 0;
+                    }
                     default:
                         return handleDefaultCommands(cmd);
                 }
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 8d0d711..b0ad978 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -35,6 +35,7 @@
 import android.net.INetworkAgentRegistry;
 import android.net.INetworkMonitor;
 import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
 import android.net.NattKeepalivePacketData;
 import android.net.Network;
 import android.net.NetworkAgent;
@@ -173,6 +174,7 @@
     // TODO: make this private with a getter.
     @NonNull public NetworkCapabilities networkCapabilities;
     @NonNull public final NetworkAgentConfig networkAgentConfig;
+    @Nullable public LocalNetworkConfig localNetworkConfig;
 
     // Underlying networks declared by the agent.
     // The networks in this list might be declared by a VPN using setUnderlyingNetworks and are
@@ -609,6 +611,7 @@
 
     public NetworkAgentInfo(INetworkAgent na, Network net, NetworkInfo info,
             @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+            @Nullable LocalNetworkConfig localNetworkConfig,
             @NonNull NetworkScore score, Context context,
             Handler handler, NetworkAgentConfig config, ConnectivityService connService, INetd netd,
             IDnsResolver dnsResolver, int factorySerialNumber, int creatorUid,
@@ -626,6 +629,7 @@
         networkInfo = info;
         linkProperties = lp;
         networkCapabilities = nc;
+        this.localNetworkConfig = localNetworkConfig;
         networkAgentConfig = config;
         mConnService = connService;
         mConnServiceDeps = deps;
@@ -905,6 +909,12 @@
         }
 
         @Override
+        public void sendLocalNetworkConfig(@NonNull final LocalNetworkConfig config) {
+            mHandler.obtainMessage(NetworkAgent.EVENT_LOCAL_NETWORK_CONFIG_CHANGED,
+                    new Pair<>(NetworkAgentInfo.this, config)).sendToTarget();
+        }
+
+        @Override
         public void sendScore(@NonNull final NetworkScore score) {
             mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_SCORE_CHANGED,
                     new Pair<>(NetworkAgentInfo.this, score)).sendToTarget();
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
index 98fda56..ea18d37 100644
--- a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -159,9 +159,11 @@
             throws RemoteException, ServiceSpecificException {
         netd.tetherInterfaceAdd(iface);
         networkAddInterface(netd, iface, maxAttempts, pollingIntervalMs);
-        List<RouteInfo> routes = new ArrayList<>();
-        routes.add(new RouteInfo(dest, null, iface, RTN_UNICAST));
-        addRoutesToLocalNetwork(netd, iface, routes);
+        // Activate a route to dest and IPv6 link local.
+        modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+                new RouteInfo(dest, null, iface, RTN_UNICAST));
+        modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+                new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
     }
 
     /**
diff --git a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
index 176b7bc..e9c39e4 100644
--- a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
+++ b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
@@ -108,6 +108,13 @@
     }
 
     /**
+     * Check whether or not IA Prefix option has 0 preferred and valid lifetimes.
+     */
+    public boolean withZeroLifetimes() {
+        return preferred == 0 && valid == 0;
+    }
+
+    /**
      * Build an IA_PD prefix option with given specific parameters.
      */
     public static ByteBuffer build(final short length, final long preferred, final long valid,
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 5937655..392cba9 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -704,6 +704,7 @@
                 argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
                 any(LinkProperties::class.java),
                 any(NetworkCapabilities::class.java),
+                any(), // LocalNetworkConfig TODO : specify when it's public
                 any(NetworkScore::class.java),
                 any(NetworkAgentConfig::class.java),
                 eq(NetworkProvider.ID_NONE))
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index cc11361..42b0d59 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -12833,7 +12833,8 @@
 
     private NetworkAgentInfo fakeNai(NetworkCapabilities nc, NetworkInfo networkInfo) {
         return new NetworkAgentInfo(null, new Network(NET_ID), networkInfo, new LinkProperties(),
-                nc, new NetworkScore.Builder().setLegacyInt(0).build(),
+                nc, null /* localNetworkConfig */,
+                new NetworkScore.Builder().setLegacyInt(0).build(),
                 mServiceContext, null, new NetworkAgentConfig(), mService, null, null, 0,
                 INVALID_UID, TEST_LINGER_DELAY_MS, mQosCallbackTracker,
                 new ConnectivityService.Dependencies());
diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
index e6c0c83..07883ff 100644
--- a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -372,9 +372,10 @@
         caps.addCapability(0);
         caps.addTransportType(transport);
         NetworkAgentInfo nai = new NetworkAgentInfo(null, new Network(netId), info,
-                new LinkProperties(), caps, new NetworkScore.Builder().setLegacyInt(50).build(),
-                mCtx, null, new NetworkAgentConfig.Builder().build(), mConnService, mNetd,
-                mDnsResolver, NetworkProvider.ID_NONE, Binder.getCallingUid(), TEST_LINGER_DELAY_MS,
+                new LinkProperties(), caps, null /* localNetworkConfiguration */,
+                new NetworkScore.Builder().setLegacyInt(50).build(), mCtx, null,
+                new NetworkAgentConfig.Builder().build(), mConnService, mNetd, mDnsResolver,
+                NetworkProvider.ID_NONE, Binder.getCallingUid(), TEST_LINGER_DELAY_MS,
                 mQosCallbackTracker, new ConnectivityService.Dependencies());
         if (setEverValidated) {
             // As tests in this class deal with testing lingering, most tests are interested
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index d674767..478ff61 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -3004,8 +3004,15 @@
         profile.mppe = useMppe;
 
         doReturn(new Network[] { new Network(101) }).when(mConnectivityManager).getAllNetworks();
-        doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(any(), any(),
-                any(), any(), any(), any(), anyInt());
+        doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(
+                any(), // INetworkAgent
+                any(), // NetworkInfo
+                any(), // LinkProperties
+                any(), // NetworkCapabilities
+                any(), // LocalNetworkConfig
+                any(), // NetworkScore
+                any(), // NetworkAgentConfig
+                anyInt()); // provider ID
 
         final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
         final TestDeps deps = (TestDeps) vpn.mDeps;
@@ -3027,8 +3034,15 @@
                 assertEquals("nomppe", mtpdArgs[argsPrefix.length]);
             }
 
-            verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
-                    any(), any(), any(), any(), anyInt());
+            verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(
+                    any(), // INetworkAgent
+                    any(), // NetworkInfo
+                    any(), // LinkProperties
+                    any(), // NetworkCapabilities
+                    any(), // LocalNetworkConfig
+                    any(), // NetworkScore
+                    any(), // NetworkAgentConfig
+                    anyInt()); // provider ID
         }, () -> { // Cleanup
                 vpn.mVpnRunner.exitVpnRunner();
                 deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
@@ -3053,7 +3067,7 @@
             .thenReturn(new Network[] { new Network(101) });
 
         when(mConnectivityManager.registerNetworkAgent(any(), any(), any(), any(),
-                any(), any(), anyInt())).thenAnswer(invocation -> {
+                any(), any(), any(), anyInt())).thenAnswer(invocation -> {
                     // The runner has registered an agent and is now ready.
                     legacyRunnerReady.open();
                     return new Network(102);
@@ -3079,7 +3093,7 @@
             ArgumentCaptor<NetworkCapabilities> ncCaptor =
                     ArgumentCaptor.forClass(NetworkCapabilities.class);
             verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
-                    lpCaptor.capture(), ncCaptor.capture(), any(), any(), anyInt());
+                    lpCaptor.capture(), ncCaptor.capture(), any(), any(), any(), anyInt());
 
             // In this test the expected address is always v4 so /32.
             // Note that the interface needs to be specified because RouteInfo objects stored in
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
index 86426c2..6220e76 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.server
 
+import android.net.LocalNetworkConfig
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
@@ -45,8 +46,9 @@
                 .build()
         val keepConnectedAgent = Agent(nc = nc, score = FromS(NetworkScore.Builder()
                 .setKeepConnectedReason(KEEP_CONNECTED_DOWNSTREAM_NETWORK)
-                .build()))
-        val dontKeepConnectedAgent = Agent(nc = nc)
+                .build()),
+                lnc = LocalNetworkConfig.Builder().build())
+        val dontKeepConnectedAgent = Agent(nc = nc, lnc = LocalNetworkConfig.Builder().build())
         doTestKeepConnected(keepConnectedAgent, dontKeepConnectedAgent)
     }
 
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
index 7914e04..cfc3a3d 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -18,6 +18,7 @@
 
 import android.content.pm.PackageManager.FEATURE_LEANBACK
 import android.net.INetd
+import android.net.LocalNetworkConfig
 import android.net.NativeNetworkConfig
 import android.net.NativeNetworkType
 import android.net.NetworkCapabilities
@@ -48,6 +49,8 @@
 private fun keepConnectedScore() =
         FromS(NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build())
 
+private fun defaultLnc() = LocalNetworkConfig.Builder().build()
+
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
 @IgnoreUpTo(Build.VERSION_CODES.R)
@@ -93,9 +96,9 @@
             addCapability(NET_CAPABILITY_LOCAL_NETWORK)
         }.build()
         val localAgent = if (sdkLevel >= VERSION_V || sdkLevel == VERSION_U && isTv) {
-            Agent(nc = ncTemplate, score = keepConnectedScore())
+            Agent(nc = ncTemplate, score = keepConnectedScore(), lnc = defaultLnc())
         } else {
-            assertFailsWith<IllegalArgumentException> { Agent(nc = ncTemplate) }
+            assertFailsWith<IllegalArgumentException> { Agent(nc = ncTemplate, lnc = defaultLnc()) }
             netdInOrder.verify(netd, never()).networkCreate(any())
             return
         }
@@ -111,4 +114,18 @@
         localAgent.disconnect()
         netdInOrder.verify(netd, timeout(TIMEOUT_MS)).networkDestroy(localAgent.network.netId)
     }
+
+    @Test
+    fun testBadAgents() {
+        assertFailsWith<IllegalArgumentException> {
+            Agent(nc = NetworkCapabilities.Builder()
+                    .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                    .build(),
+                    lnc = null)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            Agent(nc = NetworkCapabilities.Builder().build(),
+                    lnc = LocalNetworkConfig.Builder().build())
+        }
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
new file mode 100644
index 0000000..bd3efa9
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 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 com.android.server
+
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.LocalNetworkConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.RouteInfo
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertFailsWith
+
+private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
+    addTransportType(transport)
+    caps.forEach {
+        addCapability(it)
+    }
+    // Useful capabilities for everybody
+    addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+    addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+    addCapability(NET_CAPABILITY_NOT_ROAMING)
+    addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+}.build()
+
+private fun lp(iface: String) = LinkProperties().apply {
+    interfaceName = iface
+    addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+    addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class CSLocalAgentTests : CSTest() {
+    @Test
+    fun testBadAgents() {
+        assertFailsWith<IllegalArgumentException> {
+            Agent(nc = NetworkCapabilities.Builder()
+                    .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                    .build(),
+                    lnc = null)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            Agent(nc = NetworkCapabilities.Builder().build(),
+                    lnc = LocalNetworkConfig.Builder().build())
+        }
+    }
+
+    @Test
+    fun testUpdateLocalAgentConfig() {
+        deps.setBuildSdk(VERSION_V)
+
+        val cb = TestableNetworkCallback()
+        cm.requestNetwork(NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+                .build(),
+                cb)
+
+        // Set up a local agent that should forward its traffic to the best DUN upstream.
+        val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+                lp = lp("local0"),
+                lnc = LocalNetworkConfig.Builder().build(),
+        )
+        localAgent.connect()
+
+        cb.expect<Available>(localAgent.network)
+        cb.expect<CapabilitiesChanged>(localAgent.network)
+        cb.expect<LinkPropertiesChanged>(localAgent.network)
+        cb.expect<BlockedStatus>(localAgent.network)
+
+        val newLnc = LocalNetworkConfig.Builder()
+                .setUpstreamSelector(NetworkRequest.Builder()
+                        .addTransportType(TRANSPORT_WIFI)
+                        .build())
+                .build()
+        localAgent.sendLocalNetworkConfig(newLnc)
+
+        localAgent.disconnect()
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index 1d0d5df..094ded3 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -21,6 +21,7 @@
 import android.net.INetworkMonitor
 import android.net.INetworkMonitorCallbacks
 import android.net.LinkProperties
+import android.net.LocalNetworkConfig
 import android.net.Network
 import android.net.NetworkAgent
 import android.net.NetworkAgentConfig
@@ -69,6 +70,7 @@
         nac: NetworkAgentConfig,
         val nc: NetworkCapabilities,
         val lp: LinkProperties,
+        val lnc: LocalNetworkConfig?,
         val score: FromS<NetworkScore>,
         val provider: NetworkProvider?
 ) : TestableNetworkCallback.HasNetwork {
@@ -100,7 +102,7 @@
         // Create the actual agent. NetworkAgent is abstract, so make an anonymous subclass.
         if (deps.isAtLeastS()) {
             agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
-                    nc, lp, score.value, nac, provider) {}
+                    nc, lp, lnc, score.value, nac, provider) {}
         } else {
             agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
                     nc, lp, 50 /* score */, nac, provider) {}
@@ -165,4 +167,6 @@
         agent.unregister()
         cb.eventuallyExpect<Lost> { it.network == agent.network }
     }
+
+    fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 2f78212..0ccbfc3 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -27,6 +27,7 @@
 import android.net.INetd
 import android.net.InetAddresses
 import android.net.LinkProperties
+import android.net.LocalNetworkConfig
 import android.net.NetworkAgentConfig
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
@@ -292,10 +293,11 @@
             nc: NetworkCapabilities = defaultNc(),
             nac: NetworkAgentConfig = emptyAgentConfig(nc.getLegacyType()),
             lp: LinkProperties = defaultLp(),
+            lnc: LocalNetworkConfig? = null,
             score: FromS<NetworkScore> = defaultScore(),
             provider: NetworkProvider? = null
-    ) = CSAgentWrapper(context, deps, csHandlerThread, networkStack, nac, nc, lp, score, provider)
-
+    ) = CSAgentWrapper(context, deps, csHandlerThread, networkStack,
+            nac, nc, lp, lnc, score, provider)
     fun Agent(vararg transports: Int, lp: LinkProperties = defaultLp()): CSAgentWrapper {
         val nc = NetworkCapabilities.Builder().apply {
             transports.forEach {