Add firewall chains for HAPPY_BOX and user/admin PENALTY_BOX

Follow up CLs will update CS to generate blocked reason from bpf map
instead of asking NPMS.

However, one issue is NPMS set PENALTY_BOX for
BLOCKED_METERED_REASON_USER_RESTRICTED and
BLOCKED_METERED_REASON_ADMIN_DISABLED without telling the reason.
So, CS can not know the reason of PENALTY_BOX.

This CL add new firewall chain and match to distinguish reasons.
NPMS must call setUidFirewallRule with
FIREWALL_CHAIN_METERED_DENY_USER or FIREWALL_CHAIN_METERED_DENY_ADMIN
based on the reason so that CS can know the reason of restriction.

Bug: 332628891
Test: atest com.android.cts.net.HostsideRestrictBackgroundNetworkTests
Change-Id: Ia4ad4bdb345abc22c782630e828edfad2452db36
diff --git a/DnsResolver/DnsBpfHelper.cpp b/DnsResolver/DnsBpfHelper.cpp
index de8bef5..0719ade 100644
--- a/DnsResolver/DnsBpfHelper.cpp
+++ b/DnsResolver/DnsBpfHelper.cpp
@@ -69,9 +69,10 @@
   // state, making it a trustworthy source. Since this library primarily serves DNS resolvers,
   // relying solely on V+ data prevents erroneous blocking of DNS queries.
   if (android::modules::sdklevel::IsAtLeastV() && metered) {
-    // The background data setting (PENALTY_BOX_MATCH) and unrestricted data usage setting
-    // (HAPPY_BOX_MATCH) for individual apps override the system wide Data Saver setting.
-    if (uidRules & PENALTY_BOX_MATCH) return true;
+    // The background data setting (PENALTY_BOX_USER_MATCH, PENALTY_BOX_ADMIN_MATCH) and
+    // unrestricted data usage setting (HAPPY_BOX_MATCH) for individual apps override the system
+    // wide Data Saver setting.
+    if (uidRules & (PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH)) return true;
     if (uidRules & HAPPY_BOX_MATCH) return false;
 
     auto dataSaverSetting = mDataSaverEnabledMap.readValue(DATA_SAVER_ENABLED_KEY);
diff --git a/DnsResolver/DnsBpfHelperTest.cpp b/DnsResolver/DnsBpfHelperTest.cpp
index 67b5b95..18a5df4 100644
--- a/DnsResolver/DnsBpfHelperTest.cpp
+++ b/DnsResolver/DnsBpfHelperTest.cpp
@@ -158,23 +158,33 @@
     }
   } testConfigs[]{
     // clang-format off
-    // enabledRules, dataSaverEnabled, uidRules,                                        blocked
-    {NO_MATCH,       false,            NO_MATCH,                                        false},
-    {NO_MATCH,       false,            PENALTY_BOX_MATCH,                               true},
-    {NO_MATCH,       false,            HAPPY_BOX_MATCH,                                 false},
-    {NO_MATCH,       false,            PENALTY_BOX_MATCH|HAPPY_BOX_MATCH,               true},
-    {NO_MATCH,       true,             NO_MATCH,                                        true},
-    {NO_MATCH,       true,             PENALTY_BOX_MATCH,                               true},
-    {NO_MATCH,       true,             HAPPY_BOX_MATCH,                                 false},
-    {NO_MATCH,       true,             PENALTY_BOX_MATCH|HAPPY_BOX_MATCH,               true},
-    {STANDBY_MATCH,  false,            STANDBY_MATCH,                                   true},
-    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_MATCH,                 true},
-    {STANDBY_MATCH,  false,            STANDBY_MATCH|HAPPY_BOX_MATCH,                   true},
-    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_MATCH|HAPPY_BOX_MATCH, true},
-    {STANDBY_MATCH,  true,             STANDBY_MATCH,                                   true},
-    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_MATCH,                 true},
-    {STANDBY_MATCH,  true,             STANDBY_MATCH|HAPPY_BOX_MATCH,                   true},
-    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_MATCH|HAPPY_BOX_MATCH, true},
+    // enabledRules, dataSaverEnabled, uidRules,                                            blocked
+    {NO_MATCH,       false,            NO_MATCH,                                             false},
+    {NO_MATCH,       false,            PENALTY_BOX_USER_MATCH,                                true},
+    {NO_MATCH,       false,            PENALTY_BOX_ADMIN_MATCH,                               true},
+    {NO_MATCH,       false,            PENALTY_BOX_USER_MATCH|PENALTY_BOX_ADMIN_MATCH,        true},
+    {NO_MATCH,       false,            HAPPY_BOX_MATCH,                                      false},
+    {NO_MATCH,       false,            PENALTY_BOX_USER_MATCH|HAPPY_BOX_MATCH,                true},
+    {NO_MATCH,       false,            PENALTY_BOX_ADMIN_MATCH|HAPPY_BOX_MATCH,               true},
+    {NO_MATCH,       true,             NO_MATCH,                                              true},
+    {NO_MATCH,       true,             PENALTY_BOX_USER_MATCH,                                true},
+    {NO_MATCH,       true,             PENALTY_BOX_ADMIN_MATCH,                               true},
+    {NO_MATCH,       true,             PENALTY_BOX_USER_MATCH|PENALTY_BOX_ADMIN_MATCH,        true},
+    {NO_MATCH,       true,             HAPPY_BOX_MATCH,                                      false},
+    {NO_MATCH,       true,             PENALTY_BOX_USER_MATCH|HAPPY_BOX_MATCH,                true},
+    {NO_MATCH,       true,             PENALTY_BOX_ADMIN_MATCH|HAPPY_BOX_MATCH,               true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH,                                         true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_USER_MATCH,                  true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_ADMIN_MATCH,                 true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|HAPPY_BOX_MATCH,                         true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_USER_MATCH|HAPPY_BOX_MATCH,  true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_ADMIN_MATCH|HAPPY_BOX_MATCH, true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH,                                         true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_USER_MATCH,                  true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_ADMIN_MATCH,                 true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|HAPPY_BOX_MATCH,                         true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_USER_MATCH|HAPPY_BOX_MATCH,  true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_ADMIN_MATCH|HAPPY_BOX_MATCH, true},
     // clang-format on
   };
 
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index dfc7699..2aff89c 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -644,7 +644,8 @@
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
-    if (denylistMatch) return denylistMatch->rule & PENALTY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
+    uint32_t penalty_box = PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH;
+    if (denylistMatch) return denylistMatch->rule & penalty_box ? BPF_MATCH : BPF_NOMATCH;
     return BPF_NOMATCH;
 }
 
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index 098147f..8a56b4a 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -181,7 +181,7 @@
 enum UidOwnerMatchType : uint32_t {
     NO_MATCH = 0,
     HAPPY_BOX_MATCH = (1 << 0),
-    PENALTY_BOX_MATCH = (1 << 1),
+    PENALTY_BOX_USER_MATCH = (1 << 1),
     DOZABLE_MATCH = (1 << 2),
     STANDBY_MATCH = (1 << 3),
     POWERSAVE_MATCH = (1 << 4),
@@ -192,7 +192,8 @@
     OEM_DENY_1_MATCH = (1 << 9),
     OEM_DENY_2_MATCH = (1 << 10),
     OEM_DENY_3_MATCH = (1 << 11),
-    BACKGROUND_MATCH = (1 << 12)
+    BACKGROUND_MATCH = (1 << 12),
+    PENALTY_BOX_ADMIN_MATCH = (1 << 13),
 };
 // LINT.ThenChange(../framework/src/android/net/BpfNetMapsConstants.java)
 
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 026d8a9..b2aafa0 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -56,6 +56,9 @@
     field @FlaggedApi("com.android.net.flags.basic_background_restrictions_enabled") public static final int FIREWALL_CHAIN_BACKGROUND = 6; // 0x6
     field public static final int FIREWALL_CHAIN_DOZABLE = 1; // 0x1
     field public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5; // 0x5
