Use java BpfMap in BpfNetMaps#setChildChain

Bug: 217624062
Test: atest BpfNetMapsTest android.net.cts.ConnectivityManagerTest#testFirewallBlocking

Change-Id: I13e96911eccd7d1d0545a156ddc2859bcaac09eb
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 9ae2c12..02083ff 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -5903,6 +5903,7 @@
      *
      * @param chain target chain.
      * @param enable whether the chain should be enabled.
+     * @throws UnsupportedOperationException if called on pre-T devices.
      * @throws IllegalStateException if enabling or disabling the firewall chain failed.
      * @hide
      */
@@ -5926,7 +5927,6 @@
      * @param chain target chain.
      * @return {@code true} if chain is enabled, {@code false} if chain is disabled.
      * @throws UnsupportedOperationException if called on pre-T devices.
-     * @throws IllegalArgumentException if {@code chain} is a invalid value.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      * @hide
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 7af50f9..4073359 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -24,6 +24,7 @@
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+import static android.system.OsConstants.EINVAL;
 import static android.system.OsConstants.ENOENT;
 import static android.system.OsConstants.EOPNOTSUPP;
 
@@ -55,6 +56,11 @@
     private static final boolean USE_NETD = !SdkLevel.isAtLeastT();
     private static boolean sInitialized = false;
 
+    // Lock for sConfigurationMap entry for UID_RULES_CONFIGURATION_KEY.
+    // This entry is not accessed by others.
+    // BpfNetMaps acquires this lock while sequence of read, modify, and write.
+    private static final Object sUidRulesConfigBpfMapLock = new Object();
+
     private static final String CONFIGURATION_MAP_PATH =
             "/sys/fs/bpf/netd_shared/map_netd_configuration_map";
     private static final U32 UID_RULES_CONFIGURATION_KEY = new U32(0);
@@ -152,7 +158,7 @@
     public long getMatchByFirewallChain(final int chain) {
         final long match = FIREWALL_CHAIN_TO_MATCH.get(chain, NO_MATCH);
         if (match == NO_MATCH) {
-            throw new IllegalArgumentException("Invalid firewall chain: " + chain);
+            throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
         }
         return match;
     }
@@ -163,6 +169,12 @@
         }
     }
 
+    private void throwIfUseNetd(final String msg) {
+        if (USE_NETD) {
+            throw new UnsupportedOperationException(msg);
+        }
+    }
+
     /**
      * Add naughty app bandwidth rule for specific app
      *
@@ -216,12 +228,29 @@
      *
      * @param childChain target chain to enable
      * @param enable     whether to enable or disable child chain.
+     * @throws UnsupportedOperationException if called on pre-T devices.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
     public void setChildChain(final int childChain, final boolean enable) {
-        final int err = native_setChildChain(childChain, enable);
-        maybeThrow(err, "Unable to set child chain");
+        throwIfUseNetd("setChildChain is not available on pre-T devices");
+
+        final long match = getMatchByFirewallChain(childChain);
+        try {
+            synchronized (sUidRulesConfigBpfMapLock) {
+                final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+                if (config == null) {
+                    throw new ServiceSpecificException(ENOENT,
+                            "Unable to get firewall chain status: sConfigurationMap does not have"
+                                    + " entry for UID_RULES_CONFIGURATION_KEY");
+                }
+                final long newConfig = enable ? (config.val | match) : (config.val & (~match));
+                sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(newConfig));
+            }
+        } catch (ErrnoException e) {
+            throw new ServiceSpecificException(e.errno,
+                    "Unable to set child chain: " + Os.strerror(e.errno));
+        }
     }
 
     /**
@@ -230,15 +259,11 @@
      * @param childChain target chain
      * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
      * @throws UnsupportedOperationException if called on pre-T devices.
-     * @throws IllegalArgumentException if {@code childChain} is a invalid value.
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
     public boolean getChainEnabled(final int childChain) {
-        if (USE_NETD) {
-            throw new UnsupportedOperationException("getChainEnabled is not available on pre-T"
-                    + " devices");
-        }
+        throwIfUseNetd("getChainEnabled is not available on pre-T devices");
 
         final long match = getMatchByFirewallChain(childChain);
         try {
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 9d63762..99e7ecc 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -26,6 +26,7 @@
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
 import static android.net.INetd.PERMISSION_INTERNET;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -168,7 +169,7 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testGetChainEnabledInvalidChain() {
-        final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
+        final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
         assertThrows(expected, () -> mBpfNetMaps.getChainEnabled(-1 /* childChain */));
         assertThrows(expected, () -> mBpfNetMaps.getChainEnabled(1000 /* childChain */));
     }
