Merge "Register localOnly softapCallback for local only hotspot"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 95f854b..c4c79c6 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -22,6 +22,18 @@
}
]
},
+ // Also run CtsNetTestCasesLatestSdk to ensure tests using older shims pass.
+ {
+ "name": "CtsNetTestCasesLatestSdk",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
+ },
{
"name": "bpf_existence_test"
},
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 76c5d5c..bb40935 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -105,6 +105,12 @@
certificate: "com.android.tethering",
}
+filegroup {
+ name: "connectivity-hiddenapi-files",
+ srcs: ["hiddenapi/*.txt"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
// Encapsulate the contributions made by the com.android.tethering to the bootclasspath.
bootclasspath_fragment {
name: "com.android.tethering-bootclasspath-fragment",
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index d188b6c..af017f3 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -1318,7 +1318,7 @@
// Finally bring up serving on the new interface
mWifiP2pTetherInterface = group.getInterface();
- enableWifiIpServing(mWifiP2pTetherInterface, IFACE_IP_MODE_LOCAL_ONLY);
+ enableWifiP2pIpServing(mWifiP2pTetherInterface);
}
private void handleUserRestrictionAction() {
@@ -1409,20 +1409,22 @@
changeInterfaceState(ifname, ipServingMode);
}
- private void disableWifiIpServingCommon(int tetheringType, String ifname, int apState) {
- mLog.log("Canceling WiFi tethering request -"
- + " type=" + tetheringType
- + " interface=" + ifname
- + " state=" + apState);
-
- if (!TextUtils.isEmpty(ifname)) {
- final TetherState ts = mTetherStates.get(ifname);
- if (ts != null) {
- ts.ipServer.unwanted();
- return;
- }
+ private void disableWifiIpServingCommon(int tetheringType, String ifname) {
+ if (!TextUtils.isEmpty(ifname) && mTetherStates.containsKey(ifname)) {
+ mTetherStates.get(ifname).ipServer.unwanted();
+ return;
}
+ if (SdkLevel.isAtLeastT()) {
+ mLog.e("Tethering no longer handle untracked interface after T: " + ifname);
+ return;
+ }
+
+ // Attempt to guess the interface name before T. Pure AOSP code should never enter here
+ // because WIFI_AP_STATE_CHANGED intent always include ifname and it should be tracked
+ // by mTetherStates. In case OEMs have some modification in wifi side which pass null
+ // or empty ifname. Before T, tethering allow to disable the first wifi ipServer if
+ // given ifname don't match any tracking ipServer.
for (int i = 0; i < mTetherStates.size(); i++) {
final IpServer ipServer = mTetherStates.valueAt(i).ipServer;
if (ipServer.interfaceType() == tetheringType) {
@@ -1430,7 +1432,6 @@
return;
}
}
-
mLog.log("Error disabling Wi-Fi IP serving; "
+ (TextUtils.isEmpty(ifname) ? "no interface name specified"
: "specified interface: " + ifname));
@@ -1439,20 +1440,39 @@
private void disableWifiIpServing(String ifname, int apState) {
// Regardless of whether we requested this transition, the AP has gone
// down. Don't try to tether again unless we're requested to do so.
- // TODO: Remove this altogether, once Wi-Fi reliably gives us an
- // interface name with every broadcast.
mWifiTetherRequested = false;
- disableWifiIpServingCommon(TETHERING_WIFI, ifname, apState);
+ mLog.log("Canceling WiFi tethering request - interface=" + ifname + " state=" + apState);
+
+ disableWifiIpServingCommon(TETHERING_WIFI, ifname);
+ }
+
+ private void enableWifiP2pIpServing(String ifname) {
+ if (TextUtils.isEmpty(ifname)) {
+ mLog.e("Cannot enable P2P IP serving with invalid interface");
+ return;
+ }
+
+ // After T, tethering always trust the iface pass by state change intent. This allow
+ // tethering to deprecate tetherable p2p regexs after T.
+ final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI_P2P : ifaceNameToType(ifname);
+ if (!checkTetherableType(type)) {
+ mLog.e(ifname + " is not a tetherable iface, ignoring");
+ return;
+ }
+ enableIpServing(type, ifname, IpServer.STATE_LOCAL_ONLY);
}
private void disableWifiP2pIpServingIfNeeded(String ifname) {
if (TextUtils.isEmpty(ifname)) return;
- disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname, /* fake */ 0);
+ mLog.log("Canceling P2P tethering request - interface=" + ifname);
+ disableWifiIpServingCommon(TETHERING_WIFI_P2P, ifname);
}
private void enableWifiIpServing(String ifname, int wifiIpMode) {
+ mLog.log("request WiFi tethering - interface=" + ifname + " state=" + wifiIpMode);
+
// Map wifiIpMode values to IpServer.Callback serving states, inferring
// from mWifiTetherRequested as a final "best guess".
final int ipServingMode;
@@ -1468,13 +1488,18 @@
return;
}
+ // After T, tethering always trust the iface pass by state change intent. This allow
+ // tethering to deprecate tetherable wifi regexs after T.
+ final int type = SdkLevel.isAtLeastT() ? TETHERING_WIFI : ifaceNameToType(ifname);
+ if (!checkTetherableType(type)) {
+ mLog.e(ifname + " is not a tetherable iface, ignoring");
+ return;
+ }
+
if (!TextUtils.isEmpty(ifname)) {
- ensureIpServerStarted(ifname);
- changeInterfaceState(ifname, ipServingMode);
+ enableIpServing(type, ifname, ipServingMode);
} else {
- mLog.e(String.format(
- "Cannot enable IP serving in mode %s on missing interface name",
- ipServingMode));
+ mLog.e("Cannot enable IP serving on missing interface name");
}
}
@@ -2745,23 +2770,28 @@
mTetherMainSM.sendMessage(which, state, 0, newLp);
}
+ private boolean hasSystemFeature(final String feature) {
+ return mContext.getPackageManager().hasSystemFeature(feature);
+ }
+
+ private boolean checkTetherableType(int type) {
+ if ((type == TETHERING_WIFI || type == TETHERING_WIGIG)
+ && !hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+ return false;
+ }
+
+ if (type == TETHERING_WIFI_P2P && !hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
+ return false;
+ }
+
+ return type != TETHERING_INVALID;
+ }
+
private void ensureIpServerStarted(final String iface) {
// If we don't care about this type of interface, ignore.
final int interfaceType = ifaceNameToType(iface);
- if (interfaceType == TETHERING_INVALID) {
- mLog.log(iface + " is not a tetherable iface, ignoring");
- return;
- }
-
- final PackageManager pm = mContext.getPackageManager();
- if ((interfaceType == TETHERING_WIFI || interfaceType == TETHERING_WIGIG)
- && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
- mLog.log(iface + " is not tetherable, because WiFi feature is disabled");
- return;
- }
- if (interfaceType == TETHERING_WIFI_P2P
- && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
- mLog.log(iface + " is not tetherable, because WiFi Direct feature is disabled");
+ if (!checkTetherableType(interfaceType)) {
+ mLog.log(iface + " is used for " + interfaceType + " which is not tetherable");
return;
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index f99d45e..80666e6 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -58,6 +58,7 @@
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@@ -956,7 +957,7 @@
// Emulate externally-visible WifiManager effects, when hotspot mode
// is being torn down.
- sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
mLooper.dispatchAll();
@@ -1531,7 +1532,7 @@
// Emulate externally-visible WifiManager effects, when tethering mode
// is being torn down.
- sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
mTethering.interfaceRemoved(TEST_WLAN_IFNAME);
mLooper.dispatchAll();
@@ -1932,7 +1933,13 @@
mTethering.unregisterTetheringEventCallback(callback);
mLooper.dispatchAll();
mTethering.stopTethering(TETHERING_WIFI);
- sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED);
+ if (isAtLeastT()) {
+ // After T, tethering doesn't support WIFI_AP_STATE_DISABLED with null interface name.
+ callback2.assertNoStateChangeCallback();
+ sendWifiApStateChanged(WIFI_AP_STATE_DISABLED, TEST_WLAN_IFNAME,
+ IFACE_IP_MODE_TETHERED);
+ }
tetherState = callback2.pollTetherStatesChanged();
assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface});
mLooper.dispatchAll();
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index a6e78b6..9a246a6 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -132,6 +132,7 @@
RESTRICTED_MATCH = (1 << 5),
LOW_POWER_STANDBY_MATCH = (1 << 6),
IIF_MATCH = (1 << 7),
+ LOCKDOWN_VPN_MATCH = (1 << 8),
};
enum BpfPermissionMatch {
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index f3f675f..d629b41 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -214,9 +214,16 @@
return BPF_DROP;
}
}
- if (direction == BPF_INGRESS && (uidRules & IIF_MATCH)) {
- // Drops packets not coming from lo nor the allowlisted interface
- if (allowed_iif && skb->ifindex != 1 && skb->ifindex != allowed_iif) {
+ if (direction == BPF_INGRESS && skb->ifindex != 1) {
+ if (uidRules & IIF_MATCH) {
+ if (allowed_iif && skb->ifindex != allowed_iif) {
+ // Drops packets not coming from lo nor the allowed interface
+ // allowed interface=0 is a wildcard and does not drop packets
+ return BPF_DROP_UNLESS_DNS;
+ }
+ } else if (uidRules & LOCKDOWN_VPN_MATCH) {
+ // Drops packets not coming from lo and rule does not have IIF_MATCH but has
+ // LOCKDOWN_VPN_MATCH
return BPF_DROP_UNLESS_DNS;
}
}
@@ -373,8 +380,7 @@
return BPF_NOMATCH;
}
-DEFINE_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
- KVER(4, 14, 0))
+DEFINE_BPF_PROG("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create)
(struct bpf_sock* sk) {
uint64_t gid_uid = bpf_get_current_uid_gid();
/*
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 1e508a0..8c32ded 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -103,7 +103,7 @@
// Do not add static_libs to this library: put them in framework-connectivity instead.
// The jarjar rules are only so that references to jarjared utils in
// framework-connectivity-pre-jarjar match at runtime.
- jarjar_rules: ":connectivity-jarjar-rules",
+ jarjar_rules: ":framework-connectivity-jarjar-rules",
permitted_packages: [
"android.app.usage",
"android.net",
diff --git a/framework/Android.bp b/framework/Android.bp
index d7de439..c8b64c7 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -111,6 +111,7 @@
// because the tethering stubs depend on the connectivity stubs (e.g.,
// TetheringRequest depends on LinkAddress).
"framework-tethering.stubs.module_lib",
+ "framework-wifi.stubs.module_lib",
],
visibility: ["//packages/modules/Connectivity:__subpackages__"]
}
@@ -119,7 +120,7 @@
name: "framework-connectivity",
defaults: ["framework-connectivity-defaults"],
installable: true,
- jarjar_rules: ":connectivity-jarjar-rules",
+ jarjar_rules: ":framework-connectivity-jarjar-rules",
permitted_packages: ["android.net"],
impl_library_visibility: [
"//packages/modules/Connectivity/Tethering/apex",
@@ -222,3 +223,35 @@
],
output_extension: "srcjar",
}
+
+java_genrule {
+ name: "framework-connectivity-jarjar-rules",
+ tool_files: [
+ ":connectivity-hiddenapi-files",
+ ":framework-connectivity-pre-jarjar",
+ ":framework-connectivity-t-pre-jarjar",
+ ":framework-connectivity.stubs.module_lib",
+ ":framework-connectivity-t.stubs.module_lib",
+ "jarjar-excludes.txt",
+ ],
+ tools: [
+ "jarjar-rules-generator",
+ "dexdump",
+ ],
+ out: ["framework_connectivity_jarjar_rules.txt"],
+ cmd: "$(location jarjar-rules-generator) " +
+ "--jars $(location :framework-connectivity-pre-jarjar) " +
+ "$(location :framework-connectivity-t-pre-jarjar) " +
+ "--prefix android.net.connectivity " +
+ "--apistubs $(location :framework-connectivity.stubs.module_lib) " +
+ "$(location :framework-connectivity-t.stubs.module_lib) " +
+ "--unsupportedapi $(locations :connectivity-hiddenapi-files) " +
+ "--excludes $(location jarjar-excludes.txt) " +
+ "--dexdump $(location dexdump) " +
+ "--output $(out)",
+ visibility: [
+ "//packages/modules/Connectivity/framework:__subpackages__",
+ "//packages/modules/Connectivity/framework-t:__subpackages__",
+ "//packages/modules/Connectivity/service",
+ ],
+}
diff --git a/framework/jarjar-excludes.txt b/framework/jarjar-excludes.txt
new file mode 100644
index 0000000..1311765
--- /dev/null
+++ b/framework/jarjar-excludes.txt
@@ -0,0 +1,25 @@
+# INetworkStatsProvider / INetworkStatsProviderCallback are referenced from net-tests-utils, which
+# may be used by tests that do not apply connectivity jarjar rules.
+# TODO: move files to a known internal package (like android.net.connectivity.visiblefortesting)
+# so that they do not need jarjar
+android\.net\.netstats\.provider\.INetworkStatsProvider(\$.+)?
+android\.net\.netstats\.provider\.INetworkStatsProviderCallback(\$.+)?
+
+# INetworkAgent / INetworkAgentRegistry are used in NetworkAgentTest
+# TODO: move files to android.net.connectivity.visiblefortesting
+android\.net\.INetworkAgent(\$.+)?
+android\.net\.INetworkAgentRegistry(\$.+)?
+
+# IConnectivityDiagnosticsCallback used in ConnectivityDiagnosticsManagerTest
+# TODO: move files to android.net.connectivity.visiblefortesting
+android\.net\.IConnectivityDiagnosticsCallback(\$.+)?
+
+
+# KeepaliveUtils is used by ConnectivityManager CTS
+# TODO: move into service-connectivity so framework-connectivity stops using
+# ServiceConnectivityResources (callers need high permissions to find/query the resource apk anyway)
+# and have a ConnectivityManager test API instead
+android\.net\.util\.KeepaliveUtils(\$.+)?
+
+# TODO (b/217115866): add jarjar rules for Nearby
+android\.nearby\..+
diff --git a/framework/jni/android_net_NetworkUtils.cpp b/framework/jni/android_net_NetworkUtils.cpp
index 7478b3e..857ece5 100644
--- a/framework/jni/android_net_NetworkUtils.cpp
+++ b/framework/jni/android_net_NetworkUtils.cpp
@@ -232,7 +232,8 @@
return NULL;
}
- jclass class_TcpRepairWindow = env->FindClass("android/net/TcpRepairWindow");
+ jclass class_TcpRepairWindow = env->FindClass(
+ "android/net/connectivity/android/net/TcpRepairWindow");
jmethodID ctor = env->GetMethodID(class_TcpRepairWindow, "<init>", "(IIIIII)V");
return env->NewObject(class_TcpRepairWindow, ctor, trw.snd_wl1, trw.snd_wnd, trw.max_window,
@@ -253,7 +254,7 @@
{ "bindSocketToNetworkHandle", "(Ljava/io/FileDescriptor;J)I", (void*) android_net_utils_bindSocketToNetworkHandle },
{ "attachDropAllBPFFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDropAllBPFFilter },
{ "detachBPFFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_detachBPFFilter },
- { "getTcpRepairWindow", "(Ljava/io/FileDescriptor;)Landroid/net/TcpRepairWindow;", (void*) android_net_utils_getTcpRepairWindow },
+ { "getTcpRepairWindow", "(Ljava/io/FileDescriptor;)Landroid/net/connectivity/android/net/TcpRepairWindow;", (void*) android_net_utils_getTcpRepairWindow },
{ "resNetworkSend", "(J[BII)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkSend },
{ "resNetworkQuery", "(JLjava/lang/String;III)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkQuery },
{ "resNetworkResult", "(Ljava/io/FileDescriptor;)Landroid/net/DnsResolver$DnsResponse;", (void*) android_net_utils_resNetworkResult },
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index d16a6f5..9f9ee95 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -982,6 +982,16 @@
@SystemApi(client = MODULE_LIBRARIES)
public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5;
+ /**
+ * Firewall chain used for lockdown VPN.
+ * Denylist of apps that cannot receive incoming packets except on loopback because they are
+ * subject to an always-on VPN which is not currently connected.
+ *
+ * @see #BLOCKED_REASON_LOCKDOWN_VPN
+ * @hide
+ */
+ public static final int FIREWALL_CHAIN_LOCKDOWN_VPN = 6;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = false, prefix = "FIREWALL_CHAIN_", value = {
@@ -989,7 +999,8 @@
FIREWALL_CHAIN_STANDBY,
FIREWALL_CHAIN_POWERSAVE,
FIREWALL_CHAIN_RESTRICTED,
- FIREWALL_CHAIN_LOW_POWER_STANDBY
+ FIREWALL_CHAIN_LOW_POWER_STANDBY,
+ FIREWALL_CHAIN_LOCKDOWN_VPN
})
public @interface FirewallChain {}
// LINT.ThenChange(packages/modules/Connectivity/service/native/include/Common.h)
diff --git a/framework/src/android/net/DnsResolverServiceManager.java b/framework/src/android/net/DnsResolverServiceManager.java
index 79009e8..e64d2ae 100644
--- a/framework/src/android/net/DnsResolverServiceManager.java
+++ b/framework/src/android/net/DnsResolverServiceManager.java
@@ -29,7 +29,7 @@
private final IBinder mResolver;
- DnsResolverServiceManager(IBinder resolver) {
+ public DnsResolverServiceManager(IBinder resolver) {
mResolver = resolver;
}
diff --git a/framework/src/android/net/NattSocketKeepalive.java b/framework/src/android/net/NattSocketKeepalive.java
index a15d165..56cc923 100644
--- a/framework/src/android/net/NattSocketKeepalive.java
+++ b/framework/src/android/net/NattSocketKeepalive.java
@@ -33,7 +33,7 @@
@NonNull private final InetAddress mDestination;
private final int mResourceId;
- NattSocketKeepalive(@NonNull IConnectivityManager service,
+ public NattSocketKeepalive(@NonNull IConnectivityManager service,
@NonNull Network network,
@NonNull ParcelFileDescriptor pfd,
int resourceId,
@@ -48,7 +48,7 @@
}
@Override
- void startImpl(int intervalSec) {
+ protected void startImpl(int intervalSec) {
mExecutor.execute(() -> {
try {
mService.startNattKeepaliveWithFd(mNetwork, mPfd, mResourceId,
@@ -62,7 +62,7 @@
}
@Override
- void stopImpl() {
+ protected void stopImpl() {
mExecutor.execute(() -> {
try {
if (mSlot != null) {
diff --git a/framework/src/android/net/QosCallbackConnection.java b/framework/src/android/net/QosCallbackConnection.java
index de0fc24..cfceddd 100644
--- a/framework/src/android/net/QosCallbackConnection.java
+++ b/framework/src/android/net/QosCallbackConnection.java
@@ -35,7 +35,7 @@
*
* @hide
*/
-class QosCallbackConnection extends android.net.IQosCallback.Stub {
+public class QosCallbackConnection extends android.net.IQosCallback.Stub {
@NonNull private final ConnectivityManager mConnectivityManager;
@Nullable private volatile QosCallback mCallback;
@@ -56,7 +56,7 @@
* {@link Executor} must run callback sequentially, otherwise the order of
* callbacks cannot be guaranteed.
*/
- QosCallbackConnection(@NonNull final ConnectivityManager connectivityManager,
+ public QosCallbackConnection(@NonNull final ConnectivityManager connectivityManager,
@NonNull final QosCallback callback,
@NonNull final Executor executor) {
mConnectivityManager = Objects.requireNonNull(connectivityManager,
@@ -142,7 +142,7 @@
* There are no synchronization guarantees on exactly when the callback will stop receiving
* messages.
*/
- void stopReceivingMessages() {
+ public void stopReceivingMessages() {
mCallback = null;
}
}
diff --git a/framework/src/android/net/QosCallbackException.java b/framework/src/android/net/QosCallbackException.java
index b80cff4..9e5d98a 100644
--- a/framework/src/android/net/QosCallbackException.java
+++ b/framework/src/android/net/QosCallbackException.java
@@ -85,7 +85,7 @@
* {@hide}
*/
@NonNull
- static QosCallbackException createException(@ExceptionType final int type) {
+ public static QosCallbackException createException(@ExceptionType final int type) {
switch (type) {
case EX_TYPE_FILTER_NETWORK_RELEASED:
return new QosCallbackException(new NetworkReleasedException());
diff --git a/framework/src/android/net/QosFilter.java b/framework/src/android/net/QosFilter.java
index b432644..01dc4bb 100644
--- a/framework/src/android/net/QosFilter.java
+++ b/framework/src/android/net/QosFilter.java
@@ -33,13 +33,15 @@
@SystemApi
public abstract class QosFilter {
- /**
- * The constructor is kept hidden from outside this package to ensure that all derived types
- * are known and properly handled when being passed to and from {@link NetworkAgent}.
- *
- * @hide
- */
- QosFilter() {
+ /** @hide */
+ protected QosFilter() {
+ // Ensure that all derived types are known, and known to be properly handled when being
+ // passed to and from NetworkAgent.
+ // For now the only known derived type is QosSocketFilter.
+ if (!(this instanceof QosSocketFilter)) {
+ throw new UnsupportedOperationException(
+ "Unsupported QosFilter type: " + this.getClass().getName());
+ }
}
/**
diff --git a/framework/src/android/net/QosSocketInfo.java b/framework/src/android/net/QosSocketInfo.java
index 49ac22b..da9b356 100644
--- a/framework/src/android/net/QosSocketInfo.java
+++ b/framework/src/android/net/QosSocketInfo.java
@@ -73,9 +73,10 @@
* The parcel file descriptor wrapped around the socket's file descriptor.
*
* @return the parcel file descriptor of the socket
+ * @hide
*/
@NonNull
- ParcelFileDescriptor getParcelFileDescriptor() {
+ public ParcelFileDescriptor getParcelFileDescriptor() {
return mParcelFileDescriptor;
}
diff --git a/framework/src/android/net/SocketKeepalive.java b/framework/src/android/net/SocketKeepalive.java
index f6cae72..57cf5e3 100644
--- a/framework/src/android/net/SocketKeepalive.java
+++ b/framework/src/android/net/SocketKeepalive.java
@@ -52,7 +52,8 @@
* request. If it does, it MUST support at least 3 concurrent keepalive slots.
*/
public abstract class SocketKeepalive implements AutoCloseable {
- static final String TAG = "SocketKeepalive";
+ /** @hide */
+ protected static final String TAG = "SocketKeepalive";
/**
* Success. It indicates there is no error.
@@ -215,15 +216,22 @@
}
}
- @NonNull final IConnectivityManager mService;
- @NonNull final Network mNetwork;
- @NonNull final ParcelFileDescriptor mPfd;
- @NonNull final Executor mExecutor;
- @NonNull final ISocketKeepaliveCallback mCallback;
+ /** @hide */
+ @NonNull protected final IConnectivityManager mService;
+ /** @hide */
+ @NonNull protected final Network mNetwork;
+ /** @hide */
+ @NonNull protected final ParcelFileDescriptor mPfd;
+ /** @hide */
+ @NonNull protected final Executor mExecutor;
+ /** @hide */
+ @NonNull protected final ISocketKeepaliveCallback mCallback;
// TODO: remove slot since mCallback could be used to identify which keepalive to stop.
- @Nullable Integer mSlot;
+ /** @hide */
+ @Nullable protected Integer mSlot;
- SocketKeepalive(@NonNull IConnectivityManager service, @NonNull Network network,
+ /** @hide */
+ public SocketKeepalive(@NonNull IConnectivityManager service, @NonNull Network network,
@NonNull ParcelFileDescriptor pfd,
@NonNull Executor executor, @NonNull Callback callback) {
mService = service;
@@ -303,7 +311,8 @@
startImpl(intervalSec);
}
- abstract void startImpl(int intervalSec);
+ /** @hide */
+ protected abstract void startImpl(int intervalSec);
/**
* Requests that keepalive be stopped. The application must wait for {@link Callback#onStopped}
@@ -313,7 +322,8 @@
stopImpl();
}
- abstract void stopImpl();
+ /** @hide */
+ protected abstract void stopImpl();
/**
* Deactivate this {@link SocketKeepalive} and free allocated resources. The instance won't be
diff --git a/framework/src/android/net/TcpSocketKeepalive.java b/framework/src/android/net/TcpSocketKeepalive.java
index d89814d..7131784 100644
--- a/framework/src/android/net/TcpSocketKeepalive.java
+++ b/framework/src/android/net/TcpSocketKeepalive.java
@@ -24,9 +24,9 @@
import java.util.concurrent.Executor;
/** @hide */
-final class TcpSocketKeepalive extends SocketKeepalive {
+public final class TcpSocketKeepalive extends SocketKeepalive {
- TcpSocketKeepalive(@NonNull IConnectivityManager service,
+ public TcpSocketKeepalive(@NonNull IConnectivityManager service,
@NonNull Network network,
@NonNull ParcelFileDescriptor pfd,
@NonNull Executor executor,
@@ -50,7 +50,7 @@
* acknowledgement.
*/
@Override
- void startImpl(int intervalSec) {
+ protected void startImpl(int intervalSec) {
mExecutor.execute(() -> {
try {
mService.startTcpKeepalive(mNetwork, mPfd, intervalSec, mCallback);
@@ -62,7 +62,7 @@
}
@Override
- void stopImpl() {
+ protected void stopImpl() {
mExecutor.execute(() -> {
try {
if (mSlot != null) {
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index f3dfb57..08e66d9 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -73,16 +73,7 @@
}
RETURN_IF_NOT_OK(attachProgramToCgroup(BPF_EGRESS_PROG_PATH, cg_fd, BPF_CGROUP_INET_EGRESS));
RETURN_IF_NOT_OK(attachProgramToCgroup(BPF_INGRESS_PROG_PATH, cg_fd, BPF_CGROUP_INET_INGRESS));
-
- // For the devices that support cgroup socket filter, the socket filter
- // should be loaded successfully by bpfloader. So we attach the filter to
- // cgroup if the program is pinned properly.
- // TODO: delete the if statement once all devices should support cgroup
- // socket filter (ie. the minimum kernel version required is 4.14).
- if (!access(CGROUP_SOCKET_PROG_PATH, F_OK)) {
- RETURN_IF_NOT_OK(
- attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
- }
+ RETURN_IF_NOT_OK(attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
return netdutils::status::ok;
}
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index ea57bac..6def44f 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -513,7 +513,7 @@
break;
}
- String name = fullName.substring(0, index);
+ String name = unescape(fullName.substring(0, index));
String rest = fullName.substring(index);
String type = rest.replace(".local.", "");
@@ -590,6 +590,35 @@
}
}
+ // The full service name is escaped from standard DNS rules on mdnsresponder, making it suitable
+ // for passing to standard system DNS APIs such as res_query() . Thus, make the service name
+ // unescape for getting right service address. See "Notes on DNS Name Escaping" on
+ // external/mdnsresponder/mDNSShared/dns_sd.h for more details.
+ private String unescape(String s) {
+ StringBuilder sb = new StringBuilder(s.length());
+ for (int i = 0; i < s.length(); ++i) {
+ char c = s.charAt(i);
+ if (c == '\\') {
+ if (++i >= s.length()) {
+ Log.e(TAG, "Unexpected end of escape sequence in: " + s);
+ break;
+ }
+ c = s.charAt(i);
+ if (c != '.' && c != '\\') {
+ if (i + 2 >= s.length()) {
+ Log.e(TAG, "Unexpected end of escape sequence in: " + s);
+ break;
+ }
+ c = (char) ((c - '0') * 100 + (s.charAt(i + 1) - '0') * 10
+ + (s.charAt(i + 2) - '0'));
+ i += 2;
+ }
+ }
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
@VisibleForTesting
NsdService(Context ctx, Handler handler, long cleanupDelayMs) {
mCleanupDelayMs = cleanupDelayMs;
diff --git a/service/Android.bp b/service/Android.bp
index 7dbdc92..0393c79 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -169,7 +169,7 @@
"networkstack-client",
"PlatformProperties",
"service-connectivity-protos",
- "NetworkStackApiCurrentShims",
+ "NetworkStackApiStableShims",
],
apex_available: [
"com.android.tethering",
@@ -224,9 +224,15 @@
lint: { strict_updatability_linting: true },
}
-filegroup {
+genrule {
name: "connectivity-jarjar-rules",
- srcs: ["jarjar-rules.txt"],
+ defaults: ["jarjar-rules-combine-defaults"],
+ srcs: [
+ ":framework-connectivity-jarjar-rules",
+ ":service-connectivity-jarjar-gen",
+ ":service-nearby-jarjar-gen",
+ ],
+ out: ["connectivity-jarjar-rules.txt"],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
@@ -237,3 +243,45 @@
srcs: ["src/com/android/server/BpfNetMaps.java"],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
+
+java_genrule {
+ name: "service-connectivity-jarjar-gen",
+ tool_files: [
+ ":service-connectivity-pre-jarjar",
+ ":service-connectivity-tiramisu-pre-jarjar",
+ "jarjar-excludes.txt",
+ ],
+ tools: [
+ "jarjar-rules-generator",
+ "dexdump",
+ ],
+ out: ["service_connectivity_jarjar_rules.txt"],
+ cmd: "$(location jarjar-rules-generator) " +
+ "--jars $(location :service-connectivity-pre-jarjar) " +
+ "$(location :service-connectivity-tiramisu-pre-jarjar) " +
+ "--prefix android.net.connectivity " +
+ "--excludes $(location jarjar-excludes.txt) " +
+ "--dexdump $(location dexdump) " +
+ "--output $(out)",
+ visibility: ["//visibility:private"],
+}
+
+java_genrule {
+ name: "service-nearby-jarjar-gen",
+ tool_files: [
+ ":service-nearby-pre-jarjar",
+ "jarjar-excludes.txt",
+ ],
+ tools: [
+ "jarjar-rules-generator",
+ "dexdump",
+ ],
+ out: ["service_nearby_jarjar_rules.txt"],
+ cmd: "$(location jarjar-rules-generator) " +
+ "--jars $(location :service-nearby-pre-jarjar) " +
+ "--prefix com.android.server.nearby " +
+ "--excludes $(location jarjar-excludes.txt) " +
+ "--dexdump $(location dexdump) " +
+ "--output $(out)",
+ visibility: ["//visibility:private"],
+}
diff --git a/service/jarjar-excludes.txt b/service/jarjar-excludes.txt
new file mode 100644
index 0000000..b0d6763
--- /dev/null
+++ b/service/jarjar-excludes.txt
@@ -0,0 +1,9 @@
+# Classes loaded by SystemServer via their hardcoded name, so they can't be jarjared
+com\.android\.server\.ConnectivityServiceInitializer(\$.+)?
+com\.android\.server\.NetworkStatsServiceInitializer(\$.+)?
+
+# Do not jarjar com.android.server, as several unit tests fail because they lose
+# package-private visibility between jarjared and non-jarjared classes.
+# TODO: fix the tests and also jarjar com.android.server, or at least only exclude a package that
+# is specific to the module like com.android.server.connectivity
+com\.android\.server\..+
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
deleted file mode 100644
index c7223fc..0000000
--- a/service/jarjar-rules.txt
+++ /dev/null
@@ -1,123 +0,0 @@
-# Classes in framework-connectivity are restricted to the android.net package.
-# This cannot be changed because it is harcoded in ART in S.
-# Any missing jarjar rule for framework-connectivity would be caught by the
-# build as an unexpected class outside of the android.net package.
-rule com.android.net.module.util.** android.net.connectivity.@0
-rule com.android.modules.utils.** android.net.connectivity.@0
-rule android.net.NetworkFactory* android.net.connectivity.@0
-
-# From modules-utils-preconditions
-rule com.android.internal.util.Preconditions* android.net.connectivity.@0
-
-# From framework-connectivity-shared-srcs
-rule android.util.LocalLog* android.net.connectivity.@0
-rule android.util.IndentingPrintWriter* android.net.connectivity.@0
-rule com.android.internal.util.IndentingPrintWriter* android.net.connectivity.@0
-rule com.android.internal.util.MessageUtils* android.net.connectivity.@0
-rule com.android.internal.util.WakeupMessage* android.net.connectivity.@0
-rule com.android.internal.util.FileRotator* android.net.connectivity.@0
-rule com.android.internal.util.ProcFileReader* android.net.connectivity.@0
-
-# From framework-connectivity-protos
-rule com.google.protobuf.** android.net.connectivity.@0
-rule android.service.** android.net.connectivity.@0
-
-rule android.sysprop.** com.android.connectivity.@0
-
-rule com.android.internal.messages.** com.android.connectivity.@0
-
-# From dnsresolver_aidl_interface (newer AIDLs should go to android.net.resolv.aidl)
-rule android.net.resolv.aidl.** com.android.connectivity.@0
-rule android.net.IDnsResolver* com.android.connectivity.@0
-rule android.net.ResolverHostsParcel* com.android.connectivity.@0
-rule android.net.ResolverOptionsParcel* com.android.connectivity.@0
-rule android.net.ResolverParamsParcel* com.android.connectivity.@0
-rule android.net.ResolverParamsParcel* com.android.connectivity.@0
-# Also includes netd event listener AIDL, but this is handled by netd-client rules
-
-# From netd-client (newer AIDLs should go to android.net.netd.aidl)
-rule android.net.netd.aidl.** com.android.connectivity.@0
-# Avoid including android.net.INetdEventCallback, used in tests but not part of the module
-rule android.net.INetd com.android.connectivity.@0
-rule android.net.INetd$* com.android.connectivity.@0
-rule android.net.INetdUnsolicitedEventListener* com.android.connectivity.@0
-rule android.net.InterfaceConfigurationParcel* com.android.connectivity.@0
-rule android.net.MarkMaskParcel* com.android.connectivity.@0
-rule android.net.NativeNetworkConfig* com.android.connectivity.@0
-rule android.net.NativeNetworkType* com.android.connectivity.@0
-rule android.net.NativeVpnType* com.android.connectivity.@0
-rule android.net.RouteInfoParcel* com.android.connectivity.@0
-rule android.net.TetherConfigParcel* com.android.connectivity.@0
-rule android.net.TetherOffloadRuleParcel* com.android.connectivity.@0
-rule android.net.TetherStatsParcel* com.android.connectivity.@0
-rule android.net.UidRangeParcel* com.android.connectivity.@0
-rule android.net.metrics.INetdEventListener* com.android.connectivity.@0
-
-# From netlink-client
-rule android.net.netlink.** com.android.connectivity.@0
-
-# From networkstack-client (newer AIDLs should go to android.net.[networkstack|ipmemorystore].aidl)
-rule android.net.networkstack.aidl.** com.android.connectivity.@0
-rule android.net.ipmemorystore.aidl.** com.android.connectivity.@0
-rule android.net.ipmemorystore.aidl.** com.android.connectivity.@0
-rule android.net.DataStallReportParcelable* com.android.connectivity.@0
-rule android.net.DhcpResultsParcelable* com.android.connectivity.@0
-rule android.net.IIpMemoryStore* com.android.connectivity.@0
-rule android.net.INetworkMonitor* com.android.connectivity.@0
-rule android.net.INetworkStackConnector* com.android.connectivity.@0
-rule android.net.INetworkStackStatusCallback* com.android.connectivity.@0
-rule android.net.InformationElementParcelable* com.android.connectivity.@0
-rule android.net.InitialConfigurationParcelable* com.android.connectivity.@0
-rule android.net.IpMemoryStore* com.android.connectivity.@0
-rule android.net.Layer2InformationParcelable* com.android.connectivity.@0
-rule android.net.Layer2PacketParcelable* com.android.connectivity.@0
-rule android.net.NattKeepalivePacketDataParcelable* com.android.connectivity.@0
-rule android.net.NetworkMonitorManager* com.android.connectivity.@0
-rule android.net.NetworkTestResultParcelable* com.android.connectivity.@0
-rule android.net.PrivateDnsConfigParcel* com.android.connectivity.@0
-rule android.net.ProvisioningConfigurationParcelable* com.android.connectivity.@0
-rule android.net.ScanResultInfoParcelable* com.android.connectivity.@0
-rule android.net.TcpKeepalivePacketDataParcelable* com.android.connectivity.@0
-rule android.net.dhcp.DhcpLeaseParcelable* com.android.connectivity.@0
-rule android.net.dhcp.DhcpServingParamsParcel* com.android.connectivity.@0
-rule android.net.dhcp.IDhcpEventCallbacks* com.android.connectivity.@0
-rule android.net.dhcp.IDhcpServer* com.android.connectivity.@0
-rule android.net.ip.IIpClient* com.android.connectivity.@0
-rule android.net.ip.IpClientCallbacks* com.android.connectivity.@0
-rule android.net.ip.IpClientManager* com.android.connectivity.@0
-rule android.net.ip.IpClientUtil* com.android.connectivity.@0
-rule android.net.ipmemorystore.** com.android.connectivity.@0
-rule android.net.networkstack.** com.android.connectivity.@0
-rule android.net.shared.** com.android.connectivity.@0
-rule android.net.util.KeepalivePacketDataUtil* com.android.connectivity.@0
-
-# From connectivity-module-utils
-rule android.net.util.SharedLog* com.android.connectivity.@0
-rule android.net.shared.** com.android.connectivity.@0
-
-# From services-connectivity-shared-srcs
-rule android.net.util.NetworkConstants* com.android.connectivity.@0
-
-# From modules-utils-statemachine
-rule com.android.internal.util.IState* com.android.connectivity.@0
-rule com.android.internal.util.State* com.android.connectivity.@0
-
-# From the API shims
-rule com.android.networkstack.apishim.** com.android.connectivity.@0
-
-# From filegroup framework-connectivity-protos
-rule android.service.*Proto com.android.connectivity.@0
-
-# From mdns-aidl-interface
-rule android.net.mdns.aidl.** android.net.connectivity.@0
-
-# From nearby-service, including proto
-rule service.proto.** com.android.server.nearby.@0
-rule androidx.annotation.Keep* com.android.server.nearby.@0
-rule androidx.collection.** com.android.server.nearby.@0
-rule androidx.core.** com.android.server.nearby.@0
-rule androidx.versionedparcelable.** com.android.server.nearby.@0
-rule com.google.common.** com.android.server.nearby.@0
-
-# Remaining are connectivity sources in com.android.server and com.android.server.connectivity:
-# TODO: move to a subpackage of com.android.connectivity (such as com.android.connectivity.server)
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
index f13c68d..7b1f59c 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -133,12 +133,16 @@
static jint native_addUidInterfaceRules(JNIEnv* env, jobject clazz, jstring ifName,
jintArray jUids) {
- const ScopedUtfChars ifNameUtf8(env, ifName);
- if (ifNameUtf8.c_str() == nullptr) {
- return -EINVAL;
+ // Null ifName is a wildcard to allow apps to receive packets on all interfaces and ifIndex is
+ // set to 0.
+ int ifIndex;
+ if (ifName != nullptr) {
+ const ScopedUtfChars ifNameUtf8(env, ifName);
+ const std::string interfaceName(ifNameUtf8.c_str());
+ ifIndex = if_nametoindex(interfaceName.c_str());
+ } else {
+ ifIndex = 0;
}
- const std::string interfaceName(ifNameUtf8.c_str());
- const int ifIndex = if_nametoindex(interfaceName.c_str());
ScopedIntArrayRO uids(env, jUids);
if (uids.get() == nullptr) {
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index 473c9e3..5581c40 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -98,6 +98,7 @@
FLAG_MSG_TRANS(matchType, RESTRICTED_MATCH, match);
FLAG_MSG_TRANS(matchType, LOW_POWER_STANDBY_MATCH, match);
FLAG_MSG_TRANS(matchType, IIF_MATCH, match);
+ FLAG_MSG_TRANS(matchType, LOCKDOWN_VPN_MATCH, match);
if (match) {
return StringPrintf("Unknown match: %u", match);
}
@@ -286,16 +287,13 @@
}
Status TrafficController::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
- // iif should be non-zero if and only if match == MATCH_IIF
- if (match == IIF_MATCH && iif == 0) {
- return statusFromErrno(EINVAL, "Interface match must have nonzero interface index");
- } else if (match != IIF_MATCH && iif != 0) {
+ if (match != IIF_MATCH && iif != 0) {
return statusFromErrno(EINVAL, "Non-interface match must have zero interface index");
}
auto oldMatch = mUidOwnerMap.readValue(uid);
if (oldMatch.ok()) {
UidOwnerValue newMatch = {
- .iif = iif ? iif : oldMatch.value().iif,
+ .iif = (match == IIF_MATCH) ? iif : oldMatch.value().iif,
.rule = oldMatch.value().rule | match,
};
RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
@@ -335,6 +333,8 @@
return ALLOWLIST;
case LOW_POWER_STANDBY:
return ALLOWLIST;
+ case LOCKDOWN:
+ return DENYLIST;
case NONE:
default:
return DENYLIST;
@@ -360,6 +360,9 @@
case LOW_POWER_STANDBY:
res = updateOwnerMapEntry(LOW_POWER_STANDBY_MATCH, uid, rule, type);
break;
+ case LOCKDOWN:
+ res = updateOwnerMapEntry(LOCKDOWN_VPN_MATCH, uid, rule, type);
+ break;
case NONE:
default:
ALOGW("Unknown child chain: %d", chain);
@@ -399,9 +402,6 @@
Status TrafficController::addUidInterfaceRules(const int iif,
const std::vector<int32_t>& uidsToAdd) {
- if (!iif) {
- return statusFromErrno(EINVAL, "Interface rule must specify interface");
- }
std::lock_guard guard(mMutex);
for (auto uid : uidsToAdd) {
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
index 9529cae..ad53cb8 100644
--- a/service/native/TrafficControllerTest.cpp
+++ b/service/native/TrafficControllerTest.cpp
@@ -307,6 +307,7 @@
checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
checkUidOwnerRuleForChain(LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
+ checkUidOwnerRuleForChain(LOCKDOWN, LOCKDOWN_VPN_MATCH);
ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(NONE, TEST_UID, ALLOW, ALLOWLIST));
ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(INVALID_CHAIN, TEST_UID, ALLOW, ALLOWLIST));
}
@@ -491,6 +492,70 @@
checkEachUidValue({10001, 10002}, IIF_MATCH);
}
+TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRulesWithWildcard) {
+ // iif=0 is a wildcard
+ int iif = 0;
+ // Add interface rule with wildcard to uids
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
+ expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
+}
+
+TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRulesWithWildcard) {
+ // iif=0 is a wildcard
+ int iif = 0;
+ // Add interface rule with wildcard to two uids
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
+ expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
+
+ // Remove interface rule from one of the uids
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+ expectUidOwnerMapValues({1001}, IIF_MATCH, iif);
+ checkEachUidValue({1001}, IIF_MATCH);
+
+ // Remove interface rule from the remaining uid
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001})));
+ expectMapEmpty(mFakeUidOwnerMap);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndExistingMatches) {
+ // Set up existing DOZABLE_MATCH and POWERSAVE_MATCH rule
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+ TrafficController::IptOpInsert)));
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+ TrafficController::IptOpInsert)));
+
+ // iif=0 is a wildcard
+ int iif = 0;
+ // Add interface rule with wildcard to the existing uid
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
+ expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
+
+ // Remove interface rule with wildcard from the existing uid
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+ expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndNewMatches) {
+ // iif=0 is a wildcard
+ int iif = 0;
+ // Set up existing interface rule with wildcard
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
+
+ // Add DOZABLE_MATCH and POWERSAVE_MATCH rule to the existing uid
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+ TrafficController::IptOpInsert)));
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+ TrafficController::IptOpInsert)));
+ expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
+
+ // Remove DOZABLE_MATCH and POWERSAVE_MATCH rule from the existing uid
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+ TrafficController::IptOpDelete)));
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+ TrafficController::IptOpDelete)));
+ expectUidOwnerMapValues({1000}, IIF_MATCH, iif);
+}
+
TEST_F(TrafficControllerTest, TestGrantInternetPermission) {
std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
diff --git a/service/native/include/Common.h b/service/native/include/Common.h
index dc44845..847acec 100644
--- a/service/native/include/Common.h
+++ b/service/native/include/Common.h
@@ -35,6 +35,7 @@
POWERSAVE = 3,
RESTRICTED = 4,
LOW_POWER_STANDBY = 5,
+ LOCKDOWN = 6,
INVALID_CHAIN
};
// LINT.ThenChange(packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java)
diff --git a/service/proguard.flags b/service/proguard.flags
index 94397ab..557ba59 100644
--- a/service/proguard.flags
+++ b/service/proguard.flags
@@ -2,8 +2,6 @@
# TODO: instead of keeping everything, consider listing only "entry points"
# (service loader, JNI registered methods, etc) and letting the optimizer do its job
-keep class android.net.** { *; }
--keep class com.android.connectivity.** { *; }
--keep class com.android.net.** { *; }
-keep class !com.android.server.nearby.**,com.android.server.** { *; }
# Prevent proguard from stripping out any nearby-service and fast-pair-lite-protos fields.
@@ -15,4 +13,4 @@
# This replicates the base proguard rule used by the build by default
# (proguard_basic_keeps.flags), but needs to be specified here because the
# com.google.protobuf package is jarjared to the below package.
--keepclassmembers class * extends com.android.connectivity.com.google.protobuf.MessageLite { <fields>; }
+-keepclassmembers class * extends com.android.server.nearby.com.google.protobuf.MessageLite { <fields>; }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 66ef9e9..02b8e62 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -5969,6 +5969,10 @@
+ Arrays.toString(ranges) + "): netd command failed: " + e);
}
+ if (SdkLevel.isAtLeastT()) {
+ mPermissionMonitor.updateVpnLockdownUidRanges(requireVpn, ranges);
+ }
+
for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
final boolean curMetered = nai.networkCapabilities.isMetered();
maybeNotifyNetworkBlocked(nai, curMetered, curMetered,
@@ -7732,10 +7736,10 @@
private void updateVpnFiltering(LinkProperties newLp, LinkProperties oldLp,
NetworkAgentInfo nai) {
- final String oldIface = oldLp != null ? oldLp.getInterfaceName() : null;
- final String newIface = newLp != null ? newLp.getInterfaceName() : null;
- final boolean wasFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, oldLp);
- final boolean needsFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, newLp);
+ final String oldIface = getVpnIsolationInterface(nai, nai.networkCapabilities, oldLp);
+ final String newIface = getVpnIsolationInterface(nai, nai.networkCapabilities, newLp);
+ final boolean wasFiltering = requiresVpnAllowRule(nai, oldLp, oldIface);
+ final boolean needsFiltering = requiresVpnAllowRule(nai, newLp, newIface);
if (!wasFiltering && !needsFiltering) {
// Nothing to do.
@@ -7748,11 +7752,19 @@
}
final Set<UidRange> ranges = nai.networkCapabilities.getUidRanges();
+ if (ranges == null || ranges.isEmpty()) {
+ return;
+ }
+
final int vpnAppUid = nai.networkCapabilities.getOwnerUid();
// TODO: this create a window of opportunity for apps to receive traffic between the time
// when the old rules are removed and the time when new rules are added. To fix this,
// make eBPF support two allowlisted interfaces so here new rules can be added before the
// old rules are being removed.
+
+ // Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to receive
+ // packets on all interfaces. This is required to accept incoming traffic in Lockdown mode
+ // by overriding the Lockdown blocking rule.
if (wasFiltering) {
mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid);
}
@@ -8041,15 +8053,14 @@
}
/**
- * Returns whether VPN isolation (ingress interface filtering) should be applied on the given
- * network.
+ * Returns the interface which requires VPN isolation (ingress interface filtering).
*
* Ingress interface filtering enforces that all apps under the given network can only receive
* packets from the network's interface (and loopback). This is important for VPNs because
* apps that cannot bypass a fully-routed VPN shouldn't be able to receive packets from any
* non-VPN interfaces.
*
- * As a result, this method should return true iff
+ * As a result, this method should return Non-null interface iff
* 1. the network is an app VPN (not legacy VPN)
* 2. the VPN does not allow bypass
* 3. the VPN is fully-routed
@@ -8058,16 +8069,32 @@
* @see INetd#firewallAddUidInterfaceRules
* @see INetd#firewallRemoveUidInterfaceRules
*/
- private boolean requiresVpnIsolation(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
+ @Nullable
+ private String getVpnIsolationInterface(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
LinkProperties lp) {
- if (nc == null || lp == null) return false;
- return nai.isVPN()
+ if (nc == null || lp == null) return null;
+ if (nai.isVPN()
&& !nai.networkAgentConfig.allowBypass
&& nc.getOwnerUid() != Process.SYSTEM_UID
&& lp.getInterfaceName() != null
&& (lp.hasIpv4DefaultRoute() || lp.hasIpv4UnreachableDefaultRoute())
&& (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute())
- && !lp.hasExcludeRoute();
+ && !lp.hasExcludeRoute()) {
+ return lp.getInterfaceName();
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether we need to set interface filtering rule or not
+ */
+ private boolean requiresVpnAllowRule(NetworkAgentInfo nai, LinkProperties lp,
+ String filterIface) {
+ // Only filter if lp has an interface.
+ if (lp == null || lp.getInterfaceName() == null) return false;
+ // Before T, allow rules are only needed if VPN isolation is enabled.
+ // T and After T, allow rules are needed for all VPNs.
+ return filterIface != null || (nai.isVPN() && SdkLevel.isAtLeastT());
}
private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -8195,9 +8222,10 @@
if (!prevRanges.isEmpty()) {
updateVpnUidRanges(false, nai, prevRanges);
}
- final boolean wasFiltering = requiresVpnIsolation(nai, prevNc, nai.linkProperties);
- final boolean shouldFilter = requiresVpnIsolation(nai, newNc, nai.linkProperties);
- final String iface = nai.linkProperties.getInterfaceName();
+ final String oldIface = getVpnIsolationInterface(nai, prevNc, nai.linkProperties);
+ final String newIface = getVpnIsolationInterface(nai, newNc, nai.linkProperties);
+ final boolean wasFiltering = requiresVpnAllowRule(nai, nai.linkProperties, oldIface);
+ final boolean shouldFilter = requiresVpnAllowRule(nai, nai.linkProperties, newIface);
// For VPN uid interface filtering, old ranges need to be removed before new ranges can
// be added, due to the range being expanded and stored as individual UIDs. For example
// the UIDs might be updated from [0, 99999] to ([0, 10012], [10014, 99999]) which means
@@ -8209,11 +8237,16 @@
// above, where the addition of new ranges happens before the removal of old ranges.
// TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range
// to be removed will never overlap with the new range to be added.
+
+ // Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to
+ // receive packets on all interfaces. This is required to accept incoming traffic in
+ // Lockdown mode by overriding the Lockdown blocking rule.
if (wasFiltering && !prevRanges.isEmpty()) {
- mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges, prevNc.getOwnerUid());
+ mPermissionMonitor.onVpnUidRangesRemoved(oldIface, prevRanges,
+ prevNc.getOwnerUid());
}
if (shouldFilter && !newRanges.isEmpty()) {
- mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges, newNc.getOwnerUid());
+ mPermissionMonitor.onVpnUidRangesAdded(newIface, newRanges, newNc.getOwnerUid());
}
} catch (Exception e) {
// Never crash!
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index 8d99cb4..e4a2c20 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -23,6 +23,9 @@
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
import static android.net.INetd.PERMISSION_INTERNET;
import static android.net.INetd.PERMISSION_NETWORK;
@@ -37,6 +40,7 @@
import static com.android.net.module.util.CollectionUtils.toIntArray;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -74,7 +78,6 @@
import com.android.server.BpfNetMaps;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -108,10 +111,19 @@
@GuardedBy("this")
private final SparseIntArray mUidToNetworkPerm = new SparseIntArray();
- // Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges
- // for apps under the VPN
+ // NonNull keys are active non-bypassable and fully-routed VPN's interface name, Values are uid
+ // ranges for apps under the VPNs which enable interface filtering.
+ // If key is null, Values are uid ranges for apps under the VPNs which are connected but do not
+ // enable interface filtering.
@GuardedBy("this")
- private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>();
+ private final Map<String, Set<UidRange>> mVpnInterfaceUidRanges = new ArrayMap<>();
+
+ // Items are uid ranges for apps under the VPN Lockdown
+ // Ranges were given through ConnectivityManager#setRequireVpnForUids, and ranges are allowed to
+ // have duplicates. Also, it is allowed to give ranges that are already subject to lockdown.
+ // So we need to maintain uid range with multiset.
+ @GuardedBy("this")
+ private final MultiSet<UidRange> mVpnLockdownUidRanges = new MultiSet<>();
// A set of appIds for apps across all users on the device. We track appIds instead of uids
// directly to reduce its size and also eliminate the need to update this set when user is
@@ -201,6 +213,38 @@
}
}
+ private static class MultiSet<T> {
+ private final Map<T, Integer> mMap = new ArrayMap<>();
+
+ /**
+ * Returns the number of key in the set before this addition.
+ */
+ public int add(T key) {
+ final int oldCount = mMap.getOrDefault(key, 0);
+ mMap.put(key, oldCount + 1);
+ return oldCount;
+ }
+
+ /**
+ * Return the number of key in the set before this removal.
+ */
+ public int remove(T key) {
+ final int oldCount = mMap.getOrDefault(key, 0);
+ if (oldCount == 0) {
+ Log.wtf(TAG, "Attempt to remove non existing key = " + key.toString());
+ } else if (oldCount == 1) {
+ mMap.remove(key);
+ } else {
+ mMap.put(key, oldCount - 1);
+ }
+ return oldCount;
+ }
+
+ public Set<T> getSet() {
+ return mMap.keySet();
+ }
+ }
+
public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
@NonNull final BpfNetMaps bpfNetMaps) {
this(context, netd, bpfNetMaps, new Dependencies());
@@ -626,16 +670,26 @@
}
private synchronized void updateVpnUid(int uid, boolean add) {
- for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+ // Apps that can use restricted networks can always bypass VPNs.
+ if (hasRestrictedNetworksPermission(uid)) {
+ return;
+ }
+ for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
if (UidRange.containsUid(vpn.getValue(), uid)) {
final Set<Integer> changedUids = new HashSet<>();
changedUids.add(uid);
- removeBypassingUids(changedUids, -1 /* vpnAppUid */);
updateVpnUidsInterfaceRules(vpn.getKey(), changedUids, add);
}
}
}
+ private synchronized void updateLockdownUid(int uid, boolean add) {
+ if (UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid)
+ && !hasRestrictedNetworksPermission(uid)) {
+ updateLockdownUidRule(uid, add);
+ }
+ }
+
/**
* This handles both network and traffic permission, because there is no overlap in actual
* values, where network permission is NETWORK or SYSTEM, and traffic permission is INTERNET
@@ -729,9 +783,10 @@
// If the newly-installed package falls within some VPN's uid range, update Netd with it.
// This needs to happen after the mUidToNetworkPerm update above, since
- // removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
- // package can bypass VPN.
+ // hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
+ // mUidToNetworkPerm to check if the package can bypass VPN.
updateVpnUid(uid, true /* add */);
+ updateLockdownUid(uid, true /* add */);
mAllApps.add(appId);
// Log package added.
@@ -775,9 +830,10 @@
// If the newly-removed package falls within some VPN's uid range, update Netd with it.
// This needs to happen before the mUidToNetworkPerm update below, since
- // removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
- // package can bypass VPN.
+ // hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
+ // mUidToNetworkPerm to check if the package can bypass VPN.
updateVpnUid(uid, false /* add */);
+ updateLockdownUid(uid, false /* add */);
// If the package has been removed from all users on the device, clear it form mAllApps.
if (mPackageManager.getNameForUid(uid) == null) {
mAllApps.remove(appId);
@@ -859,48 +915,100 @@
/**
* Called when a new set of UID ranges are added to an active VPN network
*
- * @param iface The active VPN network's interface name
+ * @param iface The active VPN network's interface name. Null iface indicates that the app is
+ * allowed to receive packets on all interfaces.
* @param rangesToAdd The new UID ranges to be added to the network
* @param vpnAppUid The uid of the VPN app
*/
- public synchronized void onVpnUidRangesAdded(@NonNull String iface, Set<UidRange> rangesToAdd,
+ public synchronized void onVpnUidRangesAdded(@Nullable String iface, Set<UidRange> rangesToAdd,
int vpnAppUid) {
// Calculate the list of new app uids under the VPN due to the new UID ranges and update
// Netd about them. Because mAllApps only contains appIds instead of uids, the result might
// be an overestimation if an app is not installed on the user on which the VPN is running,
- // but that's safe.
+ // but that's safe: if an app is not installed, it cannot receive any packets, so dropping
+ // packets to that UID is fine.
final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps);
removeBypassingUids(changedUids, vpnAppUid);
updateVpnUidsInterfaceRules(iface, changedUids, true /* add */);
- if (mVpnUidRanges.containsKey(iface)) {
- mVpnUidRanges.get(iface).addAll(rangesToAdd);
+ if (mVpnInterfaceUidRanges.containsKey(iface)) {
+ mVpnInterfaceUidRanges.get(iface).addAll(rangesToAdd);
} else {
- mVpnUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
+ mVpnInterfaceUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
}
}
/**
* Called when a set of UID ranges are removed from an active VPN network
*
- * @param iface The VPN network's interface name
+ * @param iface The VPN network's interface name. Null iface indicates that the app is allowed
+ * to receive packets on all interfaces.
* @param rangesToRemove Existing UID ranges to be removed from the VPN network
* @param vpnAppUid The uid of the VPN app
*/
- public synchronized void onVpnUidRangesRemoved(@NonNull String iface,
+ public synchronized void onVpnUidRangesRemoved(@Nullable String iface,
Set<UidRange> rangesToRemove, int vpnAppUid) {
// Calculate the list of app uids that are no longer under the VPN due to the removed UID
// ranges and update Netd about them.
final Set<Integer> changedUids = intersectUids(rangesToRemove, mAllApps);
removeBypassingUids(changedUids, vpnAppUid);
updateVpnUidsInterfaceRules(iface, changedUids, false /* add */);
- Set<UidRange> existingRanges = mVpnUidRanges.getOrDefault(iface, null);
+ Set<UidRange> existingRanges = mVpnInterfaceUidRanges.getOrDefault(iface, null);
if (existingRanges == null) {
loge("Attempt to remove unknown vpn uid Range iface = " + iface);
return;
}
existingRanges.removeAll(rangesToRemove);
if (existingRanges.size() == 0) {
- mVpnUidRanges.remove(iface);
+ mVpnInterfaceUidRanges.remove(iface);
+ }
+ }
+
+ /**
+ * Called when UID ranges under VPN Lockdown are updated
+ *
+ * @param add {@code true} if the uids are to be added to the Lockdown, {@code false} if they
+ * are to be removed from the Lockdown.
+ * @param ranges The updated UID ranges under VPN Lockdown. This function does not treat the VPN
+ * app's UID in any special way. The caller is responsible for excluding the VPN
+ * app UID from the passed-in ranges.
+ * Ranges can have duplications and/or contain the range that is already subject
+ * to lockdown. However, ranges can not have overlaps with other ranges including
+ * ranges that are currently subject to lockdown.
+ */
+ public synchronized void updateVpnLockdownUidRanges(boolean add, UidRange[] ranges) {
+ final Set<UidRange> affectedUidRanges = new HashSet<>();
+
+ for (final UidRange range : ranges) {
+ if (add) {
+ // Rule will be added if mVpnLockdownUidRanges does not have this uid range entry
+ // currently.
+ if (mVpnLockdownUidRanges.add(range) == 0) {
+ affectedUidRanges.add(range);
+ }
+ } else {
+ // Rule will be removed if the number of the range in the set is 1 before the
+ // removal.
+ if (mVpnLockdownUidRanges.remove(range) == 1) {
+ affectedUidRanges.add(range);
+ }
+ }
+ }
+
+ // mAllApps only contains appIds instead of uids. So the generated uid list might contain
+ // apps that are installed only on some users but not others. But that's safe: if an app is
+ // not installed, it cannot receive any packets, so dropping packets to that UID is fine.
+ final Set<Integer> affectedUids = intersectUids(affectedUidRanges, mAllApps);
+
+ // We skip adding rule to privileged apps and allow them to bypass incoming packet
+ // filtering. The behaviour is consistent with how lockdown works for outgoing packets, but
+ // the implementation is different: while ConnectivityService#setRequireVpnForUids does not
+ // exclude privileged apps from the prohibit routing rules used to implement outgoing packet
+ // filtering, privileged apps can still bypass outgoing packet filtering because the
+ // prohibit rules observe the protected from VPN bit.
+ for (final int uid: affectedUids) {
+ if (!hasRestrictedNetworksPermission(uid)) {
+ updateLockdownUidRule(uid, add);
+ }
}
}
@@ -939,7 +1047,7 @@
*/
private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
uids.remove(vpnAppUid);
- uids.removeIf(uid -> mUidToNetworkPerm.get(uid, PERMISSION_NONE) == PERMISSION_SYSTEM);
+ uids.removeIf(this::hasRestrictedNetworksPermission);
}
/**
@@ -948,6 +1056,7 @@
*
* This is to instruct netd to set up appropriate filtering rules for these uids, such that they
* can only receive ingress packets from the VPN's tunnel interface (and loopback).
+ * Null iface set up a wildcard rule that allow app to receive packets on all interfaces.
*
* @param iface the interface name of the active VPN connection
* @param add {@code true} if the uids are to be added to the interface, {@code false} if they
@@ -968,6 +1077,18 @@
}
}
+ private void updateLockdownUidRule(int uid, boolean add) {
+ try {
+ if (add) {
+ mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_DENY);
+ } else {
+ mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_ALLOW);
+ }
+ } catch (ServiceSpecificException e) {
+ loge("Failed to " + (add ? "add" : "remove") + " Lockdown rule: " + e);
+ }
+ }
+
/**
* Send the updated permission information to netd. Called upon package install/uninstall.
*
@@ -1055,8 +1176,14 @@
/** Should only be used by unit tests */
@VisibleForTesting
- public Set<UidRange> getVpnUidRanges(String iface) {
- return mVpnUidRanges.get(iface);
+ public Set<UidRange> getVpnInterfaceUidRanges(String iface) {
+ return mVpnInterfaceUidRanges.get(iface);
+ }
+
+ /** Should only be used by unit tests */
+ @VisibleForTesting
+ public Set<UidRange> getVpnLockdownUidRanges() {
+ return mVpnLockdownUidRanges.getSet();
}
private synchronized void onSettingChanged() {
@@ -1121,7 +1248,7 @@
public void dump(IndentingPrintWriter pw) {
pw.println("Interface filtering rules:");
pw.increaseIndent();
- for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+ for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
pw.println("Interface: " + vpn.getKey());
pw.println("UIDs: " + vpn.getValue().toString());
pw.println();
@@ -1129,6 +1256,14 @@
pw.decreaseIndent();
pw.println();
+ pw.println("Lockdown filtering rules:");
+ pw.increaseIndent();
+ for (final UidRange range : mVpnLockdownUidRanges.getSet()) {
+ pw.println("UIDs: " + range.toString());
+ }
+ pw.decreaseIndent();
+
+ pw.println();
pw.println("Update logs:");
pw.increaseIndent();
mPermissionUpdateLogs.reverseDump(pw);
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 509e881..efea0f9 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -23,7 +23,7 @@
java_library {
name: "FrameworksNetCommonTests",
- defaults: ["framework-connectivity-test-defaults"],
+ defaults: ["framework-connectivity-internal-test-defaults"],
srcs: [
"java/**/*.java",
"java/**/*.kt",
@@ -49,6 +49,7 @@
// jarjar stops at the first matching rule, so order of concatenation affects the output.
genrule {
name: "ConnectivityCoverageJarJarRules",
+ defaults: ["jarjar-rules-combine-defaults"],
srcs: [
"tethering-jni-jarjar-rules.txt",
":connectivity-jarjar-rules",
@@ -56,8 +57,6 @@
":NetworkStackJarJarRules",
],
out: ["jarjar-rules-connectivity-coverage.txt"],
- // Concat files with a line break in the middle
- cmd: "for src in $(in); do cat $${src}; echo; done > $(out)",
visibility: ["//visibility:private"],
}
@@ -84,7 +83,7 @@
target_sdk_version: "31",
test_suites: ["general-tests", "mts-tethering"],
defaults: [
- "framework-connectivity-test-defaults",
+ "framework-connectivity-internal-test-defaults",
"FrameworksNetTests-jni-defaults",
"libnetworkstackutilsjni_deps",
],
@@ -140,6 +139,30 @@
],
}
+// defaults for tests that need to build against framework-connectivity's @hide APIs, but also
+// using fully @hide classes that are jarjared (because they have no API member). Similar to
+// framework-connectivity-test-defaults above but uses pre-jarjar class names.
+// Only usable from targets that have visibility on framework-connectivity-pre-jarjar, and apply
+// connectivity jarjar rules so that references to jarjared classes still match: this is limited to
+// connectivity internal tests only.
+java_defaults {
+ name: "framework-connectivity-internal-test-defaults",
+ sdk_version: "core_platform", // tests can use @CorePlatformApi's
+ libs: [
+ // order matters: classes in framework-connectivity are resolved before framework,
+ // meaning @hide APIs in framework-connectivity are resolved before @SystemApi
+ // stubs in framework
+ "framework-connectivity-pre-jarjar",
+ "framework-connectivity-t-pre-jarjar",
+ "framework-tethering.impl",
+ "framework",
+
+ // if sdk_version="" this gets automatically included, but here we need to add manually.
+ "framework-res",
+ ],
+ defaults_visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
+}
+
// Defaults for tests that want to run in mainline-presubmit.
// Not widely used because many of our tests have AndroidTest.xml files and
// use the mainline-param config-descriptor metadata in AndroidTest.xml.
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index 3b47100..6b5bb93 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -44,7 +44,8 @@
android.permission.MANAGE_TEST_NETWORKS
-->
- <application android:usesCleartextTraffic="true">
+ <application android:debuggable="true"
+ android:usesCleartextTraffic="true">
<uses-library android:name="android.test.runner" />
<uses-library android:name="org.apache.http.legacy" android:required="false" />
</application>
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
index 1e42fe6..bbac09b 100644
--- a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -626,15 +626,31 @@
@Test
fun testParcelingDscpPolicyIsLossless(): Unit = createConnectedNetworkAgent().let {
(agent, callback) ->
+ val policyId = 1
+ val dscpValue = 1
+ val range = Range(4444, 4444)
+ val srcPort = 555
+
// Check that policy with partial parameters is lossless.
- val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)).build()
+ val policy = DscpPolicy.Builder(policyId, dscpValue).setDestinationPortRange(range).build()
+ assertEquals(policyId, policy.policyId)
+ assertEquals(dscpValue, policy.dscpValue)
+ assertEquals(range, policy.destinationPortRange)
assertParcelingIsLossless(policy)
// Check that policy with all parameters is lossless.
- val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444))
+ val policy2 = DscpPolicy.Builder(policyId, dscpValue).setDestinationPortRange(range)
.setSourceAddress(LOCAL_IPV4_ADDRESS)
.setDestinationAddress(TEST_TARGET_IPV4_ADDR)
+ .setSourcePort(srcPort)
.setProtocol(IPPROTO_UDP).build()
+ assertEquals(policyId, policy2.policyId)
+ assertEquals(dscpValue, policy2.dscpValue)
+ assertEquals(range, policy2.destinationPortRange)
+ assertEquals(TEST_TARGET_IPV4_ADDR, policy2.destinationAddress)
+ assertEquals(LOCAL_IPV4_ADDRESS, policy2.sourceAddress)
+ assertEquals(srcPort, policy2.sourcePort)
+ assertEquals(IPPROTO_UDP, policy2.protocol)
assertParcelingIsLossless(policy2)
}
}
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 7286bf6..2b1d173 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -22,6 +22,7 @@
import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.TestableNetworkCallbackKt.anyNetwork;
@@ -60,12 +61,7 @@
import com.android.internal.util.HexDump;
import com.android.networkstack.apishim.ConstantsShim;
-import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl;
-import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl;
import com.android.networkstack.apishim.VpnManagerShimImpl;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
-import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.networkstack.apishim.common.VpnManagerShim;
import com.android.networkstack.apishim.common.VpnProfileStateShim;
import com.android.testutils.DevSdkIgnoreRule;
@@ -228,22 +224,17 @@
}
private Ikev2VpnProfile buildIkev2VpnProfileCommon(
- @NonNull Ikev2VpnProfileBuilderShim builderShim, boolean isRestrictedToTestNetworks,
+ @NonNull Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks,
boolean requiresValidation) throws Exception {
- builderShim.setBypassable(true)
+ builder.setBypassable(true)
.setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
.setProxy(TEST_PROXY_INFO)
.setMaxMtu(TEST_MTU)
.setMetered(false);
if (TestUtils.shouldTestTApis()) {
- builderShim.setRequiresInternetValidation(requiresValidation);
+ builder.setRequiresInternetValidation(requiresValidation);
}
-
- // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
- // method and does not defined in shims.
- // TODO: replace it in alternative way to remove the hidden method usage
- final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
if (isRestrictedToTestNetworks) {
builder.restrictToTestNetworks();
}
@@ -259,14 +250,13 @@
? IkeSessionTestUtils.IKE_PARAMS_V6 : IkeSessionTestUtils.IKE_PARAMS_V4,
IkeSessionTestUtils.CHILD_PARAMS);
- final Ikev2VpnProfileBuilderShim builderShim =
- Ikev2VpnProfileBuilderShimImpl.newInstance(null, null, params)
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(params)
.setRequiresInternetValidation(requiresValidation)
.setProxy(TEST_PROXY_INFO)
.setMaxMtu(TEST_MTU)
.setMetered(false);
- final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
if (isRestrictedToTestNetworks) {
builder.restrictToTestNetworks();
}
@@ -275,9 +265,8 @@
private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote,
boolean isRestrictedToTestNetworks, boolean requiresValidation) throws Exception {
- final Ikev2VpnProfileBuilderShim builder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(remote, TEST_IDENTITY, null)
- .setAuthPsk(TEST_PSK);
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK);
return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
requiresValidation);
}
@@ -285,8 +274,8 @@
private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
throws Exception {
- final Ikev2VpnProfileBuilderShim builder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null)
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
.setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
false /* requiresValidation */);
@@ -294,8 +283,8 @@
private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
throws Exception {
- final Ikev2VpnProfileBuilderShim builder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null)
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
.setAuthDigitalSignature(
mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
@@ -328,15 +317,8 @@
assertNull(profile.getServerRootCaCert());
assertNull(profile.getRsaPrivateKey());
assertNull(profile.getUserCert());
- final Ikev2VpnProfileShim<Ikev2VpnProfile> shim = new Ikev2VpnProfileShimImpl(profile);
- if (TestUtils.shouldTestTApis()) {
- assertEquals(requiresValidation, shim.isInternetValidationRequired());
- } else {
- try {
- shim.isInternetValidationRequired();
- fail("Only supported from API level 33");
- } catch (UnsupportedApiLevelException expected) {
- }
+ if (isAtLeastT()) {
+ assertEquals(requiresValidation, profile.isInternetValidationRequired());
}
}
@@ -348,8 +330,8 @@
final IkeTunnelConnectionParams expectedParams = new IkeTunnelConnectionParams(
IkeSessionTestUtils.IKE_PARAMS_V6, IkeSessionTestUtils.CHILD_PARAMS);
- final Ikev2VpnProfileBuilderShim ikeProfileBuilder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(null, null, expectedParams);
+ final Ikev2VpnProfile.Builder ikeProfileBuilder =
+ new Ikev2VpnProfile.Builder(expectedParams);
// Verify the other Ike options could not be set with IkeTunnelConnectionParams.
final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
assertThrows(expected, () -> ikeProfileBuilder.setAuthPsk(TEST_PSK));
@@ -358,10 +340,9 @@
assertThrows(expected, () -> ikeProfileBuilder.setAuthDigitalSignature(
mUserCertKey.cert, mUserCertKey.key, mServerRootCa));
- final Ikev2VpnProfile profile = (Ikev2VpnProfile) ikeProfileBuilder.build().getProfile();
+ final Ikev2VpnProfile profile = ikeProfileBuilder.build();
- assertEquals(expectedParams,
- new Ikev2VpnProfileShimImpl(profile).getIkeTunnelConnectionParams());
+ assertEquals(expectedParams, profile.getIkeTunnelConnectionParams());
}
@Test
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 7b0451f..088d202 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -54,7 +54,6 @@
import com.android.net.module.util.TrackRecord
import com.android.networkstack.apishim.ConstantsShim
import com.android.networkstack.apishim.NsdShimImpl
-import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.SC_V2
import com.android.testutils.TestableNetworkAgent
import com.android.testutils.TestableNetworkCallback
@@ -65,7 +64,6 @@
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.net.ServerSocket
@@ -90,10 +88,6 @@
@AppModeFull(reason = "Socket cannot bind in instant app mode")
@RunWith(AndroidJUnit4::class)
class NsdManagerTest {
- // NsdManager is not updatable before S, so tests do not need to be backwards compatible
- @get:Rule
- val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
-
private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
private val nsdManager by lazy { context.getSystemService(NsdManager::class.java) }
@@ -399,14 +393,17 @@
si2.serviceName = serviceName
si2.port = localPort
val registrationRecord2 = NsdRegistrationRecord()
- val registeredInfo2 = registerService(registrationRecord2, si2)
+ nsdManager.registerService(si2, NsdManager.PROTOCOL_DNS_SD, registrationRecord2)
+ val registeredInfo2 = registrationRecord2.expectCallback<ServiceRegistered>().serviceInfo
// Expect a service record to be discovered (and filter the ones
// that are unrelated to this test)
val foundInfo2 = discoveryRecord.waitForServiceDiscovered(registeredInfo2.serviceName)
// Resolve the service
- val resolvedService2 = resolveService(foundInfo2)
+ val resolveRecord2 = NsdResolveRecord()
+ nsdManager.resolveService(foundInfo2, resolveRecord2)
+ val resolvedService2 = resolveRecord2.expectCallback<ServiceResolved>().serviceInfo
// Check that the resolved service doesn't have any TXT records
assertEquals(0, resolvedService2.attributes.size)
@@ -611,6 +608,41 @@
}
}
+ @Test
+ fun testNsdManager_RegisterServiceNameWithNonStandardCharacters() {
+ val serviceNames = "^Nsd.Test|Non-#AsCiI\\Characters&\\ufffe テスト 測試"
+ val si = NsdServiceInfo().apply {
+ serviceType = SERVICE_TYPE
+ serviceName = serviceNames
+ port = 12345 // Test won't try to connect so port does not matter
+ }
+
+ // Register the service name which contains non-standard characters.
+ val registrationRecord = NsdRegistrationRecord()
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
+ registrationRecord.expectCallback<ServiceRegistered>()
+
+ tryTest {
+ // Discover that service name.
+ val discoveryRecord = NsdDiscoveryRecord()
+ nsdManager.discoverServices(
+ SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord
+ )
+ val foundInfo = discoveryRecord.waitForServiceDiscovered(serviceNames)
+
+ // Expect that resolving the service name works properly even service name contains
+ // non-standard characters.
+ val resolveRecord = NsdResolveRecord()
+ nsdManager.resolveService(foundInfo, resolveRecord)
+ val resolvedCb = resolveRecord.expectCallback<ServiceResolved>()
+ assertEquals(foundInfo.serviceName, resolvedCb.serviceInfo.serviceName)
+ } cleanupStep {
+ nsdManager.unregisterService(registrationRecord)
+ } cleanup {
+ registrationRecord.expectCallback<ServiceUnregistered>()
+ }
+ }
+
/**
* Register a service and return its registration record.
*/
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 97c1265..e3d80a0 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -21,7 +21,7 @@
android_test {
name: "FrameworksNetIntegrationTests",
- defaults: ["framework-connectivity-test-defaults"],
+ defaults: ["framework-connectivity-internal-test-defaults"],
platform_apis: true,
certificate: "platform",
srcs: [
@@ -71,8 +71,12 @@
"net-tests-utils",
],
libs: [
- "service-connectivity",
+ "service-connectivity-pre-jarjar",
"services.core",
"services.net",
],
+ visibility: [
+ "//packages/modules/Connectivity/tests/integration",
+ "//packages/modules/Connectivity/tests/unit",
+ ],
}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 5b926de..5a7208c 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -108,7 +108,7 @@
name: "FrameworksNetTestsDefaults",
min_sdk_version: "30",
defaults: [
- "framework-connectivity-test-defaults",
+ "framework-connectivity-internal-test-defaults",
],
srcs: [
"java/**/*.java",
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 3374672..52cebec 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -51,6 +51,9 @@
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK;
@@ -9502,6 +9505,46 @@
b2.expectBroadcast();
}
+ @Test
+ public void testLockdownSetFirewallUidRule() throws Exception {
+ // For ConnectivityService#setAlwaysOnVpnPackage.
+ mServiceContext.setPermission(
+ Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED);
+ // Needed to call Vpn#setAlwaysOnPackage.
+ mServiceContext.setPermission(Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
+ // Needed to call Vpn#isAlwaysOnPackageSupported.
+ mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+ // Enable Lockdown
+ final ArrayList<String> allowList = new ArrayList<>();
+ mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, ALWAYS_ON_PACKAGE,
+ true /* lockdown */, allowList);
+ waitForIdle();
+
+ // Lockdown rule is set to apps uids
+ verify(mBpfNetMaps).setUidRule(
+ eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_DENY));
+ verify(mBpfNetMaps).setUidRule(
+ eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_DENY));
+
+ reset(mBpfNetMaps);
+
+ // Disable lockdown
+ mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, null, false /* lockdown */,
+ allowList);
+ waitForIdle();
+
+ // Lockdown rule is removed from apps uids
+ verify(mBpfNetMaps).setUidRule(
+ eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_ALLOW));
+ verify(mBpfNetMaps).setUidRule(
+ eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_ALLOW));
+
+ // Interface rules are not changed by Lockdown mode enable/disable
+ verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+ verify(mBpfNetMaps, never()).removeUidInterfaceRules(any());
+ }
+
/**
* Test mutable and requestable network capabilities such as
* {@link NetworkCapabilities#NET_CAPABILITY_TRUSTED} and
@@ -10373,7 +10416,7 @@
verify(mBpfNetMaps, times(2)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture());
assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID);
assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID);
- assertTrue(mService.mPermissionMonitor.getVpnUidRanges("tun0").equals(vpnRange));
+ assertTrue(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0").equals(vpnRange));
mMockVpn.disconnect();
waitForIdle();
@@ -10381,11 +10424,11 @@
// Disconnected VPN should have interface rules removed
verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
- assertNull(mService.mPermissionMonitor.getVpnUidRanges("tun0"));
+ assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0"));
}
@Test
- public void testLegacyVpnDoesNotResultInInterfaceFilteringRule() throws Exception {
+ public void testLegacyVpnSetInterfaceFilteringRuleWithWildcard() throws Exception {
LinkProperties lp = new LinkProperties();
lp.setInterfaceName("tun0");
lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
@@ -10395,13 +10438,29 @@
mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
- // Legacy VPN should not have interface rules set up
- verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+ // A connected Legacy VPN should have interface rules with null interface.
+ // Null Interface is a wildcard and this accepts traffic from all the interfaces.
+ // There are two expected invocations, one during the VPN initial connection,
+ // one during the VPN LinkProperties update.
+ ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
+ verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
+ eq(null) /* iface */, uidCaptor.capture());
+ assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
+ assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+ assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
+ vpnRange);
+
+ mMockVpn.disconnect();
+ waitForIdle();
+
+ // Disconnected VPN should have interface rules removed
+ verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
+ assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+ assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
}
@Test
- public void testLocalIpv4OnlyVpnDoesNotResultInInterfaceFilteringRule()
- throws Exception {
+ public void testLocalIpv4OnlyVpnSetInterfaceFilteringRuleWithWildcard() throws Exception {
LinkProperties lp = new LinkProperties();
lp.setInterfaceName("tun0");
lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0"));
@@ -10412,7 +10471,25 @@
assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
// IPv6 unreachable route should not be misinterpreted as a default route
- verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+ // A connected VPN should have interface rules with null interface.
+ // Null Interface is a wildcard and this accepts traffic from all the interfaces.
+ // There are two expected invocations, one during the VPN initial connection,
+ // one during the VPN LinkProperties update.
+ ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
+ verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
+ eq(null) /* iface */, uidCaptor.capture());
+ assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
+ assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+ assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
+ vpnRange);
+
+ mMockVpn.disconnect();
+ waitForIdle();
+
+ // Disconnected VPN should have interface rules removed
+ verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
+ assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+ assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index fb821c3..ecd17ba 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -30,6 +30,9 @@
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
import static android.net.INetd.PERMISSION_INTERNET;
import static android.net.INetd.PERMISSION_NETWORK;
@@ -761,8 +764,8 @@
MOCK_APPID1);
}
- @Test
- public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
+ private void doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
+ throws Exception {
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
@@ -778,8 +781,8 @@
final Set<UidRange> vpnRange2 = Set.of(new UidRange(MOCK_UID12, MOCK_UID12));
// When VPN is connected, expect a rule to be set up for user app MOCK_UID11
- mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange1, VPN_UID);
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
+ mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange1, VPN_UID);
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
reset(mBpfNetMaps);
@@ -787,27 +790,38 @@
mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID11}));
mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_UID11);
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
reset(mBpfNetMaps);
// During VPN uid update (vpnRange1 -> vpnRange2), ConnectivityService first deletes the
// old UID rules then adds the new ones. Expect netd to be updated
- mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange1, VPN_UID);
+ mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange1, VPN_UID);
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID11}));
- mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange2, VPN_UID);
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID12}));
+ mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange2, VPN_UID);
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID12}));
reset(mBpfNetMaps);
// When VPN is disconnected, expect rules to be torn down
- mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange2, VPN_UID);
+ mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange2, VPN_UID);
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID12}));
- assertNull(mPermissionMonitor.getVpnUidRanges("tun0"));
+ assertNull(mPermissionMonitor.getVpnInterfaceUidRanges(ifName));
}
@Test
- public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
+ public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
+ doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
+ }
+
+ @Test
+ public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdatesWithWildcard()
+ throws Exception {
+ doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
+ }
+
+ private void doTestUidFilteringDuringPackageInstallAndUninstall(@Nullable String ifName) throws
+ Exception {
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
@@ -818,12 +832,12 @@
mPermissionMonitor.startMonitoring();
final Set<UidRange> vpnRange = Set.of(UidRange.createForUser(MOCK_USER1),
UidRange.createForUser(MOCK_USER2));
- mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange, VPN_UID);
+ mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange, VPN_UID);
// Newly-installed package should have uid rules added
addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID21}));
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID21}));
// Removed package should have its uid rules removed
mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
@@ -831,6 +845,168 @@
verify(mBpfNetMaps, never()).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID21}));
}
+ @Test
+ public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
+ doTestUidFilteringDuringPackageInstallAndUninstall("tun0");
+ }
+
+ @Test
+ public void testUidFilteringDuringPackageInstallAndUninstallWithWildcard() throws Exception {
+ doTestUidFilteringDuringPackageInstallAndUninstall(null /* ifName */);
+ }
+
+ @Test
+ public void testLockdownUidFilteringWithLockdownEnableDisable() {
+ doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+ CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+ buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+ buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
+ buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+ .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+ mPermissionMonitor.startMonitoring();
+ // Every app on user 0 except MOCK_UID12 are under VPN.
+ final UidRange[] vpnRange1 = {
+ new UidRange(0, MOCK_UID12 - 1),
+ new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1)
+ };
+
+ // Add Lockdown uid range, expect a rule to be set up for user app MOCK_UID11
+ mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange1);
+ verify(mBpfNetMaps)
+ .setUidRule(
+ eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+ eq(FIREWALL_RULE_DENY));
+ assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange1));
+
+ reset(mBpfNetMaps);
+
+ // Remove Lockdown uid range, expect rules to be torn down
+ mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange1);
+ verify(mBpfNetMaps)
+ .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+ eq(FIREWALL_RULE_ALLOW));
+ assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+ }
+
+ @Test
+ public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
+ doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+ CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+ buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+ buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+ .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+ mPermissionMonitor.startMonitoring();
+ // MOCK_UID11 is under VPN.
+ final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
+ final UidRange[] vpnRange = {range};
+
+ // Add Lockdown uid range at 1st time, expect a rule to be set up
+ mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
+ verify(mBpfNetMaps)
+ .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+ eq(FIREWALL_RULE_DENY));
+ assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+ reset(mBpfNetMaps);
+
+ // Add Lockdown uid range at 2nd time, expect a rule not to be set up because the uid
+ // already has the rule
+ mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
+ verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
+ assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+ reset(mBpfNetMaps);
+
+ // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because we added
+ // the range 2 times.
+ mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
+ verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
+ assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+ reset(mBpfNetMaps);
+
+ // Remove Lockdown uid range at 2nd time, expect a rule to be torn down because we added
+ // twice and we removed twice.
+ mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
+ verify(mBpfNetMaps)
+ .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+ eq(FIREWALL_RULE_ALLOW));
+ assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+ }
+
+ @Test
+ public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
+ doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+ CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+ buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+ buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+ .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+ mPermissionMonitor.startMonitoring();
+ // MOCK_UID11 is under VPN.
+ final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
+ final UidRange[] vpnRangeDuplicates = {range, range};
+ final UidRange[] vpnRange = {range};
+
+ // Add Lockdown uid ranges which contains duplicated uid ranges
+ mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRangeDuplicates);
+ verify(mBpfNetMaps)
+ .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+ eq(FIREWALL_RULE_DENY));
+ assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+ reset(mBpfNetMaps);
+
+ // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because uid
+ // ranges we added contains duplicated uid ranges.
+ mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
+ verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
+ assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+
+ reset(mBpfNetMaps);
+
+ // Remove Lockdown uid range at 2nd time, expect a rule to be torn down.
+ mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
+ verify(mBpfNetMaps)
+ .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+ eq(FIREWALL_RULE_ALLOW));
+ assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
+ }
+
+ @Test
+ public void testLockdownUidFilteringWithInstallAndUnInstall() {
+ doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+ NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
+ buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
+ .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
+ doReturn(List.of(MOCK_USER1, MOCK_USER2)).when(mUserManager).getUserHandles(eq(true));
+
+ mPermissionMonitor.startMonitoring();
+ final UidRange[] vpnRange = {
+ UidRange.createForUser(MOCK_USER1),
+ UidRange.createForUser(MOCK_USER2)
+ };
+ mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
+
+ // Installing package should add Lockdown rules
+ addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
+ verify(mBpfNetMaps)
+ .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+ eq(FIREWALL_RULE_DENY));
+ verify(mBpfNetMaps)
+ .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
+ eq(FIREWALL_RULE_DENY));
+
+ reset(mBpfNetMaps);
+
+ // Uninstalling package should remove Lockdown rules
+ mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
+ verify(mBpfNetMaps)
+ .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
+ eq(FIREWALL_RULE_ALLOW));
+ verify(mBpfNetMaps, never())
+ .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
+ eq(FIREWALL_RULE_ALLOW));
+ }
// Normal package add/remove operations will trigger multiple intent for uids corresponding to
// each user. To simulate generic package operations, the onPackageAdded/Removed will need to be
diff --git a/tools/Android.bp b/tools/Android.bp
new file mode 100644
index 0000000..27f9b75
--- /dev/null
+++ b/tools/Android.bp
@@ -0,0 +1,46 @@
+//
+// Copyright (C) 2022 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 {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Build tool used to generate jarjar rules for all classes in a jar, except those that are
+// API, UnsupportedAppUsage or otherwise excluded.
+python_binary_host {
+ name: "jarjar-rules-generator",
+ srcs: [
+ "gen_jarjar.py",
+ ],
+ main: "gen_jarjar.py",
+ version: {
+ py2: {
+ enabled: false,
+ },
+ py3: {
+ enabled: true,
+ },
+ },
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+genrule_defaults {
+ name: "jarjar-rules-combine-defaults",
+ // Concat files with a line break in the middle
+ cmd: "for src in $(in); do cat $${src}; echo; done > $(out)",
+ defaults_visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/tools/gen_jarjar.py b/tools/gen_jarjar.py
new file mode 100755
index 0000000..285bf6f
--- /dev/null
+++ b/tools/gen_jarjar.py
@@ -0,0 +1,168 @@
+#
+# Copyright (C) 2022 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.
+
+""" This script generates jarjar rule files to add a jarjar prefix to all classes, except those
+that are API, unsupported API or otherwise excluded."""
+
+import argparse
+import io
+import re
+import subprocess
+from xml import sax
+from xml.sax.handler import ContentHandler
+from zipfile import ZipFile
+
+
+def parse_arguments(argv):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '--jars', nargs='+',
+ help='Path to pre-jarjar JAR. Can be followed by multiple space-separated paths.')
+ parser.add_argument(
+ '--prefix', required=True,
+ help='Package prefix to use for jarjared classes, '
+ 'for example "com.android.connectivity" (does not end with a dot).')
+ parser.add_argument(
+ '--output', required=True, help='Path to output jarjar rules file.')
+ parser.add_argument(
+ '--apistubs', nargs='*', default=[],
+ help='Path to API stubs jar. Classes that are API will not be jarjared. Can be followed by '
+ 'multiple space-separated paths.')
+ parser.add_argument(
+ '--unsupportedapi', nargs='*', default=[],
+ help='Path to UnsupportedAppUsage hidden API .txt lists. '
+ 'Classes that have UnsupportedAppUsage API will not be jarjared. Can be followed by '
+ 'multiple space-separated paths.')
+ parser.add_argument(
+ '--excludes', nargs='*', default=[],
+ help='Path to files listing classes that should not be jarjared. Can be followed by '
+ 'multiple space-separated paths. '
+ 'Each file should contain one full-match regex per line. Empty lines or lines '
+ 'starting with "#" are ignored.')
+ parser.add_argument(
+ '--dexdump', default='dexdump', help='Path to dexdump binary.')
+ return parser.parse_args(argv)
+
+
+class DumpHandler(ContentHandler):
+ def __init__(self):
+ super().__init__()
+ self._current_package = None
+ self.classes = []
+
+ def startElement(self, name, attrs):
+ if name == 'package':
+ attr_name = attrs.getValue('name')
+ assert attr_name != '', '<package> element missing name'
+ assert self._current_package is None, f'Found nested package tags for {attr_name}'
+ self._current_package = attr_name
+ elif name == 'class':
+ attr_name = attrs.getValue('name')
+ assert attr_name != '', '<class> element missing name'
+ self.classes.append(self._current_package + '.' + attr_name)
+
+ def endElement(self, name):
+ if name == 'package':
+ self._current_package = None
+
+
+def _list_toplevel_dex_classes(jar, dexdump):
+ """List all classes in a dexed .jar file that are not inner classes."""
+ # Empty jars do net get a classes.dex: return an empty set for them
+ with ZipFile(jar, 'r') as zip_file:
+ if not zip_file.namelist():
+ return set()
+ cmd = [dexdump, '-l', 'xml', '-e', jar]
+ dump = subprocess.run(cmd, check=True, text=True, stdout=subprocess.PIPE)
+ handler = DumpHandler()
+ xml_parser = sax.make_parser()
+ xml_parser.setContentHandler(handler)
+ xml_parser.parse(io.StringIO(dump.stdout))
+ return set([_get_toplevel_class(c) for c in handler.classes])
+
+
+def _list_jar_classes(jar):
+ with ZipFile(jar, 'r') as zip:
+ files = zip.namelist()
+ assert 'classes.dex' not in files, f'Jar file {jar} is dexed, ' \
+ 'expected an intermediate zip of .class files'
+ class_len = len('.class')
+ return [f.replace('/', '.')[:-class_len] for f in files
+ if f.endswith('.class') and not f.endswith('/package-info.class')]
+
+
+def _list_hiddenapi_classes(txt_file):
+ out = set()
+ with open(txt_file, 'r') as f:
+ for line in f:
+ if not line.strip():
+ continue
+ assert line.startswith('L') and ';' in line, f'Class name not recognized: {line}'
+ clazz = line.replace('/', '.').split(';')[0][1:]
+ out.add(_get_toplevel_class(clazz))
+ return out
+
+
+def _get_toplevel_class(clazz):
+ """Return the name of the toplevel (not an inner class) enclosing class of the given class."""
+ if '$' not in clazz:
+ return clazz
+ return clazz.split('$')[0]
+
+
+def _get_excludes(path):
+ out = []
+ with open(path, 'r') as f:
+ for line in f:
+ stripped = line.strip()
+ if not stripped or stripped.startswith('#'):
+ continue
+ out.append(re.compile(stripped))
+ return out
+
+
+def make_jarjar_rules(args):
+ excluded_classes = set()
+ for apistubs_file in args.apistubs:
+ excluded_classes.update(_list_toplevel_dex_classes(apistubs_file, args.dexdump))
+
+ for unsupportedapi_file in args.unsupportedapi:
+ excluded_classes.update(_list_hiddenapi_classes(unsupportedapi_file))
+
+ exclude_regexes = []
+ for exclude_file in args.excludes:
+ exclude_regexes.extend(_get_excludes(exclude_file))
+
+ with open(args.output, 'w') as outfile:
+ for jar in args.jars:
+ jar_classes = _list_jar_classes(jar)
+ jar_classes.sort()
+ for clazz in jar_classes:
+ if (_get_toplevel_class(clazz) not in excluded_classes and
+ not any(r.fullmatch(clazz) for r in exclude_regexes)):
+ outfile.write(f'rule {clazz} {args.prefix}.@0\n')
+ # Also include jarjar rules for unit tests of the class, so the package matches
+ outfile.write(f'rule {clazz}Test {args.prefix}.@0\n')
+ outfile.write(f'rule {clazz}Test$* {args.prefix}.@0\n')
+
+
+def _main():
+ # Pass in None to use argv
+ args = parse_arguments(None)
+ make_jarjar_rules(args)
+
+
+if __name__ == '__main__':
+ _main()