+    field @FlaggedApi("com.android.net.flags.metered_network_firewall_chains") public static final int FIREWALL_CHAIN_METERED_ALLOW = 10; // 0xa
+    field @FlaggedApi("com.android.net.flags.metered_network_firewall_chains") public static final int FIREWALL_CHAIN_METERED_DENY_ADMIN = 12; // 0xc
+    field @FlaggedApi("com.android.net.flags.metered_network_firewall_chains") public static final int FIREWALL_CHAIN_METERED_DENY_USER = 11; // 0xb
     field public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7; // 0x7
     field public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8; // 0x8
     field public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9; // 0x9
diff --git a/framework/src/android/net/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java
index 5d0fe73..f3773de 100644
--- a/framework/src/android/net/BpfNetMapsConstants.java
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -19,6 +19,9 @@
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_ADMIN;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
@@ -67,7 +70,7 @@
     // LINT.IfChange(match_type)
     public static final long NO_MATCH = 0;
     public static final long HAPPY_BOX_MATCH = (1 << 0);
-    public static final long PENALTY_BOX_MATCH = (1 << 1);
+    public static final long PENALTY_BOX_USER_MATCH = (1 << 1);
     public static final long DOZABLE_MATCH = (1 << 2);
     public static final long STANDBY_MATCH = (1 << 3);
     public static final long POWERSAVE_MATCH = (1 << 4);
@@ -79,10 +82,11 @@
     public static final long OEM_DENY_2_MATCH = (1 << 10);
     public static final long OEM_DENY_3_MATCH = (1 << 11);
     public static final long BACKGROUND_MATCH = (1 << 12);
+    public static final long PENALTY_BOX_ADMIN_MATCH = (1 << 13);
 
     public static final List<Pair<Long, String>> MATCH_LIST = Arrays.asList(
             Pair.create(HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"),
-            Pair.create(PENALTY_BOX_MATCH, "PENALTY_BOX_MATCH"),
+            Pair.create(PENALTY_BOX_USER_MATCH, "PENALTY_BOX_USER_MATCH"),
             Pair.create(DOZABLE_MATCH, "DOZABLE_MATCH"),
             Pair.create(STANDBY_MATCH, "STANDBY_MATCH"),
             Pair.create(POWERSAVE_MATCH, "POWERSAVE_MATCH"),
@@ -93,11 +97,13 @@
             Pair.create(OEM_DENY_1_MATCH, "OEM_DENY_1_MATCH"),
             Pair.create(OEM_DENY_2_MATCH, "OEM_DENY_2_MATCH"),
             Pair.create(OEM_DENY_3_MATCH, "OEM_DENY_3_MATCH"),
-            Pair.create(BACKGROUND_MATCH, "BACKGROUND_MATCH")
+            Pair.create(BACKGROUND_MATCH, "BACKGROUND_MATCH"),
+            Pair.create(PENALTY_BOX_ADMIN_MATCH, "PENALTY_BOX_ADMIN_MATCH")
     );
 
     /**
-     * List of all firewall allow chains.
+     * List of all firewall allow chains that are applied to all networks regardless of meteredness
+     * See {@link #METERED_ALLOW_CHAINS} for allow chains that are only applied to metered networks.
      *
      * Allow chains mean the firewall denies all uids by default, uids must be explicitly allowed.
      */
@@ -110,7 +116,8 @@
     );
 
     /**
-     * List of all firewall deny chains.
+     * List of all firewall deny chains that are applied to all networks regardless of meteredness
+     * See {@link #METERED_DENY_CHAINS} for deny chains that are only applied to metered networks.
      *
      * Deny chains mean the firewall allows all uids by default, uids must be explicitly denied.
      */
@@ -120,5 +127,24 @@
             FIREWALL_CHAIN_OEM_DENY_2,
             FIREWALL_CHAIN_OEM_DENY_3
     );
+
+    /**
+     * List of all firewall allow chains that are only applied to metered networks.
+     * See {@link #ALLOW_CHAINS} for allow chains that are applied to all networks regardless of
+     * meteredness.
+     */
+    public static final List<Integer> METERED_ALLOW_CHAINS = List.of(
+            FIREWALL_CHAIN_METERED_ALLOW
+    );
+
+    /**
+     * List of all firewall deny chains that are only applied to metered networks.
+     * See {@link #DENY_CHAINS} for deny chains that are applied to all networks regardless of
+     * meteredness.
+     */
+    public static final List<Integer> METERED_DENY_CHAINS = List.of(
+            FIREWALL_CHAIN_METERED_DENY_USER,
+            FIREWALL_CHAIN_METERED_DENY_ADMIN
+    );
     // LINT.ThenChange(../../../../bpf_progs/netd.h)
 }
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index 19ecafb..9cbf147 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -25,11 +25,14 @@
 import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
 import static android.net.BpfNetMapsConstants.LOW_POWER_STANDBY_MATCH;
 import static android.net.BpfNetMapsConstants.MATCH_LIST;