@@ -187,4 +188,81 @@
         assertThrows(UnsupportedOperationException.class,
                 () -> mBpfNetMaps.getChainEnabled(FIREWALL_CHAIN_DOZABLE));
     }
+
+    private void doTestSetChildChain(final List<Integer> testChains) throws Exception {
+        long expectedMatch = 0;
+        for (final int chain: testChains) {
+            expectedMatch |= mBpfNetMaps.getMatchByFirewallChain(chain);
+        }
+
+        assertEquals(0, sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+
+        for (final int chain: testChains) {
+            mBpfNetMaps.setChildChain(chain, true /* enable */);
+        }
+        assertEquals(expectedMatch, sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+
+        for (final int chain: testChains) {
+            mBpfNetMaps.setChildChain(chain, false /* enable */);
+        }
+        assertEquals(0, sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val);
+    }
+
+    private void doTestSetChildChain(final int testChain) throws Exception {
+        doTestSetChildChain(List.of(testChain));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetChildChain() throws Exception {
+        sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
+        doTestSetChildChain(FIREWALL_CHAIN_DOZABLE);
+        doTestSetChildChain(FIREWALL_CHAIN_STANDBY);
+        doTestSetChildChain(FIREWALL_CHAIN_POWERSAVE);
+        doTestSetChildChain(FIREWALL_CHAIN_RESTRICTED);
+        doTestSetChildChain(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+        doTestSetChildChain(FIREWALL_CHAIN_OEM_DENY_1);
+        doTestSetChildChain(FIREWALL_CHAIN_OEM_DENY_2);
+        doTestSetChildChain(FIREWALL_CHAIN_OEM_DENY_3);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetChildChainMultipleChain() throws Exception {
+        sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
+        doTestSetChildChain(List.of(
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_STANDBY));
+        doTestSetChildChain(List.of(
+                FIREWALL_CHAIN_DOZABLE,
+                FIREWALL_CHAIN_STANDBY,
+                FIREWALL_CHAIN_POWERSAVE,
+                FIREWALL_CHAIN_RESTRICTED));
+        doTestSetChildChain(FIREWALL_CHAINS);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetChildChainInvalidChain() {
+        final Class<ServiceSpecificException> expected = ServiceSpecificException.class;
+        assertThrows(expected,
+                () -> mBpfNetMaps.setChildChain(-1 /* childChain */, true /* enable */));
+        assertThrows(expected,
+                () -> mBpfNetMaps.setChildChain(1000 /* childChain */, true /* enable */));
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testSetChildChainMissingConfiguration() {
+        // sConfigurationMap does not have entry for UID_RULES_CONFIGURATION_KEY
+        assertThrows(ServiceSpecificException.class,
+                () -> mBpfNetMaps.setChildChain(FIREWALL_CHAIN_DOZABLE, true /* enable */));
+    }
+
+    @Test
+    @IgnoreAfter(Build.VERSION_CODES.S_V2)
+    public void testSetChildChainBeforeT() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mBpfNetMaps.setChildChain(FIREWALL_CHAIN_DOZABLE, true /* enable */));
+    }
 }