From be3ff1e92310352c9f665a67f8568659dfeb7912 Mon Sep 17 00:00:00 2001 From: Motomu Utsumi Date: Wed, 8 Jun 2022 10:05:07 +0000 Subject: [PATCH] Add API to get current firewall chain status This commit adds ConnectivityManager#getFirewallChainEnabled to read the current firewall chain status Bug: 208371987 Test: m Change-Id: I1eadb69f953af5d031cd8dabde3e1f098cf0f4df --- bpf_progs/bpf_shared.h | 2 + .../src/android/net/ConnectivityManager.java | 24 ++++ .../src/android/net/IConnectivityManager.aidl | 2 + .../src/com/android/server/BpfNetMaps.java | 124 +++++++++++++++++- .../android/server/ConnectivityService.java | 7 + .../com/android/server/BpfNetMapsTest.java | 124 +++++++++++++++++- 6 files changed, 281 insertions(+), 2 deletions(-) diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h index fd449a3de4..85b9f86e3e 100644 --- a/bpf_progs/bpf_shared.h +++ b/bpf_progs/bpf_shared.h @@ -148,6 +148,7 @@ ASSERT_STRING_EQUAL(XT_BPF_DENYLIST_PROG_PATH, BPF_NETD_PATH "prog_netd_skfilte #endif // __cplusplus +// LINT.IfChange(match_type) enum UidOwnerMatchType { NO_MATCH = 0, HAPPY_BOX_MATCH = (1 << 0), @@ -163,6 +164,7 @@ enum UidOwnerMatchType { OEM_DENY_2_MATCH = (1 << 10), OEM_DENY_3_MATCH = (1 << 11), }; +// LINT.ThenChange(packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java) enum BpfPermissionMatch { BPF_PERMISSION_INTERNET = 1 << 2, diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java index 39cd7f34f6..9ae2c12dfb 100644 --- a/framework/src/android/net/ConnectivityManager.java +++ b/framework/src/android/net/ConnectivityManager.java @@ -5920,6 +5920,30 @@ public class ConnectivityManager { } } + /** + * Get the specified firewall chain status. + * + * @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 + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK + }) + public boolean getFirewallChainEnabled(@FirewallChain final int chain) { + try { + return mService.getFirewallChainEnabled(chain); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Replaces the contents of the specified UID-based firewall chain. * diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl index bc73769f55..29fea00ffa 100644 --- a/framework/src/android/net/IConnectivityManager.aidl +++ b/framework/src/android/net/IConnectivityManager.aidl @@ -244,5 +244,7 @@ interface IConnectivityManager void setFirewallChainEnabled(int chain, boolean enable); + boolean getFirewallChainEnabled(int chain); + void replaceFirewallChain(int chain, in int[] uids); } diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java index 151d0e3da4..7af50f9f8b 100644 --- a/service/src/com/android/server/BpfNetMaps.java +++ b/service/src/com/android/server/BpfNetMaps.java @@ -16,15 +16,29 @@ package com.android.server; +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_OEM_DENY_1; +import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2; +import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3; +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.ENOENT; import static android.system.OsConstants.EOPNOTSUPP; import android.net.INetd; import android.os.RemoteException; import android.os.ServiceSpecificException; +import android.system.ErrnoException; import android.system.Os; import android.util.Log; +import android.util.SparseLongArray; +import com.android.internal.annotations.VisibleForTesting; import com.android.modules.utils.build.SdkLevel; +import com.android.net.module.util.BpfMap; +import com.android.net.module.util.Struct.U32; import java.io.FileDescriptor; import java.io.IOException; @@ -41,6 +55,51 @@ public class BpfNetMaps { private static final boolean USE_NETD = !SdkLevel.isAtLeastT(); private static boolean sInitialized = false; + 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); + private static BpfMap sConfigurationMap = null; + + // LINT.IfChange(match_type) + private static final long NO_MATCH = 0; + private static final long HAPPY_BOX_MATCH = (1 << 0); + private static final long PENALTY_BOX_MATCH = (1 << 1); + private static final long DOZABLE_MATCH = (1 << 2); + private static final long STANDBY_MATCH = (1 << 3); + private static final long POWERSAVE_MATCH = (1 << 4); + private static final long RESTRICTED_MATCH = (1 << 5); + private static final long LOW_POWER_STANDBY_MATCH = (1 << 6); + private static final long IIF_MATCH = (1 << 7); + private static final long LOCKDOWN_VPN_MATCH = (1 << 8); + private static final long OEM_DENY_1_MATCH = (1 << 9); + private static final long OEM_DENY_2_MATCH = (1 << 10); + private static final long OEM_DENY_3_MATCH = (1 << 11); + // LINT.ThenChange(packages/modules/Connectivity/bpf_progs/bpf_shared.h) + + // TODO: Use Java BpfMap instead of JNI code (TrafficController) for map update. + // Currently, BpfNetMaps uses TrafficController for map update and TrafficController + // (changeUidOwnerRule and toggleUidOwnerMap) also does conversion from "firewall chain" to + // "match". Migrating map update from JNI to Java BpfMap will solve this duplication. + private static final SparseLongArray FIREWALL_CHAIN_TO_MATCH = new SparseLongArray(); + static { + FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_DOZABLE, DOZABLE_MATCH); + FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_STANDBY, STANDBY_MATCH); + FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_POWERSAVE, POWERSAVE_MATCH); + FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_RESTRICTED, RESTRICTED_MATCH); + FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH); + FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_1, OEM_DENY_1_MATCH); + FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_2, OEM_DENY_2_MATCH); + FIREWALL_CHAIN_TO_MATCH.put(FIREWALL_CHAIN_OEM_DENY_3, OEM_DENY_3_MATCH); + } + + /** + * Only tests or BpfNetMaps#ensureInitialized can call this function. + */ + @VisibleForTesting + public static void initialize(final Dependencies deps) { + sConfigurationMap = deps.getConfigurationMap(); + } + /** * Initializes the class if it is not already initialized. This method will open maps but not * cause any other effects. This method may be called multiple times on any thread. @@ -50,10 +109,30 @@ public class BpfNetMaps { if (!USE_NETD) { System.loadLibrary("service-connectivity"); native_init(); + initialize(new Dependencies()); } sInitialized = true; } + /** + * Dependencies of BpfNetMaps, for injection in tests. + */ + @VisibleForTesting + public static class Dependencies { + /** + * Get configuration BPF map. + */ + public BpfMap getConfigurationMap() { + try { + return new BpfMap<>( + CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDWR, U32.class, U32.class); + } catch (ErrnoException e) { + Log.e(TAG, "Cannot open netd configuration map: " + e); + return null; + } + } + } + /** Constructor used after T that doesn't need to use netd anymore. */ public BpfNetMaps() { this(null); @@ -61,11 +140,23 @@ public class BpfNetMaps { if (USE_NETD) throw new IllegalArgumentException("BpfNetMaps need to use netd before T"); } - public BpfNetMaps(INetd netd) { + public BpfNetMaps(final INetd netd) { ensureInitialized(); mNetd = netd; } + /** + * Get corresponding match from firewall chain. + */ + @VisibleForTesting + 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); + } + return match; + } + private void maybeThrow(final int err, final String msg) { if (err != 0) { throw new ServiceSpecificException(err, msg + ": " + Os.strerror(err)); @@ -133,6 +224,37 @@ public class BpfNetMaps { maybeThrow(err, "Unable to set child chain"); } + /** + * Get the specified firewall chain status. + * + * @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"); + } + + final long match = getMatchByFirewallChain(childChain); + try { + 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"); + } + return (config.val & match) != 0; + } catch (ErrnoException e) { + throw new ServiceSpecificException(e.errno, + "Unable to get firewall chain status: " + Os.strerror(e.errno)); + } + } + /** * Replaces the contents of the specified UID-based firewall chain. * diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index 853a1a2782..656865415a 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -11383,6 +11383,13 @@ public class ConnectivityService extends IConnectivityManager.Stub } } + @Override + public boolean getFirewallChainEnabled(final int chain) { + enforceNetworkStackOrSettingsPermission(); + + return mBpfNetMaps.getChainEnabled(chain); + } + @Override public void replaceFirewallChain(final int chain, final int[] uids) { enforceNetworkStackOrSettingsPermission(); diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java index f07a10d6d0..9d63762e3c 100644 --- a/tests/unit/java/com/android/server/BpfNetMapsTest.java +++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java @@ -16,43 +16,92 @@ package com.android.server; +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_OEM_DENY_1; +import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2; +import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3; +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.net.INetd.PERMISSION_INTERNET; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeFalse; import static org.mockito.Mockito.verify; import android.net.INetd; import android.os.Build; +import android.os.ServiceSpecificException; import androidx.test.filters.SmallTest; import com.android.modules.utils.build.SdkLevel; +import com.android.net.module.util.BpfMap; +import com.android.net.module.util.Struct.U32; import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.DevSdkIgnoreRunner; +import com.android.testutils.TestBpfMap; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; + @RunWith(DevSdkIgnoreRunner.class) @SmallTest @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) public final class BpfNetMapsTest { private static final String TAG = "BpfNetMapsTest"; + + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + private static final int TEST_UID = 10086; private static final int[] TEST_UIDS = {10002, 10003}; private static final String IFNAME = "wlan0"; private static final String CHAINNAME = "fw_dozable"; + private static final U32 UID_RULES_CONFIGURATION_KEY = new U32(0); + private static final List FIREWALL_CHAINS = List.of( + FIREWALL_CHAIN_DOZABLE, + FIREWALL_CHAIN_STANDBY, + FIREWALL_CHAIN_POWERSAVE, + FIREWALL_CHAIN_RESTRICTED, + FIREWALL_CHAIN_LOW_POWER_STANDBY, + FIREWALL_CHAIN_OEM_DENY_1, + FIREWALL_CHAIN_OEM_DENY_2, + FIREWALL_CHAIN_OEM_DENY_3 + ); + private BpfNetMaps mBpfNetMaps; @Mock INetd mNetd; + private static final TestBpfMap sConfigurationMap = + new TestBpfMap<>(U32.class, U32.class); @Before - public void setUp() { + public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mBpfNetMaps = new BpfNetMaps(mNetd); + BpfNetMaps.initialize(makeDependencies()); + sConfigurationMap.clear(); + } + + private static BpfNetMaps.Dependencies makeDependencies() { + return new BpfNetMaps.Dependencies() { + @Override + public BpfMap getConfigurationMap() { + return sConfigurationMap; + } + }; } @Test @@ -65,4 +114,77 @@ public final class BpfNetMapsTest { mBpfNetMaps.setNetPermForUids(PERMISSION_INTERNET, TEST_UIDS); verify(mNetd).trafficSetNetPermForUids(PERMISSION_INTERNET, TEST_UIDS); } + + private void doTestGetChainEnabled(final List enableChains) throws Exception { + long match = 0; + for (final int chain: enableChains) { + match |= mBpfNetMaps.getMatchByFirewallChain(chain); + } + sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(match)); + + for (final int chain: FIREWALL_CHAINS) { + final String testCase = "EnabledChains: " + enableChains + " CheckedChain: " + chain; + if (enableChains.contains(chain)) { + assertTrue("Expected getChainEnabled returns True, " + testCase, + mBpfNetMaps.getChainEnabled(chain)); + } else { + assertFalse("Expected getChainEnabled returns False, " + testCase, + mBpfNetMaps.getChainEnabled(chain)); + } + } + } + + private void doTestGetChainEnabled(final int enableChain) throws Exception { + doTestGetChainEnabled(List.of(enableChain)); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.S_V2) + public void testGetChainEnabled() throws Exception { + doTestGetChainEnabled(FIREWALL_CHAIN_DOZABLE); + doTestGetChainEnabled(FIREWALL_CHAIN_STANDBY); + doTestGetChainEnabled(FIREWALL_CHAIN_POWERSAVE); + doTestGetChainEnabled(FIREWALL_CHAIN_RESTRICTED); + doTestGetChainEnabled(FIREWALL_CHAIN_LOW_POWER_STANDBY); + doTestGetChainEnabled(FIREWALL_CHAIN_OEM_DENY_1); + doTestGetChainEnabled(FIREWALL_CHAIN_OEM_DENY_2); + doTestGetChainEnabled(FIREWALL_CHAIN_OEM_DENY_3); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.S_V2) + public void testGetChainEnabledMultipleChainEnabled() throws Exception { + doTestGetChainEnabled(List.of( + FIREWALL_CHAIN_DOZABLE, + FIREWALL_CHAIN_STANDBY)); + doTestGetChainEnabled(List.of( + FIREWALL_CHAIN_DOZABLE, + FIREWALL_CHAIN_STANDBY, + FIREWALL_CHAIN_POWERSAVE, + FIREWALL_CHAIN_RESTRICTED)); + doTestGetChainEnabled(FIREWALL_CHAINS); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.S_V2) + public void testGetChainEnabledInvalidChain() { + final Class expected = IllegalArgumentException.class; + assertThrows(expected, () -> mBpfNetMaps.getChainEnabled(-1 /* childChain */)); + assertThrows(expected, () -> mBpfNetMaps.getChainEnabled(1000 /* childChain */)); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.S_V2) + public void testGetChainEnabledMissingConfiguration() { + // sConfigurationMap does not have entry for UID_RULES_CONFIGURATION_KEY + assertThrows(ServiceSpecificException.class, + () -> mBpfNetMaps.getChainEnabled(FIREWALL_CHAIN_DOZABLE)); + } + + @Test + @IgnoreAfter(Build.VERSION_CODES.S_V2) + public void testGetChainEnabledBeforeT() { + assertThrows(UnsupportedOperationException.class, + () -> mBpfNetMaps.getChainEnabled(FIREWALL_CHAIN_DOZABLE)); + } }