+import static android.net.BpfNetMapsConstants.METERED_ALLOW_CHAINS;
+import static android.net.BpfNetMapsConstants.METERED_DENY_CHAINS;
 import static android.net.BpfNetMapsConstants.NO_MATCH;
 import static android.net.BpfNetMapsConstants.OEM_DENY_1_MATCH;
 import static android.net.BpfNetMapsConstants.OEM_DENY_2_MATCH;
 import static android.net.BpfNetMapsConstants.OEM_DENY_3_MATCH;
-import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_ADMIN_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_USER_MATCH;
 import static android.net.BpfNetMapsConstants.POWERSAVE_MATCH;
 import static android.net.BpfNetMapsConstants.RESTRICTED_MATCH;
 import static android.net.BpfNetMapsConstants.STANDBY_MATCH;
@@ -37,6 +40,9 @@
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_ADMIN;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
@@ -117,6 +123,12 @@
                 return OEM_DENY_2_MATCH;
             case FIREWALL_CHAIN_OEM_DENY_3:
                 return OEM_DENY_3_MATCH;
+            case FIREWALL_CHAIN_METERED_ALLOW:
+                return HAPPY_BOX_MATCH;
+            case FIREWALL_CHAIN_METERED_DENY_USER:
+                return PENALTY_BOX_USER_MATCH;
+            case FIREWALL_CHAIN_METERED_DENY_ADMIN:
+                return PENALTY_BOX_ADMIN_MATCH;
             default:
                 throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
         }
@@ -129,9 +141,9 @@
      * DENYLIST means the firewall allows all by default, uids must be explicitly denied
      */
     public static boolean isFirewallAllowList(final int chain) {
-        if (ALLOW_CHAINS.contains(chain)) {
+        if (ALLOW_CHAINS.contains(chain) || METERED_ALLOW_CHAINS.contains(chain)) {
             return true;
-        } else if (DENY_CHAINS.contains(chain)) {
+        } else if (DENY_CHAINS.contains(chain) || METERED_DENY_CHAINS.contains(chain)) {
             return false;
         }
         throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
@@ -264,7 +276,7 @@
         }
 
         if (!isNetworkMetered) return false;
-        if ((uidMatch & PENALTY_BOX_MATCH) != 0) return true;
+        if ((uidMatch & (PENALTY_BOX_USER_MATCH | PENALTY_BOX_ADMIN_MATCH)) != 0) return true;
         if ((uidMatch & HAPPY_BOX_MATCH) != 0) return false;
         return getDataSaverEnabled(dataSaverEnabledMap);
     }
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index b1e636d..7823258 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -128,6 +128,8 @@
                 "com.android.net.flags.support_is_uid_networking_blocked";
         static final String BASIC_BACKGROUND_RESTRICTIONS_ENABLED =
                 "com.android.net.flags.basic_background_restrictions_enabled";
+        static final String METERED_NETWORK_FIREWALL_CHAINS =
+                "com.android.net.flags.metered_network_firewall_chains";
     }
 
     /**
@@ -1068,6 +1070,61 @@
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9;
 
+    /**
+     * Firewall chain for allow list on metered networks
+     *
+     * UIDs added to this chain have access to metered networks, unless they're also in one of the
+     * denylist, {@link #FIREWALL_CHAIN_METERED_DENY_USER},
+     * {@link #FIREWALL_CHAIN_METERED_DENY_ADMIN}
+     *
+     * Note that this chain is used from a separate bpf program that is triggered by iptables and
+     * can not be controlled by {@link ConnectivityManager#setFirewallChainEnabled}.
+     *
+     * @hide
+     */
+    // TODO: Merge this chain with data saver and support setFirewallChainEnabled
+    @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_CHAIN_METERED_ALLOW = 10;
+
+    /**
+     * Firewall chain for user-set restrictions on metered networks
+     *
+     * UIDs added to this chain do not have access to metered networks.
+     * UIDs should be added to this chain based on user settings.
+     * To restrict metered network based on admin configuration (e.g. enterprise policies),
+     * {@link #FIREWALL_CHAIN_METERED_DENY_ADMIN} should be used.
+     * This chain corresponds to {@link #BLOCKED_METERED_REASON_USER_RESTRICTED}
+     *
+     * Note that this chain is used from a separate bpf program that is triggered by iptables and
+     * can not be controlled by {@link ConnectivityManager#setFirewallChainEnabled}.
+     *
+     * @hide
+     */
+    // TODO: Support setFirewallChainEnabled to control this chain
+    @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_CHAIN_METERED_DENY_USER = 11;
+
+    /**
+     * Firewall chain for admin-set restrictions on metered networks
+     *
+     * UIDs added to this chain do not have access to metered networks.
+     * UIDs should be added to this chain based on admin configuration (e.g. enterprise policies).
+     * To restrict metered network based on user settings, {@link #FIREWALL_CHAIN_METERED_DENY_USER}
+     * should be used.
+     * This chain corresponds to {@link #BLOCKED_METERED_REASON_ADMIN_DISABLED}
+     *
+     * Note that this chain is used from a separate bpf program that is triggered by iptables and
+     * can not be controlled by {@link ConnectivityManager#setFirewallChainEnabled}.
+     *
+     * @hide
+     */
+    // TODO: Support setFirewallChainEnabled to control this chain
+    @FlaggedApi(Flags.METERED_NETWORK_FIREWALL_CHAINS)
+    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int FIREWALL_CHAIN_METERED_DENY_ADMIN = 12;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = false, prefix = "FIREWALL_CHAIN_", value = {
@@ -1079,7 +1136,10 @@
         FIREWALL_CHAIN_BACKGROUND,
         FIREWALL_CHAIN_OEM_DENY_1,
         FIREWALL_CHAIN_OEM_DENY_2,
-        FIREWALL_CHAIN_OEM_DENY_3
+        FIREWALL_CHAIN_OEM_DENY_3,
+        FIREWALL_CHAIN_METERED_ALLOW,
+        FIREWALL_CHAIN_METERED_DENY_USER,
+        FIREWALL_CHAIN_METERED_DENY_ADMIN
     })
     public @interface FirewallChain {}
 
@@ -6065,7 +6125,7 @@
     })
     public void addUidToMeteredNetworkAllowList(final int uid) {
         try {
-            mService.updateMeteredNetworkAllowList(uid, true /* add */);
+            mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_ALLOW, uid, FIREWALL_RULE_ALLOW);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -6088,7 +6148,7 @@
     })
     public void removeUidFromMeteredNetworkAllowList(final int uid) {
         try {
-            mService.updateMeteredNetworkAllowList(uid, false /* remove */);
+            mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_ALLOW, uid, FIREWALL_RULE_DENY);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -6098,10 +6158,17 @@
      * Adds the specified UID to the list of UIDs that are not allowed to use background data on
      * metered networks. Takes precedence over {@link #addUidToMeteredNetworkAllowList}.
      *
+     * On V+, {@link #setUidFirewallRule} should be used with
+     * {@link #FIREWALL_CHAIN_METERED_DENY_USER} or {@link #FIREWALL_CHAIN_METERED_DENY_ADMIN}
+     * based on the reason so that users can receive {@link #BLOCKED_METERED_REASON_USER_RESTRICTED}
+     * or {@link #BLOCKED_METERED_REASON_ADMIN_DISABLED}, respectively.
+     * This API always uses {@link #FIREWALL_CHAIN_METERED_DENY_USER}.
+     *
      * @param uid uid of target app
      * @throws IllegalStateException if updating deny list failed.
      * @hide
      */
+    // TODO(b/332649177): Deprecate this API after V
     @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(anyOf = {
             android.Manifest.permission.NETWORK_SETTINGS,
@@ -6110,7 +6177,7 @@
     })
     public void addUidToMeteredNetworkDenyList(final int uid) {
         try {
-            mService.updateMeteredNetworkDenyList(uid, true /* add */);
+            mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, uid, FIREWALL_RULE_DENY);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -6121,10 +6188,17 @@
      * networks if background data is not restricted. The deny list takes precedence over the
      * allow list.
      *
+     * On V+, {@link #setUidFirewallRule} should be used with
+     * {@link #FIREWALL_CHAIN_METERED_DENY_USER} or {@link #FIREWALL_CHAIN_METERED_DENY_ADMIN}
+     * based on the reason so that users can receive {@link #BLOCKED_METERED_REASON_USER_RESTRICTED}
+     * or {@link #BLOCKED_METERED_REASON_ADMIN_DISABLED}, respectively.
+     * This API always uses {@link #FIREWALL_CHAIN_METERED_DENY_USER}.
+     *
      * @param uid uid of target app
      * @throws IllegalStateException if updating deny list failed.
      * @hide
      */
+    // TODO(b/332649177): Deprecate this API after V
     @SystemApi(client = MODULE_LIBRARIES)
     @RequiresPermission(anyOf = {
             android.Manifest.permission.NETWORK_SETTINGS,
@@ -6133,7 +6207,7 @@
     })
     public void removeUidFromMeteredNetworkDenyList(final int uid) {
         try {
-            mService.updateMeteredNetworkDenyList(uid, false /* remove */);
+            mService.setUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, uid, FIREWALL_RULE_ALLOW);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -6191,6 +6265,10 @@
     /**
      * Enables or disables the specified firewall chain.
      *
+     * Note that metered firewall chains can not be controlled by this API.
+     * See {@link #FIREWALL_CHAIN_METERED_ALLOW}, {@link #FIREWALL_CHAIN_METERED_DENY_USER}, and
+     * {@link #FIREWALL_CHAIN_METERED_DENY_ADMIN} for more detail.
+     *
      * @param chain target chain.
      * @param enable whether the chain should be enabled.
      * @throws UnsupportedOperationException if called on pre-T devices.
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index d3a02b9..55c7085 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -242,10 +242,6 @@
 
     void setDataSaverEnabled(boolean enable);
 
-    void updateMeteredNetworkAllowList(int uid, boolean add);
-
-    void updateMeteredNetworkDenyList(int uid, boolean add);
-
     void setUidFirewallRule(int chain, int uid, int rule);
 
     int getUidFirewallRule(int chain, int uid);
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index fc6d8c4..01bd71e 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -23,11 +23,9 @@
 import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
 import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
 import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
-import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
 import static android.net.BpfNetMapsConstants.IIF_MATCH;
 import static android.net.BpfNetMapsConstants.INGRESS_DISCARD_MAP_PATH;
 import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
-import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
 import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
 import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH;
 import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
@@ -446,62 +444,6 @@
     }
 
     /**
-     * Add naughty app bandwidth rule for specific app
-     *
-     * @param uid uid of target app
-     * @throws ServiceSpecificException in case of failure, with an error code indicating the
-     *                                  cause of the failure.
-     */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public void addNaughtyApp(final int uid) {
-        throwIfPreT("addNaughtyApp is not available on pre-T devices");
-
-        addRule(uid, PENALTY_BOX_MATCH, "addNaughtyApp");
-    }
-
-    /**
-     * Remove naughty app bandwidth rule for specific app
-     *
-     * @param uid uid of target app
-     * @throws ServiceSpecificException in case of failure, with an error code indicating the
-     *                                  cause of the failure.
-     */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public void removeNaughtyApp(final int uid) {
-        throwIfPreT("removeNaughtyApp is not available on pre-T devices");
-
-        removeRule(uid, PENALTY_BOX_MATCH, "removeNaughtyApp");
-    }
-
-    /**
-     * Add nice app bandwidth rule for specific app
-     *
-     * @param uid uid of target app
-     * @throws ServiceSpecificException in case of failure, with an error code indicating the
-     *                                  cause of the failure.
-     */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public void addNiceApp(final int uid) {
-        throwIfPreT("addNiceApp is not available on pre-T devices");
-
-        addRule(uid, HAPPY_BOX_MATCH, "addNiceApp");
-    }
-
-    /**
-     * Remove nice app bandwidth rule for specific app
-     *
-     * @param uid uid of target app
-     * @throws ServiceSpecificException in case of failure, with an error code indicating the
-     *                                  cause of the failure.
-     */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public void removeNiceApp(final int uid) {
-        throwIfPreT("removeNiceApp is not available on pre-T devices");
-
-        removeRule(uid, HAPPY_BOX_MATCH, "removeNiceApp");
-    }
-
-    /**
      * Set target firewall child chain
      *
      * @param childChain target chain to enable
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index a15a2bf..0ea3366 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -24,6 +24,8 @@
 import static android.content.pm.PackageManager.FEATURE_WIFI;
 import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.BpfNetMapsConstants.METERED_ALLOW_CHAINS;
+import static android.net.BpfNetMapsConstants.METERED_DENY_CHAINS;
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_ATTEMPTED_BITMASK;
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_SUCCEEDED_BITMASK;
 import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT;
@@ -13474,36 +13476,6 @@
         }
     }
 
-    @Override
-    public void updateMeteredNetworkAllowList(final int uid, final boolean add) {
-        enforceNetworkStackOrSettingsPermission();
-
-        try {
-            if (add) {
-                mBpfNetMaps.addNiceApp(uid);
-            } else {
-                mBpfNetMaps.removeNiceApp(uid);
-            }
-        } catch (ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @Override
-    public void updateMeteredNetworkDenyList(final int uid, final boolean add) {
-        enforceNetworkStackOrSettingsPermission();
-
-        try {
-            if (add) {
-                mBpfNetMaps.addNaughtyApp(uid);
-            } else {
-                mBpfNetMaps.removeNaughtyApp(uid);
-            }
-        } catch (ServiceSpecificException e) {
-            throw new IllegalStateException(e);
-        }
-    }
-
     private int setPackageFirewallRule(final int chain, final String packageName, final int rule)
             throws PackageManager.NameNotFoundException {
         final PackageManager pm = mContext.getPackageManager();
@@ -13563,6 +13535,8 @@
             case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1:
             case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2:
             case ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3:
+            case ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER:
+            case ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_ADMIN:
                 defaultRule = FIREWALL_RULE_ALLOW;
                 break;
             case ConnectivityManager.FIREWALL_CHAIN_DOZABLE:
@@ -13570,6 +13544,7 @@
             case ConnectivityManager.FIREWALL_CHAIN_RESTRICTED:
             case ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY:
             case ConnectivityManager.FIREWALL_CHAIN_BACKGROUND:
+            case ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW:
                 defaultRule = FIREWALL_RULE_DENY;
                 break;
             default:
@@ -13606,6 +13581,12 @@
                     + " the feature is disabled.");
             return;
         }
+        if (METERED_ALLOW_CHAINS.contains(chain) || METERED_DENY_CHAINS.contains(chain)) {
+            // Metered chains are used from a separate bpf program that is triggered by iptables
+            // and can not be controlled by setFirewallChainEnabled.
+            throw new UnsupportedOperationException(
+                    "Chain (" + chain + ") can not be controlled by setFirewallChainEnabled");
+        }
 
         try {
             mBpfNetMaps.setChildChain(chain, enable);
@@ -13626,6 +13607,13 @@
     public boolean getFirewallChainEnabled(final int chain) {
         enforceNetworkStackOrSettingsPermission();
 
+        if (METERED_ALLOW_CHAINS.contains(chain) || METERED_DENY_CHAINS.contains(chain)) {
+            // Metered chains are used from a separate bpf program that is triggered by iptables
+            // and can not be controlled by setFirewallChainEnabled.
+            throw new UnsupportedOperationException(
+                    "getFirewallChainEnabled can not return status of chain (" + chain + ")");
+        }
+
         return mBpfNetMaps.isChainEnabled(chain);
     }
 
diff --git a/tests/unit/java/android/net/NetworkStackBpfNetMapsTest.kt b/tests/unit/java/android/net/NetworkStackBpfNetMapsTest.kt
index a9ccbdd..b5d78f3 100644
--- a/tests/unit/java/android/net/NetworkStackBpfNetMapsTest.kt
+++ b/tests/unit/java/android/net/NetworkStackBpfNetMapsTest.kt
@@ -21,7 +21,8 @@
 import android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY
 import android.net.BpfNetMapsConstants.DOZABLE_MATCH
 import android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH
-import android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH
+import android.net.BpfNetMapsConstants.PENALTY_BOX_ADMIN_MATCH
+import android.net.BpfNetMapsConstants.PENALTY_BOX_USER_MATCH
 import android.net.BpfNetMapsConstants.STANDBY_MATCH
 import android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY
 import android.net.BpfNetMapsUtils.getMatchByFirewallChain
@@ -48,9 +49,9 @@
 private const val TEST_UID3 = TEST_UID2 + 1
 private const val NO_IIF = 0
 
-// pre-T devices does not support Bpf.
+// NetworkStack can not use this before U due to b/326143935
 @RunWith(DevSdkIgnoreRunner::class)
-@IgnoreUpTo(VERSION_CODES.S_V2)
+@IgnoreUpTo(VERSION_CODES.TIRAMISU)
 class NetworkStackBpfNetMapsTest {
     @Rule
     @JvmField
@@ -102,14 +103,18 @@
         }
         // Verify the size matches, this also verifies no common item in allow and deny chains.
         assertEquals(
-            BpfNetMapsConstants.ALLOW_CHAINS.size +
-                BpfNetMapsConstants.DENY_CHAINS.size,
+                BpfNetMapsConstants.ALLOW_CHAINS.size +
+                        BpfNetMapsConstants.DENY_CHAINS.size +
+                        BpfNetMapsConstants.METERED_ALLOW_CHAINS.size +
+                        BpfNetMapsConstants.METERED_DENY_CHAINS.size,
             declaredChains.size
         )
         declaredChains.forEach {
             assertTrue(
-                BpfNetMapsConstants.ALLOW_CHAINS.contains(it.get(null)) ||
-                    BpfNetMapsConstants.DENY_CHAINS.contains(it.get(null))
+                    BpfNetMapsConstants.ALLOW_CHAINS.contains(it.get(null)) ||
+                            BpfNetMapsConstants.METERED_ALLOW_CHAINS.contains(it.get(null)) ||
+                            BpfNetMapsConstants.DENY_CHAINS.contains(it.get(null)) ||
+                            BpfNetMapsConstants.METERED_DENY_CHAINS.contains(it.get(null))
             )
         }
     }
@@ -190,7 +195,16 @@
 
         // Add uid1 to penalty box, verify the network is blocked for uid1, while uid2 is not
         // affected.
-        testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, PENALTY_BOX_MATCH))
+        testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, PENALTY_BOX_USER_MATCH))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID2, metered = true))
+        testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, PENALTY_BOX_ADMIN_MATCH))
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID2, metered = true))
+        testUidOwnerMap.updateEntry(
+                S32(TEST_UID1),
+                UidOwnerValue(NO_IIF, PENALTY_BOX_USER_MATCH or PENALTY_BOX_ADMIN_MATCH)
+        )
         assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
         assertFalse(isUidNetworkingBlocked(TEST_UID2, metered = true))
 
@@ -206,7 +220,14 @@
         // priority.
         testUidOwnerMap.updateEntry(
             S32(TEST_UID1),
-            UidOwnerValue(NO_IIF, PENALTY_BOX_MATCH or HAPPY_BOX_MATCH)
+            UidOwnerValue(NO_IIF, PENALTY_BOX_USER_MATCH or HAPPY_BOX_MATCH)
+        )
+        assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+        assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true))
+        assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true))
+        testUidOwnerMap.updateEntry(
+                S32(TEST_UID1),
+                UidOwnerValue(NO_IIF, PENALTY_BOX_ADMIN_MATCH or HAPPY_BOX_MATCH)
         )
         assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
         assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true))
@@ -240,7 +261,7 @@
         for (uid in FIRST_APPLICATION_UID - 5..FIRST_APPLICATION_UID + 5) {
             // system uid is not blocked regardless of firewall chains
             val expectBlocked = uid >= FIRST_APPLICATION_UID
-            testUidOwnerMap.updateEntry(S32(uid), UidOwnerValue(NO_IIF, PENALTY_BOX_MATCH))
+            testUidOwnerMap.updateEntry(S32(uid), UidOwnerValue(NO_IIF, PENALTY_BOX_USER_MATCH))
             assertEquals(
                 expectBlocked,
                     isUidNetworkingBlocked(uid, metered = true),
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index ea905d5..fa79795 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -31,13 +31,17 @@
 import static android.net.BpfNetMapsConstants.OEM_DENY_1_MATCH;
 import static android.net.BpfNetMapsConstants.OEM_DENY_2_MATCH;
 import static android.net.BpfNetMapsConstants.OEM_DENY_3_MATCH;
-import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_ADMIN_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_USER_MATCH;
 import static android.net.BpfNetMapsConstants.POWERSAVE_MATCH;
 import static android.net.BpfNetMapsConstants.RESTRICTED_MATCH;
 import static android.net.BpfNetMapsConstants.STANDBY_MATCH;
 import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_ADMIN;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
@@ -334,146 +338,6 @@
         }
     }
 
-    private void doTestRemoveNaughtyApp(final int iif, final long match) throws Exception {
-        mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(iif, match));
-
-        mBpfNetMaps.removeNaughtyApp(TEST_UID);
-
-        checkUidOwnerValue(TEST_UID, iif, match & ~PENALTY_BOX_MATCH);
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testRemoveNaughtyApp() throws Exception {
-        doTestRemoveNaughtyApp(NO_IIF, PENALTY_BOX_MATCH);
-
-        // PENALTY_BOX_MATCH with other matches
-        doTestRemoveNaughtyApp(NO_IIF, PENALTY_BOX_MATCH | DOZABLE_MATCH | POWERSAVE_MATCH);
-
-        // PENALTY_BOX_MATCH with IIF_MATCH
-        doTestRemoveNaughtyApp(TEST_IF_INDEX, PENALTY_BOX_MATCH | IIF_MATCH);
-
-        // PENALTY_BOX_MATCH is not enabled
-        doTestRemoveNaughtyApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testRemoveNaughtyAppMissingUid() {
-        // UidOwnerMap does not have entry for TEST_UID
-        assertThrows(ServiceSpecificException.class,
-                () -> mBpfNetMaps.removeNaughtyApp(TEST_UID));
-    }
-
-    @Test
-    @IgnoreAfter(Build.VERSION_CODES.S_V2)
-    public void testRemoveNaughtyAppBeforeT() {
-        assertThrows(UnsupportedOperationException.class,
-                () -> mBpfNetMaps.removeNaughtyApp(TEST_UID));
-    }
-
-    private void doTestAddNaughtyApp(final int iif, final long match) throws Exception {
-        if (match != NO_MATCH) {
-            mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(iif, match));
-        }
-
-        mBpfNetMaps.addNaughtyApp(TEST_UID);
-
-        checkUidOwnerValue(TEST_UID, iif, match | PENALTY_BOX_MATCH);
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testAddNaughtyApp() throws Exception {
-        doTestAddNaughtyApp(NO_IIF, NO_MATCH);
-
-        // Other matches are enabled
-        doTestAddNaughtyApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
-
-        // IIF_MATCH is enabled
-        doTestAddNaughtyApp(TEST_IF_INDEX, IIF_MATCH);
-
-        // PENALTY_BOX_MATCH is already enabled
-        doTestAddNaughtyApp(NO_IIF, PENALTY_BOX_MATCH | DOZABLE_MATCH);
-    }
-
-    @Test
-    @IgnoreAfter(Build.VERSION_CODES.S_V2)
-    public void testAddNaughtyAppBeforeT() {
-        assertThrows(UnsupportedOperationException.class,
-                () -> mBpfNetMaps.addNaughtyApp(TEST_UID));
-    }
-
-    private void doTestRemoveNiceApp(final int iif, final long match) throws Exception {
-        mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(iif, match));
-
-        mBpfNetMaps.removeNiceApp(TEST_UID);
-
-        checkUidOwnerValue(TEST_UID, iif, match & ~HAPPY_BOX_MATCH);
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testRemoveNiceApp() throws Exception {
-        doTestRemoveNiceApp(NO_IIF, HAPPY_BOX_MATCH);
-
-        // HAPPY_BOX_MATCH with other matches
-        doTestRemoveNiceApp(NO_IIF, HAPPY_BOX_MATCH | DOZABLE_MATCH | POWERSAVE_MATCH);
-
-        // HAPPY_BOX_MATCH with IIF_MATCH
-        doTestRemoveNiceApp(TEST_IF_INDEX, HAPPY_BOX_MATCH | IIF_MATCH);
-
-        // HAPPY_BOX_MATCH is not enabled
-        doTestRemoveNiceApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testRemoveNiceAppMissingUid() {
-        // UidOwnerMap does not have entry for TEST_UID
-        assertThrows(ServiceSpecificException.class,
-                () -> mBpfNetMaps.removeNiceApp(TEST_UID));
-    }
-
-    @Test
-    @IgnoreAfter(Build.VERSION_CODES.S_V2)
-    public void testRemoveNiceAppBeforeT() {
-        assertThrows(UnsupportedOperationException.class,
-                () -> mBpfNetMaps.removeNiceApp(TEST_UID));
-    }
-
-    private void doTestAddNiceApp(final int iif, final long match) throws Exception {
-        if (match != NO_MATCH) {
-            mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(iif, match));
-        }
-
-        mBpfNetMaps.addNiceApp(TEST_UID);
-
-        checkUidOwnerValue(TEST_UID, iif, match | HAPPY_BOX_MATCH);
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
-    public void testAddNiceApp() throws Exception {
-        doTestAddNiceApp(NO_IIF, NO_MATCH);
-
-        // Other matches are enabled
-        doTestAddNiceApp(NO_IIF, DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH);
-
-        // IIF_MATCH is enabled
-        doTestAddNiceApp(TEST_IF_INDEX, IIF_MATCH);
-
-        // HAPPY_BOX_MATCH is already enabled
-        doTestAddNiceApp(NO_IIF, HAPPY_BOX_MATCH | DOZABLE_MATCH);
-    }
-
-    @Test
-    @IgnoreAfter(Build.VERSION_CODES.S_V2)
-    public void testAddNiceAppBeforeT() {
-        assertThrows(UnsupportedOperationException.class,
-                () -> mBpfNetMaps.addNiceApp(TEST_UID));
-    }
-
     private void doTestUpdateUidLockdownRule(final int iif, final long match, final boolean add)
             throws Exception {
         if (match != NO_MATCH) {
@@ -658,6 +522,9 @@
         doTestSetUidRule(FIREWALL_CHAIN_OEM_DENY_1);
         doTestSetUidRule(FIREWALL_CHAIN_OEM_DENY_2);
         doTestSetUidRule(FIREWALL_CHAIN_OEM_DENY_3);
+        doTestSetUidRule(FIREWALL_CHAIN_METERED_ALLOW);
+        doTestSetUidRule(FIREWALL_CHAIN_METERED_DENY_USER);
+        doTestSetUidRule(FIREWALL_CHAIN_METERED_DENY_ADMIN);
     }
 
     @Test
@@ -1079,7 +946,7 @@
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testDumpUidOwnerMap() throws Exception {
         doTestDumpUidOwnerMap(HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH");
-        doTestDumpUidOwnerMap(PENALTY_BOX_MATCH, "PENALTY_BOX_MATCH");
+        doTestDumpUidOwnerMap(PENALTY_BOX_USER_MATCH, "PENALTY_BOX_USER_MATCH");
         doTestDumpUidOwnerMap(DOZABLE_MATCH, "DOZABLE_MATCH");
         doTestDumpUidOwnerMap(STANDBY_MATCH, "STANDBY_MATCH");
         doTestDumpUidOwnerMap(POWERSAVE_MATCH, "POWERSAVE_MATCH");
@@ -1089,6 +956,7 @@
         doTestDumpUidOwnerMap(OEM_DENY_1_MATCH, "OEM_DENY_1_MATCH");
         doTestDumpUidOwnerMap(OEM_DENY_2_MATCH, "OEM_DENY_2_MATCH");
         doTestDumpUidOwnerMap(OEM_DENY_3_MATCH, "OEM_DENY_3_MATCH");
+        doTestDumpUidOwnerMap(PENALTY_BOX_ADMIN_MATCH, "PENALTY_BOX_ADMIN_MATCH");
 
         doTestDumpUidOwnerMap(HAPPY_BOX_MATCH | POWERSAVE_MATCH,
                 "HAPPY_BOX_MATCH POWERSAVE_MATCH");
@@ -1137,7 +1005,6 @@
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testDumpUidOwnerMapConfig() throws Exception {
         doTestDumpOwnerMatchConfig(HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH");
-        doTestDumpOwnerMatchConfig(PENALTY_BOX_MATCH, "PENALTY_BOX_MATCH");
         doTestDumpOwnerMatchConfig(DOZABLE_MATCH, "DOZABLE_MATCH");
         doTestDumpOwnerMatchConfig(STANDBY_MATCH, "STANDBY_MATCH");
         doTestDumpOwnerMatchConfig(POWERSAVE_MATCH, "POWERSAVE_MATCH");
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index ce49533..59e7d22 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -63,6 +63,9 @@
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_ADMIN;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
@@ -10497,6 +10500,9 @@
         doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_1, FIREWALL_RULE_ALLOW);
         doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_2, FIREWALL_RULE_ALLOW);
         doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_3, FIREWALL_RULE_ALLOW);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_METERED_ALLOW, FIREWALL_RULE_DENY);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_USER, FIREWALL_RULE_ALLOW);
+        doTestSetUidFirewallRule(FIREWALL_CHAIN_METERED_DENY_ADMIN, FIREWALL_RULE_ALLOW);
     }
 
     @Test @IgnoreUpTo(SC_V2)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSFirewallChainTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSFirewallChainTest.kt
index 16de4da..83ccccd 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSFirewallChainTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSFirewallChainTest.kt
@@ -16,7 +16,14 @@
 
 package com.android.server
 
-import android.net.ConnectivityManager
+import android.net.BpfNetMapsConstants.METERED_ALLOW_CHAINS
+import android.net.BpfNetMapsConstants.METERED_DENY_CHAINS
+import android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND
+import android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_ALLOW
+import android.net.ConnectivityManager.FIREWALL_CHAIN_METERED_DENY_USER
+import android.net.ConnectivityManager.FIREWALL_RULE_ALLOW
+import android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT
+import android.net.ConnectivityManager.FIREWALL_RULE_DENY
 import android.os.Build
 import androidx.test.filters.SmallTest
 import com.android.server.connectivity.ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN
@@ -24,6 +31,7 @@
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertThrows
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -52,13 +60,13 @@
     @FeatureFlags(flags = [Flag(BACKGROUND_FIREWALL_CHAIN, true)])
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     fun setFirewallChainEnabled_backgroundChainEnabled_afterU() {
-        cm.setFirewallChainEnabled(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, true)
-        verify(bpfNetMaps).setChildChain(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, true)
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true)
+        verify(bpfNetMaps).setChildChain(FIREWALL_CHAIN_BACKGROUND, true)
 
         clearInvocations(bpfNetMaps)
 
-        cm.setFirewallChainEnabled(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, false)
-        verify(bpfNetMaps).setChildChain(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, false)
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, false)
+        verify(bpfNetMaps).setChildChain(FIREWALL_CHAIN_BACKGROUND, false)
     }
 
     @Test
@@ -69,10 +77,10 @@
     }
 
     private fun verifySetFirewallChainEnabledOnBackgroundDoesNothing() {
-        cm.setFirewallChainEnabled(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, true)
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true)
         verify(bpfNetMaps, never()).setChildChain(anyInt(), anyBoolean())
 
-        cm.setFirewallChainEnabled(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, false)
+        cm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, false)
         verify(bpfNetMaps, never()).setChildChain(anyInt(), anyBoolean())
     }
 
@@ -88,8 +96,8 @@
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     fun replaceFirewallChain_backgroundChainEnabled_afterU() {
         val uids = intArrayOf(53, 42, 79)
-        cm.replaceFirewallChain(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uids)
-        verify(bpfNetMaps).replaceUidChain(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uids)
+        cm.replaceFirewallChain(FIREWALL_CHAIN_BACKGROUND, uids)
+        verify(bpfNetMaps).replaceUidChain(FIREWALL_CHAIN_BACKGROUND, uids)
     }
 
     @Test
@@ -101,7 +109,7 @@
 
     private fun verifyReplaceFirewallChainOnBackgroundDoesNothing() {
         val uids = intArrayOf(53, 42, 79)
-        cm.replaceFirewallChain(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uids)
+        cm.replaceFirewallChain(FIREWALL_CHAIN_BACKGROUND, uids)
         verify(bpfNetMaps, never()).replaceUidChain(anyInt(), any(IntArray::class.java))
     }
 
@@ -118,24 +126,18 @@
     fun setUidFirewallRule_backgroundChainEnabled_afterU() {
         val uid = 2345
 
-        cm.setUidFirewallRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid,
-            ConnectivityManager.FIREWALL_RULE_DEFAULT)
-        verify(bpfNetMaps).setUidRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid,
-            ConnectivityManager.FIREWALL_RULE_DENY)
+        cm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_DEFAULT)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_DENY)
 
         clearInvocations(bpfNetMaps)
 
-        cm.setUidFirewallRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid,
-            ConnectivityManager.FIREWALL_RULE_DENY)
-        verify(bpfNetMaps).setUidRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid,
-            ConnectivityManager.FIREWALL_RULE_DENY)
+        cm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_DENY)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_DENY)
 
         clearInvocations(bpfNetMaps)
 
-        cm.setUidFirewallRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid,
-            ConnectivityManager.FIREWALL_RULE_ALLOW)
-        verify(bpfNetMaps).setUidRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid,
-            ConnectivityManager.FIREWALL_RULE_ALLOW)
+        cm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_ALLOW)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_ALLOW)
     }
 
     @Test
@@ -148,10 +150,49 @@
     private fun verifySetUidFirewallRuleOnBackgroundDoesNothing() {
         val uid = 2345
 
-        listOf(ConnectivityManager.FIREWALL_RULE_DEFAULT, ConnectivityManager.FIREWALL_RULE_ALLOW,
-            ConnectivityManager.FIREWALL_RULE_DENY).forEach { rule ->
-            cm.setUidFirewallRule(ConnectivityManager.FIREWALL_CHAIN_BACKGROUND, uid, rule)
+        listOf(FIREWALL_RULE_DEFAULT, FIREWALL_RULE_ALLOW, FIREWALL_RULE_DENY).forEach { rule ->
+            cm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, uid, rule)
             verify(bpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt())
         }
     }
+
+    @Test
+    fun testSetFirewallChainEnabled_meteredChain() {
+        (METERED_ALLOW_CHAINS + METERED_DENY_CHAINS).forEach {
+            assertThrows(UnsupportedOperationException::class.java) {
+                cm.setFirewallChainEnabled(it, true)
+            }
+            assertThrows(UnsupportedOperationException::class.java) {
+                cm.setFirewallChainEnabled(it, false)
+            }
+        }
+    }
+
+    @Test
+    fun testAddUidToMeteredNetworkAllowList() {
+        val uid = 1001
+        cm.addUidToMeteredNetworkAllowList(uid)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_METERED_ALLOW, uid, FIREWALL_RULE_ALLOW)
+    }
+
+    @Test
+    fun testRemoveUidFromMeteredNetworkAllowList() {
+        val uid = 1001
+        cm.removeUidFromMeteredNetworkAllowList(uid)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_METERED_ALLOW, uid, FIREWALL_RULE_DENY)
+    }
+
+    @Test
+    fun testAddUidToMeteredNetworkDenyList() {
+        val uid = 1001
+        cm.addUidToMeteredNetworkDenyList(uid)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_METERED_DENY_USER, uid, FIREWALL_RULE_DENY)
+    }
+
+    @Test
+    fun testRemoveUidFromMeteredNetworkDenyList() {
+        val uid = 1001
+        cm.removeUidFromMeteredNetworkDenyList(uid)
+        verify(bpfNetMaps).setUidRule(FIREWALL_CHAIN_METERED_DENY_USER, uid, FIREWALL_RULE_ALLOW)
+    }
 }