Merge "Increase the number of test service names"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 6996ad9..780ba26 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -23,6 +23,12 @@
},
{
"name": "TetheringIntegrationTests"
+ },
+ {
+ "name": "traffic_controller_unit_test"
+ },
+ {
+ "name": "libnetworkstats_test"
}
],
"postsubmit": [
@@ -32,6 +38,16 @@
// TODO: move to presubmit when known green.
{
"name": "bpf_existence_test"
+ },
+ {
+ "name": "libclat_test"
+ },
+ {
+ "name": "traffic_controller_unit_test",
+ "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
+ },
+ {
+ "name": "libnetworkstats_test"
}
],
"mainline-presubmit": [
@@ -48,6 +64,12 @@
},
{
"name": "ConnectivityCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+ },
+ {
+ "name": "traffic_controller_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+ },
+ {
+ "name": "libnetworkstats_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
}
],
"mainline-postsubmit": [
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 0e04dbe..ecbaf61 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -53,6 +53,7 @@
"framework-statsd.stubs.module_lib",
"framework-tethering.impl",
"framework-wifi",
+ "framework-bluetooth",
"unsupportedappusage",
],
plugins: ["java_api_finder"],
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index c72d3a6..53a14ad 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -31,6 +31,7 @@
// package names and keys, so that apex will be unused anyway.
apps: ["TetheringNext"], // Replace to "Tethering" if ConnectivityNextEnableDefaults is false.
}
+enable_tethering_next_apex = true
// This is a placeholder comment to avoid merge conflicts
// as the above target may have different "enabled" values
// depending on the branch
@@ -51,7 +52,8 @@
first: {
jni_libs: [
"libservice-connectivity",
- "libcom_android_connectivity_com_android_net_module_util_jni"
+ "libcom_android_connectivity_com_android_net_module_util_jni",
+ "libtraffic_controller_jni",
],
},
both: {
@@ -63,6 +65,9 @@
],
canned_fs_config: "canned_fs_config",
bpfs: [
+ "clatd.o_mainline",
+ "netd.o_mainline",
+ "dscp_policy.o",
"offload.o",
"test.o",
],
@@ -117,8 +122,13 @@
// Additional hidden API flag files to override the defaults. This must only be
// modified by the Soong or platform compat team.
hidden_api: {
- max_target_r_low_priority: ["hiddenapi/hiddenapi-max-target-r-loprio.txt"],
- max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+ max_target_r_low_priority: [
+ "hiddenapi/hiddenapi-max-target-r-loprio.txt",
+ ],
+ max_target_o_low_priority: [
+ "hiddenapi/hiddenapi-max-target-o-low-priority.txt",
+ "hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt",
+ ],
unsupported: ["hiddenapi/hiddenapi-unsupported.txt"],
},
}
@@ -133,6 +143,7 @@
name: "com.android.tethering.inprocess",
base: "com.android.tethering",
package_name: "com.android.tethering.inprocess",
+ enabled: enable_tethering_next_apex,
apps: [
"ServiceConnectivityResources",
"InProcessTethering",
diff --git a/Tethering/apex/canned_fs_config b/Tethering/apex/canned_fs_config
index 44c57ab..06e9617 100644
--- a/Tethering/apex/canned_fs_config
+++ b/Tethering/apex/canned_fs_config
@@ -1,2 +1,2 @@
/bin/for-system 0 1000 0550
-/bin/for-system/clatd 1029 1029 06555
+/bin/for-system/clatd 1029 1029 06755
diff --git a/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt b/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt
new file mode 100644
index 0000000..88c77f2
--- /dev/null
+++ b/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt
@@ -0,0 +1,87 @@
+Landroid/net/nsd/DnsSdTxtRecord;-><init>()V
+Landroid/net/nsd/DnsSdTxtRecord;-><init>(Landroid/net/nsd/DnsSdTxtRecord;)V
+Landroid/net/nsd/DnsSdTxtRecord;-><init>([B)V
+Landroid/net/nsd/DnsSdTxtRecord;->contains(Ljava/lang/String;)Z
+Landroid/net/nsd/DnsSdTxtRecord;->CREATOR:Landroid/os/Parcelable$Creator;
+Landroid/net/nsd/DnsSdTxtRecord;->get(Ljava/lang/String;)Ljava/lang/String;
+Landroid/net/nsd/DnsSdTxtRecord;->getKey(I)Ljava/lang/String;
+Landroid/net/nsd/DnsSdTxtRecord;->getRawData()[B
+Landroid/net/nsd/DnsSdTxtRecord;->getValue(I)[B
+Landroid/net/nsd/DnsSdTxtRecord;->getValue(Ljava/lang/String;)[B
+Landroid/net/nsd/DnsSdTxtRecord;->getValueAsString(I)Ljava/lang/String;
+Landroid/net/nsd/DnsSdTxtRecord;->insert([B[BI)V
+Landroid/net/nsd/DnsSdTxtRecord;->keyCount()I
+Landroid/net/nsd/DnsSdTxtRecord;->mData:[B
+Landroid/net/nsd/DnsSdTxtRecord;->mSeperator:B
+Landroid/net/nsd/DnsSdTxtRecord;->remove(Ljava/lang/String;)I
+Landroid/net/nsd/DnsSdTxtRecord;->set(Ljava/lang/String;Ljava/lang/String;)V
+Landroid/net/nsd/DnsSdTxtRecord;->size()I
+Landroid/net/nsd/INsdManager$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
+Landroid/net/nsd/INsdManager$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
+Landroid/net/nsd/INsdManager$Stub$Proxy;->getMessenger()Landroid/os/Messenger;
+Landroid/net/nsd/INsdManager$Stub$Proxy;->mRemote:Landroid/os/IBinder;
+Landroid/net/nsd/INsdManager$Stub$Proxy;->setEnabled(Z)V
+Landroid/net/nsd/INsdManager$Stub;-><init>()V
+Landroid/net/nsd/INsdManager$Stub;->DESCRIPTOR:Ljava/lang/String;
+Landroid/net/nsd/INsdManager$Stub;->TRANSACTION_getMessenger:I
+Landroid/net/nsd/INsdManager$Stub;->TRANSACTION_setEnabled:I
+Landroid/net/nsd/INsdManager;->setEnabled(Z)V
+Landroid/net/nsd/NsdManager;-><init>(Landroid/content/Context;Landroid/net/nsd/INsdManager;)V
+Landroid/net/nsd/NsdManager;->BASE:I
+Landroid/net/nsd/NsdManager;->checkListener(Ljava/lang/Object;)V
+Landroid/net/nsd/NsdManager;->checkProtocol(I)V
+Landroid/net/nsd/NsdManager;->checkServiceInfo(Landroid/net/nsd/NsdServiceInfo;)V
+Landroid/net/nsd/NsdManager;->DBG:Z
+Landroid/net/nsd/NsdManager;->DISABLE:I
+Landroid/net/nsd/NsdManager;->disconnect()V
+Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES:I
+Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES_FAILED:I
+Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES_STARTED:I
+Landroid/net/nsd/NsdManager;->ENABLE:I
+Landroid/net/nsd/NsdManager;->EVENT_NAMES:Landroid/util/SparseArray;
+Landroid/net/nsd/NsdManager;->fatal(Ljava/lang/String;)V
+Landroid/net/nsd/NsdManager;->FIRST_LISTENER_KEY:I
+Landroid/net/nsd/NsdManager;->getListenerKey(Ljava/lang/Object;)I
+Landroid/net/nsd/NsdManager;->getMessenger()Landroid/os/Messenger;
+Landroid/net/nsd/NsdManager;->getNsdServiceInfoType(Landroid/net/nsd/NsdServiceInfo;)Ljava/lang/String;
+Landroid/net/nsd/NsdManager;->init()V
+Landroid/net/nsd/NsdManager;->mAsyncChannel:Lcom/android/internal/util/AsyncChannel;
+Landroid/net/nsd/NsdManager;->mConnected:Ljava/util/concurrent/CountDownLatch;
+Landroid/net/nsd/NsdManager;->mContext:Landroid/content/Context;
+Landroid/net/nsd/NsdManager;->mHandler:Landroid/net/nsd/NsdManager$ServiceHandler;
+Landroid/net/nsd/NsdManager;->mListenerKey:I
+Landroid/net/nsd/NsdManager;->mListenerMap:Landroid/util/SparseArray;
+Landroid/net/nsd/NsdManager;->mMapLock:Ljava/lang/Object;
+Landroid/net/nsd/NsdManager;->mService:Landroid/net/nsd/INsdManager;
+Landroid/net/nsd/NsdManager;->mServiceMap:Landroid/util/SparseArray;
+Landroid/net/nsd/NsdManager;->nameOf(I)Ljava/lang/String;
+Landroid/net/nsd/NsdManager;->NATIVE_DAEMON_EVENT:I
+Landroid/net/nsd/NsdManager;->nextListenerKey()I
+Landroid/net/nsd/NsdManager;->putListener(Ljava/lang/Object;Landroid/net/nsd/NsdServiceInfo;)I
+Landroid/net/nsd/NsdManager;->REGISTER_SERVICE:I
+Landroid/net/nsd/NsdManager;->REGISTER_SERVICE_FAILED:I
+Landroid/net/nsd/NsdManager;->REGISTER_SERVICE_SUCCEEDED:I
+Landroid/net/nsd/NsdManager;->removeListener(I)V
+Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE:I
+Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE_FAILED:I
+Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE_SUCCEEDED:I
+Landroid/net/nsd/NsdManager;->SERVICE_FOUND:I
+Landroid/net/nsd/NsdManager;->SERVICE_LOST:I
+Landroid/net/nsd/NsdManager;->setEnabled(Z)V
+Landroid/net/nsd/NsdManager;->STOP_DISCOVERY:I
+Landroid/net/nsd/NsdManager;->STOP_DISCOVERY_FAILED:I
+Landroid/net/nsd/NsdManager;->STOP_DISCOVERY_SUCCEEDED:I
+Landroid/net/nsd/NsdManager;->TAG:Ljava/lang/String;
+Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE:I
+Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE_FAILED:I
+Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE_SUCCEEDED:I
+Landroid/net/nsd/NsdServiceInfo;-><init>(Ljava/lang/String;Ljava/lang/String;)V
+Landroid/net/nsd/NsdServiceInfo;->getTxtRecord()[B
+Landroid/net/nsd/NsdServiceInfo;->getTxtRecordSize()I
+Landroid/net/nsd/NsdServiceInfo;->mHost:Ljava/net/InetAddress;
+Landroid/net/nsd/NsdServiceInfo;->mPort:I
+Landroid/net/nsd/NsdServiceInfo;->mServiceName:Ljava/lang/String;
+Landroid/net/nsd/NsdServiceInfo;->mServiceType:Ljava/lang/String;
+Landroid/net/nsd/NsdServiceInfo;->mTxtRecord:Landroid/util/ArrayMap;
+Landroid/net/nsd/NsdServiceInfo;->setTxtRecords(Ljava/lang/String;)V
+Landroid/net/nsd/NsdServiceInfo;->TAG:Ljava/lang/String;
diff --git a/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority.txt b/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority.txt
index ea0f61a..1f49d1b 100644
--- a/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority.txt
+++ b/Tethering/apex/hiddenapi/hiddenapi-max-target-o-low-priority.txt
@@ -1163,93 +1163,6 @@
Landroid/net/NetworkWatchlistManager;->reportWatchlistIfNecessary()V
Landroid/net/NetworkWatchlistManager;->SHARED_MEMORY_TAG:Ljava/lang/String;
Landroid/net/NetworkWatchlistManager;->TAG:Ljava/lang/String;
-Landroid/net/nsd/DnsSdTxtRecord;-><init>()V
-Landroid/net/nsd/DnsSdTxtRecord;-><init>(Landroid/net/nsd/DnsSdTxtRecord;)V
-Landroid/net/nsd/DnsSdTxtRecord;-><init>([B)V
-Landroid/net/nsd/DnsSdTxtRecord;->contains(Ljava/lang/String;)Z
-Landroid/net/nsd/DnsSdTxtRecord;->CREATOR:Landroid/os/Parcelable$Creator;
-Landroid/net/nsd/DnsSdTxtRecord;->get(Ljava/lang/String;)Ljava/lang/String;
-Landroid/net/nsd/DnsSdTxtRecord;->getKey(I)Ljava/lang/String;
-Landroid/net/nsd/DnsSdTxtRecord;->getRawData()[B
-Landroid/net/nsd/DnsSdTxtRecord;->getValue(I)[B
-Landroid/net/nsd/DnsSdTxtRecord;->getValue(Ljava/lang/String;)[B
-Landroid/net/nsd/DnsSdTxtRecord;->getValueAsString(I)Ljava/lang/String;
-Landroid/net/nsd/DnsSdTxtRecord;->insert([B[BI)V
-Landroid/net/nsd/DnsSdTxtRecord;->keyCount()I
-Landroid/net/nsd/DnsSdTxtRecord;->mData:[B
-Landroid/net/nsd/DnsSdTxtRecord;->mSeperator:B
-Landroid/net/nsd/DnsSdTxtRecord;->remove(Ljava/lang/String;)I
-Landroid/net/nsd/DnsSdTxtRecord;->set(Ljava/lang/String;Ljava/lang/String;)V
-Landroid/net/nsd/DnsSdTxtRecord;->size()I
-Landroid/net/nsd/INsdManager$Stub$Proxy;-><init>(Landroid/os/IBinder;)V
-Landroid/net/nsd/INsdManager$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String;
-Landroid/net/nsd/INsdManager$Stub$Proxy;->getMessenger()Landroid/os/Messenger;
-Landroid/net/nsd/INsdManager$Stub$Proxy;->mRemote:Landroid/os/IBinder;
-Landroid/net/nsd/INsdManager$Stub$Proxy;->setEnabled(Z)V
-Landroid/net/nsd/INsdManager$Stub;-><init>()V
-Landroid/net/nsd/INsdManager$Stub;->DESCRIPTOR:Ljava/lang/String;
-Landroid/net/nsd/INsdManager$Stub;->TRANSACTION_getMessenger:I
-Landroid/net/nsd/INsdManager$Stub;->TRANSACTION_setEnabled:I
-Landroid/net/nsd/INsdManager;->setEnabled(Z)V
-Landroid/net/nsd/NsdManager;-><init>(Landroid/content/Context;Landroid/net/nsd/INsdManager;)V
-Landroid/net/nsd/NsdManager;->BASE:I
-Landroid/net/nsd/NsdManager;->checkListener(Ljava/lang/Object;)V
-Landroid/net/nsd/NsdManager;->checkProtocol(I)V
-Landroid/net/nsd/NsdManager;->checkServiceInfo(Landroid/net/nsd/NsdServiceInfo;)V
-Landroid/net/nsd/NsdManager;->DBG:Z
-Landroid/net/nsd/NsdManager;->DISABLE:I
-Landroid/net/nsd/NsdManager;->disconnect()V
-Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES:I
-Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES_FAILED:I
-Landroid/net/nsd/NsdManager;->DISCOVER_SERVICES_STARTED:I
-Landroid/net/nsd/NsdManager;->ENABLE:I
-Landroid/net/nsd/NsdManager;->EVENT_NAMES:Landroid/util/SparseArray;
-Landroid/net/nsd/NsdManager;->fatal(Ljava/lang/String;)V
-Landroid/net/nsd/NsdManager;->FIRST_LISTENER_KEY:I
-Landroid/net/nsd/NsdManager;->getListenerKey(Ljava/lang/Object;)I
-Landroid/net/nsd/NsdManager;->getMessenger()Landroid/os/Messenger;
-Landroid/net/nsd/NsdManager;->getNsdServiceInfoType(Landroid/net/nsd/NsdServiceInfo;)Ljava/lang/String;
-Landroid/net/nsd/NsdManager;->init()V
-Landroid/net/nsd/NsdManager;->mAsyncChannel:Lcom/android/internal/util/AsyncChannel;
-Landroid/net/nsd/NsdManager;->mConnected:Ljava/util/concurrent/CountDownLatch;
-Landroid/net/nsd/NsdManager;->mContext:Landroid/content/Context;
-Landroid/net/nsd/NsdManager;->mHandler:Landroid/net/nsd/NsdManager$ServiceHandler;
-Landroid/net/nsd/NsdManager;->mListenerKey:I
-Landroid/net/nsd/NsdManager;->mListenerMap:Landroid/util/SparseArray;
-Landroid/net/nsd/NsdManager;->mMapLock:Ljava/lang/Object;
-Landroid/net/nsd/NsdManager;->mService:Landroid/net/nsd/INsdManager;
-Landroid/net/nsd/NsdManager;->mServiceMap:Landroid/util/SparseArray;
-Landroid/net/nsd/NsdManager;->nameOf(I)Ljava/lang/String;
-Landroid/net/nsd/NsdManager;->NATIVE_DAEMON_EVENT:I
-Landroid/net/nsd/NsdManager;->nextListenerKey()I
-Landroid/net/nsd/NsdManager;->putListener(Ljava/lang/Object;Landroid/net/nsd/NsdServiceInfo;)I
-Landroid/net/nsd/NsdManager;->REGISTER_SERVICE:I
-Landroid/net/nsd/NsdManager;->REGISTER_SERVICE_FAILED:I
-Landroid/net/nsd/NsdManager;->REGISTER_SERVICE_SUCCEEDED:I
-Landroid/net/nsd/NsdManager;->removeListener(I)V
-Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE:I
-Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE_FAILED:I
-Landroid/net/nsd/NsdManager;->RESOLVE_SERVICE_SUCCEEDED:I
-Landroid/net/nsd/NsdManager;->SERVICE_FOUND:I
-Landroid/net/nsd/NsdManager;->SERVICE_LOST:I
-Landroid/net/nsd/NsdManager;->setEnabled(Z)V
-Landroid/net/nsd/NsdManager;->STOP_DISCOVERY:I
-Landroid/net/nsd/NsdManager;->STOP_DISCOVERY_FAILED:I
-Landroid/net/nsd/NsdManager;->STOP_DISCOVERY_SUCCEEDED:I
-Landroid/net/nsd/NsdManager;->TAG:Ljava/lang/String;
-Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE:I
-Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE_FAILED:I
-Landroid/net/nsd/NsdManager;->UNREGISTER_SERVICE_SUCCEEDED:I
-Landroid/net/nsd/NsdServiceInfo;-><init>(Ljava/lang/String;Ljava/lang/String;)V
-Landroid/net/nsd/NsdServiceInfo;->getTxtRecord()[B
-Landroid/net/nsd/NsdServiceInfo;->getTxtRecordSize()I
-Landroid/net/nsd/NsdServiceInfo;->mHost:Ljava/net/InetAddress;
-Landroid/net/nsd/NsdServiceInfo;->mPort:I
-Landroid/net/nsd/NsdServiceInfo;->mServiceName:Ljava/lang/String;
-Landroid/net/nsd/NsdServiceInfo;->mServiceType:Ljava/lang/String;
-Landroid/net/nsd/NsdServiceInfo;->mTxtRecord:Landroid/util/ArrayMap;
-Landroid/net/nsd/NsdServiceInfo;->setTxtRecords(Ljava/lang/String;)V
-Landroid/net/nsd/NsdServiceInfo;->TAG:Ljava/lang/String;
Landroid/net/ProxyInfo;-><init>(Landroid/net/ProxyInfo;)V
Landroid/net/ProxyInfo;-><init>(Landroid/net/Uri;)V
Landroid/net/ProxyInfo;-><init>(Landroid/net/Uri;I)V
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index f62df7f..6735317 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -8,6 +8,10 @@
native <methods>;
}
+-keep class com.android.networkstack.tethering.util.TcUtils {
+ native <methods>;
+}
+
-keepclassmembers public class * extends com.android.networkstack.tethering.util.Struct {
*;
}
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index b4228da..2bb19db 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -614,10 +614,8 @@
return false;
}
- if (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) {
- // BT configures the interface elsewhere: only start DHCP.
- // TODO: make all tethering types behave the same way, and delete the bluetooth
- // code that calls into NetworkManagementService directly.
+ if (shouldNotConfigureBluetoothInterface()) {
+ // Interface was already configured elsewhere, only start DHCP.
return configureDhcp(enabled, mIpv4Address, null /* clientAddress */);
}
@@ -651,12 +649,15 @@
return configureDhcp(enabled, mIpv4Address, mStaticIpv4ClientAddr);
}
+ private boolean shouldNotConfigureBluetoothInterface() {
+ // Before T, bluetooth tethering configures the interface elsewhere.
+ return (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT();
+ }
+
private LinkAddress requestIpv4Address(final boolean useLastAddress) {
if (mStaticIpv4ServerAddr != null) return mStaticIpv4ServerAddr;
- if (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) {
- return new LinkAddress(BLUETOOTH_IFACE_ADDR);
- }
+ if (shouldNotConfigureBluetoothInterface()) return new LinkAddress(BLUETOOTH_IFACE_ADDR);
return mPrivateAddressCoordinator.requestDownstreamAddress(this, useLastAddress);
}
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
index ad410eb..77efb51 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
@@ -55,11 +55,11 @@
static final boolean DOWNSTREAM = true;
static final boolean UPSTREAM = false;
- // The priority of clat/tether hooks - smaller is higher priority.
+ // The priority of tether hooks - smaller is higher priority.
// TC tether is higher priority then TC clat to match XDP winning over TC.
- // Sync from system/netd/server/OffloadUtils.h.
- static final short PRIO_TETHER6 = 1;
- static final short PRIO_TETHER4 = 2;
+ // Sync from system/netd/server/TcUtils.h.
+ static final short PRIO_TETHER6 = 2;
+ static final short PRIO_TETHER4 = 3;
// note that the above must be lower than PRIO_CLAT from netd's OffloadUtils.cpp
private static String makeProgPath(boolean downstream, int ipVersion, boolean ether) {
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 55c24d3..db9a64f 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -134,7 +134,12 @@
import com.android.internal.util.MessageUtils;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.networkstack.apishim.common.BluetoothPanShim;
+import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
+import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.networkstack.tethering.util.PrefixUtils;
import com.android.networkstack.tethering.util.TetheringUtils;
@@ -265,8 +270,11 @@
private int mOffloadStatus = TETHER_HARDWARE_OFFLOAD_STOPPED;
private EthernetManager.TetheredInterfaceRequest mEthernetIfaceRequest;
+ private TetheredInterfaceRequestShim mBluetoothIfaceRequest;
private String mConfiguredEthernetIface;
+ private String mConfiguredBluetoothIface;
private EthernetCallback mEthernetCallback;
+ private TetheredInterfaceCallbackShim mBluetoothCallback;
private SettingsObserver mSettingsObserver;
private BluetoothPan mBluetoothPan;
private PanServiceListener mBluetoothPanListener;
@@ -533,14 +541,16 @@
}
}
- // This method needs to exist because TETHERING_BLUETOOTH and TETHERING_WIGIG can't use
- // enableIpServing.
+ // This method needs to exist because TETHERING_BLUETOOTH before Android T and TETHERING_WIGIG
+ // can't use enableIpServing.
private void processInterfaceStateChange(final String iface, boolean enabled) {
// Do not listen to USB interface state changes or USB interface add/removes. USB tethering
// is driven only by USB_ACTION broadcasts.
final int type = ifaceNameToType(iface);
if (type == TETHERING_USB || type == TETHERING_NCM) return;
+ if (type == TETHERING_BLUETOOTH && SdkLevel.isAtLeastT()) return;
+
if (enabled) {
ensureIpServerStarted(iface);
} else {
@@ -769,6 +779,9 @@
TETHERING_BLUETOOTH);
}
mPendingPanRequests.clear();
+ mBluetoothIfaceRequest = null;
+ mBluetoothCallback = null;
+ maybeDisableBluetoothIpServing();
});
}
@@ -779,7 +792,11 @@
private void setBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
final boolean enable, final IIntResultListener listener) {
- bluetoothPan.setBluetoothTethering(enable);
+ if (SdkLevel.isAtLeastT()) {
+ changeBluetoothTetheringSettings(bluetoothPan, enable);
+ } else {
+ changeBluetoothTetheringSettingsPreT(bluetoothPan, enable);
+ }
// Enabling bluetooth tethering settings can silently fail. Send internal error if the
// result is not expected.
@@ -788,6 +805,68 @@
sendTetherResult(listener, result, TETHERING_BLUETOOTH);
}
+ private void changeBluetoothTetheringSettingsPreT(@NonNull final BluetoothPan bluetoothPan,
+ final boolean enable) {
+ bluetoothPan.setBluetoothTethering(enable);
+ }
+
+ private void changeBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
+ final boolean enable) {
+ final BluetoothPanShim panShim = mDeps.getBluetoothPanShim(bluetoothPan);
+ if (enable) {
+ if (mBluetoothIfaceRequest != null) {
+ Log.d(TAG, "Bluetooth tethering settings already enabled");
+ return;
+ }
+
+ mBluetoothCallback = new BluetoothCallback();
+ try {
+ mBluetoothIfaceRequest = panShim.requestTetheredInterface(mExecutor,
+ mBluetoothCallback);
+ } catch (UnsupportedApiLevelException e) {
+ Log.wtf(TAG, "Use unsupported API, " + e);
+ }
+ } else {
+ if (mBluetoothIfaceRequest == null) {
+ Log.d(TAG, "Bluetooth tethering settings already disabled");
+ return;
+ }
+
+ mBluetoothIfaceRequest.release();
+ mBluetoothIfaceRequest = null;
+ mBluetoothCallback = null;
+ // If bluetooth request is released, tethering won't able to receive
+ // onUnavailable callback, explicitly disable bluetooth IpServer manually.
+ maybeDisableBluetoothIpServing();
+ }
+ }
+
+ // BluetoothCallback is only called after T. Before T, PanService would call tether/untether to
+ // notify bluetooth interface status.
+ private class BluetoothCallback implements TetheredInterfaceCallbackShim {
+ @Override
+ public void onAvailable(String iface) {
+ if (this != mBluetoothCallback) return;
+
+ enableIpServing(TETHERING_BLUETOOTH, iface, getRequestedState(TETHERING_BLUETOOTH));
+ mConfiguredBluetoothIface = iface;
+ }
+
+ @Override
+ public void onUnavailable() {
+ if (this != mBluetoothCallback) return;
+
+ maybeDisableBluetoothIpServing();
+ }
+ }
+
+ private void maybeDisableBluetoothIpServing() {
+ if (mConfiguredBluetoothIface == null) return;
+
+ ensureIpServerStopped(mConfiguredBluetoothIface);
+ mConfiguredBluetoothIface = null;
+ }
+
private int setEthernetTethering(final boolean enable) {
final EthernetManager em = (EthernetManager) mContext.getSystemService(
Context.ETHERNET_SERVICE);
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 7df9475..c1a747e 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -18,6 +18,7 @@
import android.app.usage.NetworkStatsManager;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothPan;
import android.content.Context;
import android.net.INetd;
import android.net.ip.IpServer;
@@ -31,6 +32,8 @@
import androidx.annotation.NonNull;
import com.android.internal.util.StateMachine;
+import com.android.networkstack.apishim.BluetoothPanShimImpl;
+import com.android.networkstack.apishim.common.BluetoothPanShim;
import java.util.ArrayList;
@@ -158,4 +161,13 @@
TetheringConfiguration cfg) {
return new PrivateAddressCoordinator(ctx, cfg);
}
+
+ /**
+ * Get BluetoothPanShim object to enable/disable bluetooth tethering.
+ *
+ * TODO: use BluetoothPan directly when mainline module is built with API 32.
+ */
+ public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) {
+ return BluetoothPanShimImpl.newInstance(pan);
+ }
}
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 2f2cde0..267c376 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -16,6 +16,7 @@
package android.net.ip;
+import static android.net.INetd.IF_STATE_DOWN;
import static android.net.INetd.IF_STATE_UP;
import static android.net.RouteInfo.RTN_UNICAST;
import static android.net.TetheringManager.TETHERING_BLUETOOTH;
@@ -33,6 +34,7 @@
import static android.net.ip.IpServer.STATE_UNAVAILABLE;
import static android.system.OsConstants.ETH_P_IPV6;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH;
import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH;
@@ -400,11 +402,16 @@
}
@Test
- public void canBeTethered() throws Exception {
+ public void canBeTetheredAsBluetooth() throws Exception {
initStateMachine(TETHERING_BLUETOOTH);
dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
- InOrder inOrder = inOrder(mCallback, mNetd);
+ InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
+ if (isAtLeastT()) {
+ inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+ inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
+ IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
+ }
inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
// One for ipv4 route, one for ipv6 link local route.
@@ -426,7 +433,13 @@
inOrder.verify(mNetd).tetherApplyDnsInterfaces();
inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
- inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
+ // One is ipv4 address clear (set to 0.0.0.0), another is set interface down which only
+ // happen after T. Before T, the interface configuration control in bluetooth side.
+ if (isAtLeastT()) {
+ inOrder.verify(mNetd).interfaceSetCfg(
+ argThat(cfg -> assertContainsFlag(cfg.flags, IF_STATE_DOWN)));
+ }
+ inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> cfg.flags.length == 0));
inOrder.verify(mAddressCoordinator).releaseDownstream(any());
inOrder.verify(mCallback).updateInterfaceState(
mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
@@ -443,7 +456,7 @@
InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
- IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
+ IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
@@ -587,7 +600,8 @@
inOrder.verify(mNetd).tetherApplyDnsInterfaces();
inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
- inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
+ inOrder.verify(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg(
+ argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
inOrder.verify(mAddressCoordinator).releaseDownstream(any());
inOrder.verify(mBpfCoordinator).tetherOffloadClientClear(mIpServer);
inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer);
@@ -683,7 +697,11 @@
initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
- assertDhcpStarted(mBluetoothPrefix);
+ if (isAtLeastT()) {
+ assertDhcpStarted(PrefixUtils.asIpPrefix(mTestAddress));
+ } else {
+ assertDhcpStarted(mBluetoothPrefix);
+ }
}
@Test
@@ -1371,7 +1389,6 @@
for (String flag : flags) {
if (flag.equals(match)) return true;
}
- fail("Missing flag: " + match);
return false;
}
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 40d133a..e4dbc7d 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -62,6 +62,7 @@
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0;
@@ -81,6 +82,8 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Matchers.anyInt;
@@ -182,6 +185,10 @@
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.net.module.util.CollectionUtils;
+import com.android.networkstack.apishim.common.BluetoothPanShim;
+import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
+import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceRequestShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
import com.android.testutils.MiscAsserts;
@@ -261,6 +268,8 @@
@Mock private PackageManager mPackageManager;
@Mock private BluetoothAdapter mBluetoothAdapter;
@Mock private BluetoothPan mBluetoothPan;
+ @Mock private BluetoothPanShim mBluetoothPanShim;
+ @Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim;
private final MockIpServerDependencies mIpServerDependencies =
spy(new MockIpServerDependencies());
@@ -285,6 +294,7 @@
private PrivateAddressCoordinator mPrivateAddressCoordinator;
private SoftApCallback mSoftApCallback;
private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+ private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim;
private TestConnectivityManager mCm;
@@ -483,13 +493,23 @@
return false;
}
-
@Override
public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx,
TetheringConfiguration cfg) {
mPrivateAddressCoordinator = super.getPrivateAddressCoordinator(ctx, cfg);
return mPrivateAddressCoordinator;
}
+
+ @Override
+ public BluetoothPanShim getBluetoothPanShim(BluetoothPan pan) {
+ try {
+ when(mBluetoothPanShim.requestTetheredInterface(
+ any(), any())).thenReturn(mTetheredInterfaceRequestShim);
+ } catch (UnsupportedApiLevelException e) {
+ fail("BluetoothPan#requestTetheredInterface is not supported");
+ }
+ return mBluetoothPanShim;
+ }
}
private static LinkProperties buildUpstreamLinkProperties(String interfaceName,
@@ -2557,6 +2577,44 @@
@Test
public void testBluetoothTethering() throws Exception {
+ // Switch to @IgnoreUpTo(Build.VERSION_CODES.S_V2) when it is available for AOSP.
+ assumeTrue(isAtLeastT());
+
+ final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
+ mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
+ mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
+ mLooper.dispatchAll();
+ verifySetBluetoothTethering(true /* enable */, true /* bindToPanService */);
+ result.assertHasResult();
+
+ mTetheredInterfaceCallbackShim.onAvailable(TEST_BT_IFNAME);
+ mLooper.dispatchAll();
+ verifyNetdCommandForBtSetup();
+
+ // If PAN disconnect, tethering should also be stopped.
+ mTetheredInterfaceCallbackShim.onUnavailable();
+ mLooper.dispatchAll();
+ verifyNetdCommandForBtTearDown();
+
+ // Tethering could restart if PAN reconnect.
+ mTetheredInterfaceCallbackShim.onAvailable(TEST_BT_IFNAME);
+ mLooper.dispatchAll();
+ verifyNetdCommandForBtSetup();
+
+ // Pretend that bluetooth tethering was disabled.
+ mockBluetoothSettings(true /* bluetoothOn */, false /* tetheringOn */);
+ mTethering.stopTethering(TETHERING_BLUETOOTH);
+ mLooper.dispatchAll();
+ verifySetBluetoothTethering(false /* enable */, false /* bindToPanService */);
+
+ verifyNetdCommandForBtTearDown();
+ }
+
+ @Test
+ public void testBluetoothTetheringBeforeT() throws Exception {
+ // Switch to @IgnoreAfter(Build.VERSION_CODES.S_V2) when it is available for AOSP.
+ assumeFalse(isAtLeastT());
+
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result);
@@ -2610,12 +2668,17 @@
mTethering.interfaceAdded(TEST_BT_IFNAME);
mLooper.dispatchAll();
- mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
- mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
- final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
- mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
- mLooper.dispatchAll();
- tetherResult.assertHasResult();
+ if (isAtLeastT()) {
+ mTetheredInterfaceCallbackShim.onAvailable(TEST_BT_IFNAME);
+ mLooper.dispatchAll();
+ } else {
+ mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false);
+ mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true);
+ final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR);
+ mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult);
+ mLooper.dispatchAll();
+ tetherResult.assertHasResult();
+ }
verifyNetdCommandForBtSetup();
@@ -2632,6 +2695,10 @@
}
private void verifyNetdCommandForBtSetup() throws Exception {
+ if (isAtLeastT()) {
+ verify(mNetd).interfaceSetCfg(argThat(cfg -> TEST_BT_IFNAME.equals(cfg.ifName)
+ && assertContainsFlag(cfg.flags, INetd.IF_STATE_UP)));
+ }
verify(mNetd).tetherInterfaceAdd(TEST_BT_IFNAME);
verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME),
@@ -2644,19 +2711,30 @@
reset(mNetd);
}
+ private boolean assertContainsFlag(String[] flags, String match) {
+ for (String flag : flags) {
+ if (flag.equals(match)) return true;
+ }
+ return false;
+ }
+
private void verifyNetdCommandForBtTearDown() throws Exception {
verify(mNetd).tetherApplyDnsInterfaces();
verify(mNetd).tetherInterfaceRemove(TEST_BT_IFNAME);
verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME);
- verify(mNetd).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
+ // One is ipv4 address clear (set to 0.0.0.0), another is set interface down which only
+ // happen after T. Before T, the interface configuration control in bluetooth side.
+ verify(mNetd, times(isAtLeastT() ? 2 : 1)).interfaceSetCfg(
+ any(InterfaceConfigurationParcel.class));
verify(mNetd).tetherStop();
verify(mNetd).ipfwdDisableForwarding(TETHERING_NAME);
+ reset(mNetd);
}
// If bindToPanService is true, this function would return ServiceListener which could notify
// PanService is connected or disconnected.
private ServiceListener verifySetBluetoothTethering(final boolean enable,
- final boolean bindToPanService) {
+ final boolean bindToPanService) throws Exception {
ServiceListener listener = null;
verify(mBluetoothAdapter).isEnabled();
if (bindToPanService) {
@@ -2671,7 +2749,19 @@
verify(mBluetoothAdapter, never()).getProfileProxy(eq(mServiceContext), any(),
anyInt());
}
- verify(mBluetoothPan).setBluetoothTethering(enable);
+
+ if (isAtLeastT()) {
+ if (enable) {
+ final ArgumentCaptor<TetheredInterfaceCallbackShim> callbackCaptor =
+ ArgumentCaptor.forClass(TetheredInterfaceCallbackShim.class);
+ verify(mBluetoothPanShim).requestTetheredInterface(any(), callbackCaptor.capture());
+ mTetheredInterfaceCallbackShim = callbackCaptor.getValue();
+ } else {
+ verify(mTetheredInterfaceRequestShim).release();
+ }
+ } else {
+ verify(mBluetoothPan).setBluetoothTethering(enable);
+ }
verify(mBluetoothPan).isTetheringOn();
verifyNoMoreInteractions(mBluetoothAdapter, mBluetoothPan);
reset(mBluetoothAdapter, mBluetoothPan);
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index d015ef6..cce2b71 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -42,10 +42,12 @@
// TODO: remove it when NetworkStatsService is moved into the mainline module and no more
// calls to JNI in libservices.core.
"//frameworks/base/services/core/jni",
+ "//packages/modules/Connectivity/service",
+ "//packages/modules/Connectivity/service/native/libs/libclat",
"//packages/modules/Connectivity/Tethering",
+ "//packages/modules/Connectivity/service/native",
+ "//packages/modules/Connectivity/service-t/native/libs/libnetworkstats",
"//packages/modules/Connectivity/tests/unit/jni",
- // TODO: remove system/netd/* when all BPF code is moved out of Netd.
- "//system/netd/libnetdbpf",
"//system/netd/server",
"//system/netd/tests",
],
@@ -55,6 +57,16 @@
// bpf kernel programs
//
bpf {
+ name: "dscp_policy.o",
+ srcs: ["dscp_policy.c"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
+ sub_dir: "net_shared",
+}
+
+bpf {
name: "offload.o",
srcs: ["offload.c"],
cflags: [
@@ -71,3 +83,29 @@
"-Werror",
],
}
+
+bpf {
+ name: "clatd.o_mainline",
+ srcs: ["clatd.c"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
+ include_dirs: [
+ "frameworks/libs/net/common/netd/libnetdutils/include",
+ ],
+ sub_dir: "net_shared",
+}
+
+bpf {
+ name: "netd.o_mainline",
+ srcs: ["netd.c"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
+ include_dirs: [
+ "frameworks/libs/net/common/netd/libnetdutils/include",
+ ],
+ sub_dir: "net_shared",
+}
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index 8577d9d..f0df97b 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -20,7 +20,6 @@
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/in6.h>
-#include <netdutils/UidConstants.h>
// This header file is shared by eBPF kernel programs (C) and netd (C++) and
// some of the maps are also accessed directly from Java mainline module code.
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
new file mode 100644
index 0000000..dc646c3
--- /dev/null
+++ b/bpf_progs/clatd.c
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#include <linux/bpf.h>
+#include <linux/if.h>
+#include <linux/if_ether.h>
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/pkt_cls.h>
+#include <linux/swab.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+// bionic kernel uapi linux/udp.h header is munged...
+#define __kernel_udphdr udphdr
+#include <linux/udp.h>
+
+#include "bpf_helpers.h"
+#include "bpf_net_helpers.h"
+#include "bpf_shared.h"
+
+// From kernel:include/net/ip.h
+#define IP_DF 0x4000 // Flag: "Don't Fragment"
+
+DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
+
+static inline __always_inline int nat64(struct __sk_buff* skb, bool is_ethernet) {
+ const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+ void* data = (void*)(long)skb->data;
+ const void* data_end = (void*)(long)skb->data_end;
+ const struct ethhdr* const eth = is_ethernet ? data : NULL; // used iff is_ethernet
+ const struct ipv6hdr* const ip6 = is_ethernet ? (void*)(eth + 1) : data;
+
+ // Require ethernet dst mac address to be our unicast address.
+ if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
+
+ // Must be meta-ethernet IPv6 frame
+ if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_PIPE;
+
+ // Must have (ethernet and) ipv6 header
+ if (data + l2_header_size + sizeof(*ip6) > data_end) return TC_ACT_PIPE;
+
+ // Ethertype - if present - must be IPv6
+ if (is_ethernet && (eth->h_proto != htons(ETH_P_IPV6))) return TC_ACT_PIPE;
+
+ // IP version must be 6
+ if (ip6->version != 6) return TC_ACT_PIPE;
+
+ // Maximum IPv6 payload length that can be translated to IPv4
+ if (ntohs(ip6->payload_len) > 0xFFFF - sizeof(struct iphdr)) return TC_ACT_PIPE;
+
+ switch (ip6->nexthdr) {
+ case IPPROTO_TCP: // For TCP & UDP the checksum neutrality of the chosen IPv6
+ case IPPROTO_UDP: // address means there is no need to update their checksums.
+ case IPPROTO_GRE: // We do not need to bother looking at GRE/ESP headers,
+ case IPPROTO_ESP: // since there is never a checksum to update.
+ break;
+
+ default: // do not know how to handle anything else
+ return TC_ACT_PIPE;
+ }
+
+ ClatIngress6Key k = {
+ .iif = skb->ifindex,
+ .pfx96.in6_u.u6_addr32 =
+ {
+ ip6->saddr.in6_u.u6_addr32[0],
+ ip6->saddr.in6_u.u6_addr32[1],
+ ip6->saddr.in6_u.u6_addr32[2],
+ },
+ .local6 = ip6->daddr,
+ };
+
+ ClatIngress6Value* v = bpf_clat_ingress6_map_lookup_elem(&k);
+
+ if (!v) return TC_ACT_PIPE;
+
+ struct ethhdr eth2; // used iff is_ethernet
+ if (is_ethernet) {
+ eth2 = *eth; // Copy over the ethernet header (src/dst mac)
+ eth2.h_proto = htons(ETH_P_IP); // But replace the ethertype
+ }
+
+ struct iphdr ip = {
+ .version = 4, // u4
+ .ihl = sizeof(struct iphdr) / sizeof(__u32), // u4
+ .tos = (ip6->priority << 4) + (ip6->flow_lbl[0] >> 4), // u8
+ .tot_len = htons(ntohs(ip6->payload_len) + sizeof(struct iphdr)), // u16
+ .id = 0, // u16
+ .frag_off = htons(IP_DF), // u16
+ .ttl = ip6->hop_limit, // u8
+ .protocol = ip6->nexthdr, // u8
+ .check = 0, // u16
+ .saddr = ip6->saddr.in6_u.u6_addr32[3], // u32
+ .daddr = v->local4.s_addr, // u32
+ };
+
+ // Calculate the IPv4 one's complement checksum of the IPv4 header.
+ __wsum sum4 = 0;
+ for (int i = 0; i < sizeof(ip) / sizeof(__u16); ++i) {
+ sum4 += ((__u16*)&ip)[i];
+ }
+ // Note that sum4 is guaranteed to be non-zero by virtue of ip.version == 4
+ sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse u32 into range 1 .. 0x1FFFE
+ sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse any potential carry into u16
+ ip.check = (__u16)~sum4; // sum4 cannot be zero, so this is never 0xFFFF
+
+ // Calculate the *negative* IPv6 16-bit one's complement checksum of the IPv6 header.
+ __wsum sum6 = 0;
+ // We'll end up with a non-zero sum due to ip6->version == 6 (which has '0' bits)
+ for (int i = 0; i < sizeof(*ip6) / sizeof(__u16); ++i) {
+ sum6 += ~((__u16*)ip6)[i]; // note the bitwise negation
+ }
+
+ // Note that there is no L4 checksum update: we are relying on the checksum neutrality
+ // of the ipv6 address chosen by netd's ClatdController.
+
+ // Packet mutations begin - point of no return, but if this first modification fails
+ // the packet is probably still pristine, so let clatd handle it.
+ if (bpf_skb_change_proto(skb, htons(ETH_P_IP), 0)) return TC_ACT_PIPE;
+
+ // This takes care of updating the skb->csum field for a CHECKSUM_COMPLETE packet.
+ //
+ // In such a case, skb->csum is a 16-bit one's complement sum of the entire payload,
+ // thus we need to subtract out the ipv6 header's sum, and add in the ipv4 header's sum.
+ // However, by construction of ip.check above the checksum of an ipv4 header is zero.
+ // Thus we only need to subtract the ipv6 header's sum, which is the same as adding
+ // in the sum of the bitwise negation of the ipv6 header.
+ //
+ // bpf_csum_update() always succeeds if the skb is CHECKSUM_COMPLETE and returns an error
+ // (-ENOTSUPP) if it isn't. So we just ignore the return code.
+ //
+ // if (skb->ip_summed == CHECKSUM_COMPLETE)
+ // return (skb->csum = csum_add(skb->csum, csum));
+ // else
+ // return -ENOTSUPP;
+ bpf_csum_update(skb, sum6);
+
+ // bpf_skb_change_proto() invalidates all pointers - reload them.
+ data = (void*)(long)skb->data;
+ data_end = (void*)(long)skb->data_end;
+
+ // I cannot think of any valid way for this error condition to trigger, however I do
+ // believe the explicit check is required to keep the in kernel ebpf verifier happy.
+ if (data + l2_header_size + sizeof(struct iphdr) > data_end) return TC_ACT_SHOT;
+
+ if (is_ethernet) {
+ struct ethhdr* new_eth = data;
+
+ // Copy over the updated ethernet header
+ *new_eth = eth2;
+
+ // Copy over the new ipv4 header.
+ *(struct iphdr*)(new_eth + 1) = ip;
+ } else {
+ // Copy over the new ipv4 header without an ethernet header.
+ *(struct iphdr*)data = ip;
+ }
+
+ // Redirect, possibly back to same interface, so tcpdump sees packet twice.
+ if (v->oif) return bpf_redirect(v->oif, BPF_F_INGRESS);
+
+ // Just let it through, tcpdump will not see IPv4 packet.
+ return TC_ACT_PIPE;
+}
+
+DEFINE_BPF_PROG("schedcls/ingress6/clat_ether", AID_ROOT, AID_SYSTEM, sched_cls_ingress6_clat_ether)
+(struct __sk_buff* skb) {
+ return nat64(skb, true);
+}
+
+DEFINE_BPF_PROG("schedcls/ingress6/clat_rawip", AID_ROOT, AID_SYSTEM, sched_cls_ingress6_clat_rawip)
+(struct __sk_buff* skb) {
+ return nat64(skb, false);
+}
+
+DEFINE_BPF_MAP_GRW(clat_egress4_map, HASH, ClatEgress4Key, ClatEgress4Value, 16, AID_SYSTEM)
+
+DEFINE_BPF_PROG("schedcls/egress4/clat_ether", AID_ROOT, AID_SYSTEM, sched_cls_egress4_clat_ether)
+(struct __sk_buff* skb) {
+ return TC_ACT_PIPE;
+}
+
+DEFINE_BPF_PROG("schedcls/egress4/clat_rawip", AID_ROOT, AID_SYSTEM, sched_cls_egress4_clat_rawip)
+(struct __sk_buff* skb) {
+ void* data = (void*)(long)skb->data;
+ const void* data_end = (void*)(long)skb->data_end;
+ const struct iphdr* const ip4 = data;
+
+ // Must be meta-ethernet IPv4 frame
+ if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_PIPE;
+
+ // Must have ipv4 header
+ if (data + sizeof(*ip4) > data_end) return TC_ACT_PIPE;
+
+ // IP version must be 4
+ if (ip4->version != 4) return TC_ACT_PIPE;
+
+ // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+ if (ip4->ihl != 5) return TC_ACT_PIPE;
+
+ // Calculate the IPv4 one's complement checksum of the IPv4 header.
+ __wsum sum4 = 0;
+ for (int i = 0; i < sizeof(*ip4) / sizeof(__u16); ++i) {
+ sum4 += ((__u16*)ip4)[i];
+ }
+ // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
+ sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse u32 into range 1 .. 0x1FFFE
+ sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse any potential carry into u16
+ // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF
+ if (sum4 != 0xFFFF) return TC_ACT_PIPE;
+
+ // Minimum IPv4 total length is the size of the header
+ if (ntohs(ip4->tot_len) < sizeof(*ip4)) return TC_ACT_PIPE;
+
+ // We are incapable of dealing with IPv4 fragments
+ if (ip4->frag_off & ~htons(IP_DF)) return TC_ACT_PIPE;
+
+ switch (ip4->protocol) {
+ case IPPROTO_TCP: // For TCP & UDP the checksum neutrality of the chosen IPv6
+ case IPPROTO_GRE: // address means there is no need to update their checksums.
+ case IPPROTO_ESP: // We do not need to bother looking at GRE/ESP headers,
+ break; // since there is never a checksum to update.
+
+ case IPPROTO_UDP: // See above comment, but must also have UDP header...
+ if (data + sizeof(*ip4) + sizeof(struct udphdr) > data_end) return TC_ACT_PIPE;
+ const struct udphdr* uh = (const struct udphdr*)(ip4 + 1);
+ // If IPv4/UDP checksum is 0 then fallback to clatd so it can calculate the
+ // checksum. Otherwise the network or more likely the NAT64 gateway might
+ // drop the packet because in most cases IPv6/UDP packets with a zero checksum
+ // are invalid. See RFC 6935. TODO: calculate checksum via bpf_csum_diff()
+ if (!uh->check) return TC_ACT_PIPE;
+ break;
+
+ default: // do not know how to handle anything else
+ return TC_ACT_PIPE;
+ }
+
+ ClatEgress4Key k = {
+ .iif = skb->ifindex,
+ .local4.s_addr = ip4->saddr,
+ };
+
+ ClatEgress4Value* v = bpf_clat_egress4_map_lookup_elem(&k);
+
+ if (!v) return TC_ACT_PIPE;
+
+ // Translating without redirecting doesn't make sense.
+ if (!v->oif) return TC_ACT_PIPE;
+
+ // This implementation is currently limited to rawip.
+ if (v->oifIsEthernet) return TC_ACT_PIPE;
+
+ struct ipv6hdr ip6 = {
+ .version = 6, // __u8:4
+ .priority = ip4->tos >> 4, // __u8:4
+ .flow_lbl = {(ip4->tos & 0xF) << 4, 0, 0}, // __u8[3]
+ .payload_len = htons(ntohs(ip4->tot_len) - 20), // __be16
+ .nexthdr = ip4->protocol, // __u8
+ .hop_limit = ip4->ttl, // __u8
+ .saddr = v->local6, // struct in6_addr
+ .daddr = v->pfx96, // struct in6_addr
+ };
+ ip6.daddr.in6_u.u6_addr32[3] = ip4->daddr;
+
+ // Calculate the IPv6 16-bit one's complement checksum of the IPv6 header.
+ __wsum sum6 = 0;
+ // We'll end up with a non-zero sum due to ip6.version == 6
+ for (int i = 0; i < sizeof(ip6) / sizeof(__u16); ++i) {
+ sum6 += ((__u16*)&ip6)[i];
+ }
+
+ // Note that there is no L4 checksum update: we are relying on the checksum neutrality
+ // of the ipv6 address chosen by netd's ClatdController.
+
+ // Packet mutations begin - point of no return, but if this first modification fails
+ // the packet is probably still pristine, so let clatd handle it.
+ if (bpf_skb_change_proto(skb, htons(ETH_P_IPV6), 0)) return TC_ACT_PIPE;
+
+ // This takes care of updating the skb->csum field for a CHECKSUM_COMPLETE packet.
+ //
+ // In such a case, skb->csum is a 16-bit one's complement sum of the entire payload,
+ // thus we need to subtract out the ipv4 header's sum, and add in the ipv6 header's sum.
+ // However, we've already verified the ipv4 checksum is correct and thus 0.
+ // Thus we only need to add the ipv6 header's sum.
+ //
+ // bpf_csum_update() always succeeds if the skb is CHECKSUM_COMPLETE and returns an error
+ // (-ENOTSUPP) if it isn't. So we just ignore the return code (see above for more details).
+ bpf_csum_update(skb, sum6);
+
+ // bpf_skb_change_proto() invalidates all pointers - reload them.
+ data = (void*)(long)skb->data;
+ data_end = (void*)(long)skb->data_end;
+
+ // I cannot think of any valid way for this error condition to trigger, however I do
+ // believe the explicit check is required to keep the in kernel ebpf verifier happy.
+ if (data + sizeof(ip6) > data_end) return TC_ACT_SHOT;
+
+ // Copy over the new ipv6 header without an ethernet header.
+ *(struct ipv6hdr*)data = ip6;
+
+ // Redirect to non v4-* interface. Tcpdump only sees packet after this redirect.
+ return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("netd");
diff --git a/bpf_progs/dscp_policy.c b/bpf_progs/dscp_policy.c
new file mode 100644
index 0000000..9989e6b
--- /dev/null
+++ b/bpf_progs/dscp_policy.c
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#include <linux/types.h>
+#include <linux/bpf.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/if_ether.h>
+#include <linux/pkt_cls.h>
+#include <linux/tcp.h>
+#include <stdint.h>
+#include <netinet/in.h>
+#include <netinet/udp.h>
+#include <string.h>
+
+#include "bpf_helpers.h"
+
+#define MAX_POLICIES 16
+#define MAP_A 1
+#define MAP_B 2
+
+#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
+
+// TODO: these are already defined in /system/netd/bpf_progs/bpf_net_helpers.h
+// should they be moved to common location?
+static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) =
+ (void*)BPF_FUNC_get_socket_cookie;
+static int (*bpf_skb_store_bytes)(struct __sk_buff* skb, __u32 offset, const void* from, __u32 len,
+ __u64 flags) = (void*)BPF_FUNC_skb_store_bytes;
+static int (*bpf_l3_csum_replace)(struct __sk_buff* skb, __u32 offset, __u64 from, __u64 to,
+ __u64 flags) = (void*)BPF_FUNC_l3_csum_replace;
+
+typedef struct {
+ // Add family here to match __sk_buff ?
+ struct in_addr srcIp;
+ struct in_addr dstIp;
+ __be16 srcPort;
+ __be16 dstPort;
+ uint8_t proto;
+ uint8_t dscpVal;
+ uint8_t pad[2];
+} Ipv4RuleEntry;
+STRUCT_SIZE(Ipv4RuleEntry, 2 * 4 + 2 * 2 + 2 * 1 + 2); // 16, 4 for in_addr
+
+#define SRC_IP_MASK 1
+#define DST_IP_MASK 2
+#define SRC_PORT_MASK 4
+#define DST_PORT_MASK 8
+#define PROTO_MASK 16
+
+typedef struct {
+ struct in6_addr srcIp;
+ struct in6_addr dstIp;
+ __be16 srcPort;
+ __be16 dstPortStart;
+ __be16 dstPortEnd;
+ uint8_t proto;
+ uint8_t dscpVal;
+ uint8_t mask;
+ uint8_t pad[3];
+} Ipv4Policy;
+STRUCT_SIZE(Ipv4Policy, 2 * 16 + 3 * 2 + 3 * 1 + 3); // 44
+
+typedef struct {
+ struct in6_addr srcIp;
+ struct in6_addr dstIp;
+ __be16 srcPort;
+ __be16 dstPortStart;
+ __be16 dstPortEnd;
+ uint8_t proto;
+ uint8_t dscpVal;
+ uint8_t mask;
+ // should we override this struct to include the param bitmask for linear search?
+ // For mapping socket to policies, all the params should match exactly since we can
+ // pull any missing from the sock itself.
+} Ipv6RuleEntry;
+STRUCT_SIZE(Ipv6RuleEntry, 2 * 16 + 3 * 2 + 3 * 1 + 3); // 44
+
+// TODO: move to using 1 map. Map v4 address to 0xffff::v4
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_A, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES,
+ AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv4_socket_to_policies_map_B, HASH, uint64_t, Ipv4RuleEntry, MAX_POLICIES,
+ AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_A, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES,
+ AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_socket_to_policies_map_B, HASH, uint64_t, Ipv6RuleEntry, MAX_POLICIES,
+ AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(switch_comp_map, ARRAY, int, uint64_t, 1, AID_SYSTEM)
+
+DEFINE_BPF_MAP_GRW(ipv4_dscp_policies_map, ARRAY, uint32_t, Ipv4Policy, MAX_POLICIES,
+ AID_SYSTEM)
+DEFINE_BPF_MAP_GRW(ipv6_dscp_policies_map, ARRAY, uint32_t, Ipv6RuleEntry, MAX_POLICIES,
+ AID_SYSTEM)
+
+DEFINE_BPF_PROG_KVER("schedcls/set_dscp", AID_ROOT, AID_SYSTEM,
+ schedcls_set_dscp, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ int one = 0;
+ uint64_t* selectedMap = bpf_switch_comp_map_lookup_elem(&one);
+
+ // use this with HASH map so map lookup only happens once policies have been added?
+ if (!selectedMap) {
+ return TC_ACT_PIPE;
+ }
+
+ // used for map lookup
+ uint64_t cookie = bpf_get_socket_cookie(skb);
+
+ // Do we need separate maps for ipv4/ipv6
+ if (skb->protocol == htons(ETH_P_IP)) { //maybe bpf_htons()
+ Ipv4RuleEntry* v4Policy;
+ if (*selectedMap == MAP_A) {
+ v4Policy = bpf_ipv4_socket_to_policies_map_A_lookup_elem(&cookie);
+ } else {
+ v4Policy = bpf_ipv4_socket_to_policies_map_B_lookup_elem(&cookie);
+ }
+
+ // How to use bitmask here to compare params efficiently?
+ // TODO: add BPF_PROG_TYPE_SK_SKB prog type to Loader?
+
+ void* data = (void*)(long)skb->data;
+ const void* data_end = (void*)(long)skb->data_end;
+ const struct iphdr* const iph = data;
+
+ // Must have ipv4 header
+ if (data + sizeof(*iph) > data_end) return TC_ACT_PIPE;
+
+ // IP version must be 4
+ if (iph->version != 4) return TC_ACT_PIPE;
+
+ // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+ if (iph->ihl != 5) return TC_ACT_PIPE;
+
+ if (iph->protocol != IPPROTO_UDP) return TC_ACT_PIPE;
+
+ struct udphdr *udp;
+ udp = data + sizeof(struct iphdr); //sizeof(struct ethhdr)
+
+ if ((void*)(udp + 1) > data_end) return TC_ACT_PIPE;
+
+ // Source/destination port in udphdr are stored in be16, need to convert to le16.
+ // This can be done via ntohs or htons. Is there a more preferred way?
+ // Cached policy was found.
+ if (v4Policy && iph->saddr == v4Policy->srcIp.s_addr &&
+ iph->daddr == v4Policy->dstIp.s_addr &&
+ ntohs(udp->source) == v4Policy->srcPort &&
+ ntohs(udp->dest) == v4Policy->dstPort &&
+ iph->protocol == v4Policy->proto) {
+ // set dscpVal in packet. Least sig 2 bits of TOS
+ // reference ipv4_change_dsfield()
+
+ // TODO: fix checksum...
+ int ecn = iph->tos & 3;
+ uint8_t newDscpVal = (v4Policy->dscpVal << 2) + ecn;
+ int oldDscpVal = iph->tos >> 2;
+ bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t));
+ bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0);
+ return TC_ACT_PIPE;
+ }
+
+ // linear scan ipv4_dscp_policies_map, stored socket params do not match actual
+ int bestScore = -1;
+ uint32_t bestMatch = 0;
+
+ for (register uint64_t i = 0; i < MAX_POLICIES; i++) {
+ int score = 0;
+ uint8_t tempMask = 0;
+ // Using a uint62 in for loop prevents infinite loop during BPF load,
+ // but the key is uint32, so convert back.
+ uint32_t key = i;
+ Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&key);
+
+ // if mask is 0 continue, key does not have corresponding policy value
+ if (policy && policy->mask != 0) {
+ if ((policy->mask & SRC_IP_MASK) == SRC_IP_MASK &&
+ iph->saddr == policy->srcIp.s6_addr32[3]) {
+ score++;
+ tempMask |= SRC_IP_MASK;
+ }
+ if ((policy->mask & DST_IP_MASK) == DST_IP_MASK &&
+ iph->daddr == policy->dstIp.s6_addr32[3]) {
+ score++;
+ tempMask |= DST_IP_MASK;
+ }
+ if ((policy->mask & SRC_PORT_MASK) == SRC_PORT_MASK &&
+ ntohs(udp->source) == htons(policy->srcPort)) {
+ score++;
+ tempMask |= SRC_PORT_MASK;
+ }
+ if ((policy->mask & DST_PORT_MASK) == DST_PORT_MASK &&
+ ntohs(udp->dest) >= htons(policy->dstPortStart) &&
+ ntohs(udp->dest) <= htons(policy->dstPortEnd)) {
+ score++;
+ tempMask |= DST_PORT_MASK;
+ }
+ if ((policy->mask & PROTO_MASK) == PROTO_MASK &&
+ iph->protocol == policy->proto) {
+ score++;
+ tempMask |= PROTO_MASK;
+ }
+
+ if (score > bestScore && tempMask == policy->mask) {
+ bestMatch = i;
+ bestScore = score;
+ }
+ }
+ }
+
+ uint8_t newDscpVal = 0; // Can 0 be used as default forwarding value?
+ uint8_t curDscp = iph->tos & 252;
+ if (bestScore > 0) {
+ Ipv4Policy* policy = bpf_ipv4_dscp_policies_map_lookup_elem(&bestMatch);
+ if (policy) {
+ // TODO: if DSCP value is already set ignore?
+ // TODO: update checksum, for testing increment counter...
+ int ecn = iph->tos & 3;
+ newDscpVal = (policy->dscpVal << 2) + ecn;
+ }
+ }
+
+ Ipv4RuleEntry value = {
+ .srcIp.s_addr = iph->saddr,
+ .dstIp.s_addr = iph->daddr,
+ .srcPort = udp->source,
+ .dstPort = udp->dest,
+ .proto = iph->protocol,
+ .dscpVal = newDscpVal,
+ };
+
+ if (!cookie)
+ return TC_ACT_PIPE;
+
+ // Update map
+ if (*selectedMap == MAP_A) {
+ bpf_ipv4_socket_to_policies_map_A_update_elem(&cookie, &value, BPF_ANY);
+ } else {
+ bpf_ipv4_socket_to_policies_map_B_update_elem(&cookie, &value, BPF_ANY);
+ }
+
+ // Need to store bytes after updating map or program will not load.
+ if (newDscpVal != curDscp) {
+ // 1 is the offset (Version/Header length)
+ int oldDscpVal = iph->tos >> 2;
+ bpf_l3_csum_replace(skb, 1, oldDscpVal, newDscpVal, sizeof(uint8_t));
+ bpf_skb_store_bytes(skb, 1, &newDscpVal, sizeof(uint8_t), 0);
+ }
+
+ } else if (skb->protocol == htons(ETH_P_IPV6)) { //maybe bpf_htons()
+ Ipv6RuleEntry* v6Policy;
+ if (*selectedMap == MAP_A) {
+ v6Policy = bpf_ipv6_socket_to_policies_map_A_lookup_elem(&cookie);
+ } else {
+ v6Policy = bpf_ipv6_socket_to_policies_map_B_lookup_elem(&cookie);
+ }
+
+ if (!v6Policy)
+ return TC_ACT_PIPE;
+
+ // TODO: Add code to process IPv6 packet.
+ }
+
+ // Always return TC_ACT_PIPE
+ return TC_ACT_PIPE;
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("Connectivity");
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
new file mode 100644
index 0000000..f0af8b4
--- /dev/null
+++ b/bpf_progs/netd.c
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#include <bpf_helpers.h>
+#include <linux/bpf.h>
+#include <linux/if.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/pkt_cls.h>
+#include <linux/tcp.h>
+#include <netdutils/UidConstants.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include "bpf_net_helpers.h"
+#include "bpf_shared.h"
+
+// This is defined for cgroup bpf filter only.
+#define BPF_DROP_UNLESS_DNS 2
+#define BPF_PASS 1
+#define BPF_DROP 0
+
+// This is used for xt_bpf program only.
+#define BPF_NOMATCH 0
+#define BPF_MATCH 1
+
+#define BPF_EGRESS 0
+#define BPF_INGRESS 1
+
+#define IP_PROTO_OFF offsetof(struct iphdr, protocol)
+#define IPV6_PROTO_OFF offsetof(struct ipv6hdr, nexthdr)
+#define IPPROTO_IHL_OFF 0
+#define TCP_FLAG_OFF 13
+#define RST_OFFSET 2
+
+DEFINE_BPF_MAP_GRW(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE,
+ AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE,
+ AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE,
+ AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE,
+ AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(configuration_map, HASH, uint32_t, uint8_t, CONFIGURATION_MAP_SIZE,
+ AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE,
+ AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_GRW(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE, AID_NET_BW_ACCT)
+
+/* never actually used from ebpf */
+DEFINE_BPF_MAP_GRW(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE,
+ AID_NET_BW_ACCT)
+
+static __always_inline int is_system_uid(uint32_t uid) {
+ return (uid <= MAX_SYSTEM_UID) && (uid >= MIN_SYSTEM_UID);
+}
+
+/*
+ * Note: this blindly assumes an MTU of 1500, and that packets > MTU are always TCP,
+ * and that TCP is using the Linux default settings with TCP timestamp option enabled
+ * which uses 12 TCP option bytes per frame.
+ *
+ * These are not unreasonable assumptions:
+ *
+ * The internet does not really support MTUs greater than 1500, so most TCP traffic will
+ * be at that MTU, or slightly below it (worst case our upwards adjustment is too small).
+ *
+ * The chance our traffic isn't IP at all is basically zero, so the IP overhead correction
+ * is bound to be needed.
+ *
+ * Furthermore, the likelyhood that we're having to deal with GSO (ie. > MTU) packets that
+ * are not IP/TCP is pretty small (few other things are supported by Linux) and worse case
+ * our extra overhead will be slightly off, but probably still better than assuming none.
+ *
+ * Most servers are also Linux and thus support/default to using TCP timestamp option
+ * (and indeed TCP timestamp option comes from RFC 1323 titled "TCP Extensions for High
+ * Performance" which also defined TCP window scaling and are thus absolutely ancient...).
+ *
+ * All together this should be more correct than if we simply ignored GSO frames
+ * (ie. counted them as single packets with no extra overhead)
+ *
+ * Especially since the number of packets is important for any future clat offload correction.
+ * (which adjusts upward by 20 bytes per packet to account for ipv4 -> ipv6 header conversion)
+ */
+#define DEFINE_UPDATE_STATS(the_stats_map, TypeOfKey) \
+ static __always_inline inline void update_##the_stats_map(struct __sk_buff* skb, \
+ int direction, TypeOfKey* key) { \
+ StatsValue* value = bpf_##the_stats_map##_lookup_elem(key); \
+ if (!value) { \
+ StatsValue newValue = {}; \
+ bpf_##the_stats_map##_update_elem(key, &newValue, BPF_NOEXIST); \
+ value = bpf_##the_stats_map##_lookup_elem(key); \
+ } \
+ if (value) { \
+ const int mtu = 1500; \
+ uint64_t packets = 1; \
+ uint64_t bytes = skb->len; \
+ if (bytes > mtu) { \
+ bool is_ipv6 = (skb->protocol == htons(ETH_P_IPV6)); \
+ int ip_overhead = (is_ipv6 ? sizeof(struct ipv6hdr) : sizeof(struct iphdr)); \
+ int tcp_overhead = ip_overhead + sizeof(struct tcphdr) + 12; \
+ int mss = mtu - tcp_overhead; \
+ uint64_t payload = bytes - tcp_overhead; \
+ packets = (payload + mss - 1) / mss; \
+ bytes = tcp_overhead * packets + payload; \
+ } \
+ if (direction == BPF_EGRESS) { \
+ __sync_fetch_and_add(&value->txPackets, packets); \
+ __sync_fetch_and_add(&value->txBytes, bytes); \
+ } else if (direction == BPF_INGRESS) { \
+ __sync_fetch_and_add(&value->rxPackets, packets); \
+ __sync_fetch_and_add(&value->rxBytes, bytes); \
+ } \
+ } \
+ }
+
+DEFINE_UPDATE_STATS(app_uid_stats_map, uint32_t)
+DEFINE_UPDATE_STATS(iface_stats_map, uint32_t)
+DEFINE_UPDATE_STATS(stats_map_A, StatsKey)
+DEFINE_UPDATE_STATS(stats_map_B, StatsKey)
+
+static inline bool skip_owner_match(struct __sk_buff* skb) {
+ int offset = -1;
+ int ret = 0;
+ if (skb->protocol == htons(ETH_P_IP)) {
+ offset = IP_PROTO_OFF;
+ uint8_t proto, ihl;
+ uint8_t flag;
+ ret = bpf_skb_load_bytes(skb, offset, &proto, 1);
+ if (!ret) {
+ if (proto == IPPROTO_ESP) {
+ return true;
+ } else if (proto == IPPROTO_TCP) {
+ ret = bpf_skb_load_bytes(skb, IPPROTO_IHL_OFF, &ihl, 1);
+ ihl = ihl & 0x0F;
+ ret = bpf_skb_load_bytes(skb, ihl * 4 + TCP_FLAG_OFF, &flag, 1);
+ if (ret == 0 && (flag >> RST_OFFSET & 1)) {
+ return true;
+ }
+ }
+ }
+ } else if (skb->protocol == htons(ETH_P_IPV6)) {
+ offset = IPV6_PROTO_OFF;
+ uint8_t proto;
+ ret = bpf_skb_load_bytes(skb, offset, &proto, 1);
+ if (!ret) {
+ if (proto == IPPROTO_ESP) {
+ return true;
+ } else if (proto == IPPROTO_TCP) {
+ uint8_t flag;
+ ret = bpf_skb_load_bytes(skb, sizeof(struct ipv6hdr) + TCP_FLAG_OFF, &flag, 1);
+ if (ret == 0 && (flag >> RST_OFFSET & 1)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+}
+
+static __always_inline BpfConfig getConfig(uint32_t configKey) {
+ uint32_t mapSettingKey = configKey;
+ BpfConfig* config = bpf_configuration_map_lookup_elem(&mapSettingKey);
+ if (!config) {
+ // Couldn't read configuration entry. Assume everything is disabled.
+ return DEFAULT_CONFIG;
+ }
+ return *config;
+}
+
+static inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid, int direction) {
+ if (skip_owner_match(skb)) return BPF_PASS;
+
+ if (is_system_uid(uid)) return BPF_PASS;
+
+ BpfConfig enabledRules = getConfig(UID_RULES_CONFIGURATION_KEY);
+
+ UidOwnerValue* uidEntry = bpf_uid_owner_map_lookup_elem(&uid);
+ uint8_t uidRules = uidEntry ? uidEntry->rule : 0;
+ uint32_t allowed_iif = uidEntry ? uidEntry->iif : 0;
+
+ if (enabledRules) {
+ if ((enabledRules & DOZABLE_MATCH) && !(uidRules & DOZABLE_MATCH)) {
+ return BPF_DROP;
+ }
+ if ((enabledRules & STANDBY_MATCH) && (uidRules & STANDBY_MATCH)) {
+ return BPF_DROP;
+ }
+ if ((enabledRules & POWERSAVE_MATCH) && !(uidRules & POWERSAVE_MATCH)) {
+ return BPF_DROP;
+ }
+ if ((enabledRules & RESTRICTED_MATCH) && !(uidRules & RESTRICTED_MATCH)) {
+ 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) {
+ return BPF_DROP_UNLESS_DNS;
+ }
+ }
+ return BPF_PASS;
+}
+
+static __always_inline inline void update_stats_with_config(struct __sk_buff* skb, int direction,
+ StatsKey* key, uint8_t selectedMap) {
+ if (selectedMap == SELECT_MAP_A) {
+ update_stats_map_A(skb, direction, key);
+ } else if (selectedMap == SELECT_MAP_B) {
+ update_stats_map_B(skb, direction, key);
+ }
+}
+
+static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb, int direction) {
+ uint32_t sock_uid = bpf_get_socket_uid(skb);
+ uint64_t cookie = bpf_get_socket_cookie(skb);
+ UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
+ uint32_t uid, tag;
+ if (utag) {
+ uid = utag->uid;
+ tag = utag->tag;
+ } else {
+ uid = sock_uid;
+ tag = 0;
+ }
+
+ // Always allow and never count clat traffic. Only the IPv4 traffic on the stacked
+ // interface is accounted for and subject to usage restrictions.
+ // TODO: remove sock_uid check once Nat464Xlat javaland adds the socket tag AID_CLAT for clat.
+ if (sock_uid == AID_CLAT || uid == AID_CLAT) {
+ return BPF_PASS;
+ }
+
+ int match = bpf_owner_match(skb, sock_uid, direction);
+ if ((direction == BPF_EGRESS) && (match == BPF_DROP)) {
+ // If an outbound packet is going to be dropped, we do not count that
+ // traffic.
+ return match;
+ }
+
+// Workaround for secureVPN with VpnIsolation enabled, refer to b/159994981 for details.
+// Keep TAG_SYSTEM_DNS in sync with DnsResolver/include/netd_resolv/resolv.h
+// and TrafficStatsConstants.java
+#define TAG_SYSTEM_DNS 0xFFFFFF82
+ if (tag == TAG_SYSTEM_DNS && uid == AID_DNS) {
+ uid = sock_uid;
+ if (match == BPF_DROP_UNLESS_DNS) match = BPF_PASS;
+ } else {
+ if (match == BPF_DROP_UNLESS_DNS) match = BPF_DROP;
+ }
+
+ StatsKey key = {.uid = uid, .tag = tag, .counterSet = 0, .ifaceIndex = skb->ifindex};
+
+ uint8_t* counterSet = bpf_uid_counterset_map_lookup_elem(&uid);
+ if (counterSet) key.counterSet = (uint32_t)*counterSet;
+
+ uint32_t mapSettingKey = CURRENT_STATS_MAP_CONFIGURATION_KEY;
+ uint8_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey);
+
+ // Use asm("%0 &= 1" : "+r"(match)) before return match,
+ // to help kernel's bpf verifier, so that it can be 100% certain
+ // that the returned value is always BPF_NOMATCH(0) or BPF_MATCH(1).
+ if (!selectedMap) {
+ asm("%0 &= 1" : "+r"(match));
+ return match;
+ }
+
+ if (key.tag) {
+ update_stats_with_config(skb, direction, &key, *selectedMap);
+ key.tag = 0;
+ }
+
+ update_stats_with_config(skb, direction, &key, *selectedMap);
+ update_app_uid_stats_map(skb, direction, &uid);
+ asm("%0 &= 1" : "+r"(match));
+ return match;
+}
+
+DEFINE_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_ROOT, bpf_cgroup_ingress)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, BPF_INGRESS);
+}
+
+DEFINE_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_ROOT, bpf_cgroup_egress)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, BPF_EGRESS);
+}
+
+DEFINE_BPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
+(struct __sk_buff* skb) {
+ // Clat daemon does not generate new traffic, all its traffic is accounted for already
+ // on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead,
+ // but that can be corrected for later when merging v4-foo stats into interface foo's).
+ // TODO: remove sock_uid check once Nat464Xlat javaland adds the socket tag AID_CLAT for clat.
+ uint32_t sock_uid = bpf_get_socket_uid(skb);
+ if (sock_uid == AID_CLAT) return BPF_NOMATCH;
+ if (sock_uid == AID_SYSTEM) {
+ uint64_t cookie = bpf_get_socket_cookie(skb);
+ UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
+ if (utag && utag->uid == AID_CLAT) return BPF_NOMATCH;
+ }
+
+ uint32_t key = skb->ifindex;
+ update_iface_stats_map(skb, BPF_EGRESS, &key);
+ return BPF_MATCH;
+}
+
+DEFINE_BPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
+(struct __sk_buff* skb) {
+ // Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule
+ // (in clat_raw_PREROUTING chain), which triggers before this (in bw_raw_PREROUTING chain).
+ // It will be accounted for on the v4-* clat interface instead.
+ // Keep that in mind when moving this out of iptables xt_bpf and into tc ingress (or xdp).
+
+ uint32_t key = skb->ifindex;
+ update_iface_stats_map(skb, BPF_INGRESS, &key);
+ return BPF_MATCH;
+}
+
+DEFINE_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN, tc_bpf_ingress_account_prog)
+(struct __sk_buff* skb) {
+ // Account for ingress traffic before tc drops it.
+ uint32_t key = skb->ifindex;
+ update_iface_stats_map(skb, BPF_INGRESS, &key);
+ return TC_ACT_UNSPEC;
+}
+
+DEFINE_BPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
+(struct __sk_buff* skb) {
+ uint32_t sock_uid = bpf_get_socket_uid(skb);
+ if (is_system_uid(sock_uid)) return BPF_MATCH;
+
+ // 65534 is the overflow 'nobody' uid, usually this being returned means
+ // that skb->sk is NULL during RX (early decap socket lookup failure),
+ // which commonly happens for incoming packets to an unconnected udp socket.
+ // Additionally bpf_get_socket_cookie() returns 0 if skb->sk is NULL
+ if ((sock_uid == 65534) && !bpf_get_socket_cookie(skb) && is_received_skb(skb))
+ return BPF_MATCH;
+
+ UidOwnerValue* allowlistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
+ if (allowlistMatch) return allowlistMatch->rule & HAPPY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
+ return BPF_NOMATCH;
+}
+
+DEFINE_BPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
+(struct __sk_buff* skb) {
+ uint32_t sock_uid = bpf_get_socket_uid(skb);
+ UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
+ if (denylistMatch) return denylistMatch->rule & PENALTY_BOX_MATCH ? BPF_MATCH : BPF_NOMATCH;
+ return BPF_NOMATCH;
+}
+
+DEFINE_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
+ KVER(4, 14, 0))
+(struct bpf_sock* sk) {
+ uint64_t gid_uid = bpf_get_current_uid_gid();
+ /*
+ * A given app is guaranteed to have the same app ID in all the profiles in
+ * which it is installed, and install permission is granted to app for all
+ * user at install time so we only check the appId part of a request uid at
+ * run time. See UserHandle#isSameApp for detail.
+ */
+ uint32_t appId = (gid_uid & 0xffffffff) % PER_USER_RANGE;
+ uint8_t* permissions = bpf_uid_permission_map_lookup_elem(&appId);
+ if (!permissions) {
+ // UID not in map. Default to just INTERNET permission.
+ return 1;
+ }
+
+ // A return value of 1 means allow, everything else means deny.
+ return (*permissions & BPF_PERMISSION_INTERNET) == BPF_PERMISSION_INTERNET;
+}
+
+LICENSE("Apache 2.0");
+CRITICAL("netd");
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 79bb128..abcfbeb 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -19,11 +19,22 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+java_defaults {
+ name: "enable-framework-connectivity-t-targets",
+ enabled: true,
+}
+// The above defaults can be used to disable framework-connectivity t
+// targets while minimizing merge conflicts in the build rules.
+
+
java_sdk_library {
name: "framework-connectivity-tiramisu",
sdk_version: "module_current",
min_sdk_version: "Tiramisu",
- defaults: ["framework-module-defaults"],
+ defaults: [
+ "framework-module-defaults",
+ "enable-framework-connectivity-t-targets",
+ ],
srcs: [
":framework-connectivity-tiramisu-updatable-sources",
],
diff --git a/framework/aidl-export/android/net/DscpPolicy.aidl b/framework/aidl-export/android/net/DscpPolicy.aidl
new file mode 100644
index 0000000..8da42ca
--- /dev/null
+++ b/framework/aidl-export/android/net/DscpPolicy.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+@JavaOnlyStableParcelable parcelable DscpPolicy;
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 827da6d..a373b71 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -294,7 +294,7 @@
ctor public NetworkCapabilities(android.net.NetworkCapabilities);
method public int describeContents();
method @NonNull public int[] getCapabilities();
- method @NonNull public int[] getEnterpriseCapabilitySubLevels();
+ method @NonNull public int[] getEnterpriseIds();
method public int getLinkDownstreamBandwidthKbps();
method public int getLinkUpstreamBandwidthKbps();
method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier();
@@ -302,6 +302,7 @@
method public int getSignalStrength();
method @Nullable public android.net.TransportInfo getTransportInfo();
method public boolean hasCapability(int);
+ method public boolean hasEnterpriseId(int);
method public boolean hasTransport(int);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkCapabilities> CREATOR;
@@ -325,6 +326,8 @@
field public static final int NET_CAPABILITY_NOT_ROAMING = 18; // 0x12
field public static final int NET_CAPABILITY_NOT_SUSPENDED = 21; // 0x15
field public static final int NET_CAPABILITY_NOT_VPN = 15; // 0xf
+ field public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35; // 0x23
+ field public static final int NET_CAPABILITY_PRIORITIZE_LATENCY = 34; // 0x22
field public static final int NET_CAPABILITY_RCS = 8; // 0x8
field public static final int NET_CAPABILITY_SUPL = 1; // 0x1
field public static final int NET_CAPABILITY_TEMPORARILY_NOT_METERED = 25; // 0x19
@@ -332,6 +335,11 @@
field public static final int NET_CAPABILITY_VALIDATED = 16; // 0x10
field public static final int NET_CAPABILITY_WIFI_P2P = 6; // 0x6
field public static final int NET_CAPABILITY_XCAP = 9; // 0x9
+ field public static final int NET_ENTERPRISE_ID_1 = 1; // 0x1
+ field public static final int NET_ENTERPRISE_ID_2 = 2; // 0x2
+ field public static final int NET_ENTERPRISE_ID_3 = 3; // 0x3
+ field public static final int NET_ENTERPRISE_ID_4 = 4; // 0x4
+ field public static final int NET_ENTERPRISE_ID_5 = 5; // 0x5
field public static final int SIGNAL_STRENGTH_UNSPECIFIED = -2147483648; // 0x80000000
field public static final int TRANSPORT_BLUETOOTH = 2; // 0x2
field public static final int TRANSPORT_CELLULAR = 0; // 0x0
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 415b3e5..8da421d 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -127,17 +127,13 @@
public static final class NetworkAgentConfig.Builder {
method @NonNull public android.net.NetworkAgentConfig.Builder setBypassableVpn(boolean);
+ method @NonNull public android.net.NetworkAgentConfig.Builder setExcludeLocalRoutesVpn(boolean);
method @NonNull public android.net.NetworkAgentConfig.Builder setSubscriberId(@Nullable String);
}
public final class NetworkCapabilities implements android.os.Parcelable {
method @Nullable public java.util.Set<android.util.Range<java.lang.Integer>> getUids();
method public boolean hasForbiddenCapability(int);
- field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1 = 1; // 0x1
- field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2 = 2; // 0x2
- field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3 = 3; // 0x3
- field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4 = 4; // 0x4
- field public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5 = 5; // 0x5
field public static final long REDACT_ALL = -1L; // 0xffffffffffffffffL
field public static final long REDACT_FOR_ACCESS_FINE_LOCATION = 1L; // 0x1L
field public static final long REDACT_FOR_LOCAL_MAC_ADDRESS = 2L; // 0x2L
@@ -151,7 +147,9 @@
}
public class NetworkRequest implements android.os.Parcelable {
+ method @NonNull public int[] getEnterpriseIds();
method @NonNull public int[] getForbiddenCapabilities();
+ method public boolean hasEnterpriseId(int);
method public boolean hasForbiddenCapability(int);
}
@@ -163,7 +161,10 @@
public final class ProfileNetworkPreference implements android.os.Parcelable {
method public int describeContents();
+ method @NonNull public java.util.List<java.lang.Integer> getExcludedUids();
+ method @NonNull public java.util.List<java.lang.Integer> getIncludedUids();
method public int getPreference();
+ method public int getPreferenceEnterpriseId();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.ProfileNetworkPreference> CREATOR;
}
@@ -171,7 +172,10 @@
public static final class ProfileNetworkPreference.Builder {
ctor public ProfileNetworkPreference.Builder();
method @NonNull public android.net.ProfileNetworkPreference build();
+ method @NonNull public android.net.ProfileNetworkPreference.Builder setExcludedUids(@Nullable java.util.List<java.lang.Integer>);
+ method @NonNull public android.net.ProfileNetworkPreference.Builder setIncludedUids(@Nullable java.util.List<java.lang.Integer>);
method @NonNull public android.net.ProfileNetworkPreference.Builder setPreference(int);
+ method @NonNull public android.net.ProfileNetworkPreference.Builder setPreferenceEnterpriseId(int);
}
public final class TestNetworkInterface implements android.os.Parcelable {
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 8d084e6..d420958 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -93,6 +93,35 @@
method @Deprecated public void onUpstreamChanged(@Nullable android.net.Network);
}
+ public final class DscpPolicy implements android.os.Parcelable {
+ method @Nullable public java.net.InetAddress getDestinationAddress();
+ method @Nullable public android.util.Range<java.lang.Integer> getDestinationPortRange();
+ method public int getDscpValue();
+ method public int getPolicyId();
+ method public int getProtocol();
+ method @Nullable public java.net.InetAddress getSourceAddress();
+ method public int getSourcePort();
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.DscpPolicy> CREATOR;
+ field public static final int PROTOCOL_ANY = -1; // 0xffffffff
+ field public static final int SOURCE_PORT_ANY = -1; // 0xffffffff
+ field public static final int STATUS_DELETED = 4; // 0x4
+ field public static final int STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3; // 0x3
+ field public static final int STATUS_POLICY_NOT_FOUND = 5; // 0x5
+ field public static final int STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2; // 0x2
+ field public static final int STATUS_REQUEST_DECLINED = 1; // 0x1
+ field public static final int STATUS_SUCCESS = 0; // 0x0
+ }
+
+ public static final class DscpPolicy.Builder {
+ ctor public DscpPolicy.Builder(int, int);
+ method @NonNull public android.net.DscpPolicy build();
+ method @NonNull public android.net.DscpPolicy.Builder setDestinationAddress(@NonNull java.net.InetAddress);
+ method @NonNull public android.net.DscpPolicy.Builder setDestinationPortRange(@NonNull android.util.Range<java.lang.Integer>);
+ method @NonNull public android.net.DscpPolicy.Builder setProtocol(int);
+ method @NonNull public android.net.DscpPolicy.Builder setSourceAddress(@NonNull java.net.InetAddress);
+ method @NonNull public android.net.DscpPolicy.Builder setSourcePort(int);
+ }
+
public final class InvalidPacketException extends java.lang.Exception {
ctor public InvalidPacketException(int);
method public int getError();
@@ -217,6 +246,7 @@
method public void onAddKeepalivePacketFilter(int, @NonNull android.net.KeepalivePacketData);
method public void onAutomaticReconnectDisabled();
method public void onBandwidthUpdateRequested();
+ method public void onDscpPolicyStatusUpdated(int, int);
method public void onNetworkCreated();
method public void onNetworkDestroyed();
method public void onNetworkUnwanted();
@@ -229,6 +259,7 @@
method public void onStopSocketKeepalive(int);
method public void onValidationStatus(int, @Nullable android.net.Uri);
method @NonNull public android.net.Network register();
+ method public void sendAddDscpPolicy(@NonNull android.net.DscpPolicy);
method public final void sendLinkProperties(@NonNull android.net.LinkProperties);
method public final void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
method public final void sendNetworkScore(@NonNull android.net.NetworkScore);
@@ -236,6 +267,8 @@
method public final void sendQosCallbackError(int, int);
method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes);
method public final void sendQosSessionLost(int, int, int);
+ method public void sendRemoveAllDscpPolicies();
+ method public void sendRemoveDscpPolicy(int);
method public final void sendSocketKeepaliveEvent(int, int);
method @Deprecated public void setLegacySubtype(int, @NonNull String);
method public void setLingerDuration(@NonNull java.time.Duration);
@@ -294,11 +327,11 @@
ctor public NetworkCapabilities.Builder();
ctor public NetworkCapabilities.Builder(@NonNull android.net.NetworkCapabilities);
method @NonNull public android.net.NetworkCapabilities.Builder addCapability(int);
- method @NonNull public android.net.NetworkCapabilities.Builder addEnterpriseCapabilitySubLevel(int);
+ method @NonNull public android.net.NetworkCapabilities.Builder addEnterpriseId(int);
method @NonNull public android.net.NetworkCapabilities.Builder addTransportType(int);
method @NonNull public android.net.NetworkCapabilities build();
method @NonNull public android.net.NetworkCapabilities.Builder removeCapability(int);
- method @NonNull public android.net.NetworkCapabilities.Builder removeEnterpriseCapabilitySubLevel(int);
+ method @NonNull public android.net.NetworkCapabilities.Builder removeEnterpriseId(int);
method @NonNull public android.net.NetworkCapabilities.Builder removeTransportType(int);
method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAdministratorUids(@NonNull int[]);
method @NonNull public android.net.NetworkCapabilities.Builder setLinkDownstreamBandwidthKbps(int);
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 7d65f8c..7593482 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -16,6 +16,7 @@
package android.net;
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST;
import static android.net.NetworkRequest.Type.LISTEN;
import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST;
@@ -5537,6 +5538,9 @@
ProfileNetworkPreference.Builder preferenceBuilder =
new ProfileNetworkPreference.Builder();
preferenceBuilder.setPreference(preference);
+ if (preference != PROFILE_NETWORK_PREFERENCE_DEFAULT) {
+ preferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ }
setProfileNetworkPreferences(profile,
List.of(preferenceBuilder.build()), executor, listener);
}
@@ -5612,7 +5616,7 @@
* background data is restricted.
*
* @param uid uid of target app
- * @throws IllegalStateException if update allow list failed.
+ * @throws IllegalStateException if updating allow list failed.
* @hide
*/
@SystemApi(client = MODULE_LIBRARIES)
@@ -5634,7 +5638,7 @@
* Takes precedence over {@link #updateMeteredNetworkAllowList}.
*
* @param uid uid of target app
- * @throws IllegalStateException if update deny list failed.
+ * @throws IllegalStateException if updating deny list failed.
* @hide
*/
@SystemApi(client = MODULE_LIBRARIES)
@@ -5656,8 +5660,8 @@
*
* @param chain target chain.
* @param uid uid to allow/deny.
- * @param allow either add or remove rule.
- * @throws IllegalStateException if update firewall rule failed.
+ * @param allow whether networking is allowed or denied.
+ * @throws IllegalStateException if updating firewall rule failed.
* @hide
*/
@SystemApi(client = MODULE_LIBRARIES)
@@ -5680,7 +5684,7 @@
*
* @param chain target chain.
* @param enable whether the chain should be enabled.
- * @throws IllegalStateException if set firewall chain failed.
+ * @throws IllegalStateException if enabling or disabling the firewall chain failed.
* @hide
*/
@SystemApi(client = MODULE_LIBRARIES)
@@ -5702,7 +5706,7 @@
*
* @param chain target chain to replace.
* @param uids The list of UIDs to be placed into chain.
- * @throws IllegalStateException if replace firewall chain failed.
+ * @throws IllegalStateException if replacing the firewall chain failed.
* @throws IllegalArgumentException if {@code chain} is not a valid chain.
* @hide
*/
@@ -5727,7 +5731,7 @@
* NetworkStatsFactory which is platform code but will be moved into connectivity (tethering)
* mainline module.
*
- * @throws IllegalStateException if swap active stats map failed.
+ * @throws IllegalStateException if swapping active stats map failed.
* @hide
*/
@SystemApi(client = MODULE_LIBRARIES)
diff --git a/framework/src/android/net/DscpPolicy.java b/framework/src/android/net/DscpPolicy.java
new file mode 100644
index 0000000..cda8205
--- /dev/null
+++ b/framework/src/android/net/DscpPolicy.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Range;
+
+import com.android.net.module.util.InetAddressUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.Objects;
+
+
+/**
+ * DSCP policy to be set on the requesting NetworkAgent.
+ * @hide
+ */
+@SystemApi
+public final class DscpPolicy implements Parcelable {
+ /**
+ * Indicates that the policy does not specify a protocol.
+ */
+ public static final int PROTOCOL_ANY = -1;
+
+ /**
+ * Indicates that the policy does not specify a port.
+ */
+ public static final int SOURCE_PORT_ANY = -1;
+
+ /**
+ * Policy was successfully added.
+ */
+ public static final int STATUS_SUCCESS = 0;
+
+ /**
+ * Policy was rejected for any reason besides invalid classifier or insufficient resources.
+ */
+ public static final int STATUS_REQUEST_DECLINED = 1;
+
+ /**
+ * Requested policy contained a classifier which is not supported.
+ */
+ public static final int STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED = 2;
+
+ /**
+ * TODO: should this error case be supported?
+ */
+ public static final int STATUS_INSUFFICIENT_PROCESSING_RESOURCES = 3;
+
+ /**
+ * Policy was deleted.
+ */
+ public static final int STATUS_DELETED = 4;
+
+ /**
+ * Policy was not found during deletion.
+ */
+ public static final int STATUS_POLICY_NOT_FOUND = 5;
+
+ /** The unique policy ID. Each requesting network is responsible for maintaining policy IDs
+ * unique within that network. In the case where a policy with an existing ID is created, the
+ * new policy will update the existing policy with the same ID.
+ */
+ private final int mPolicyId;
+
+ /** The QoS DSCP marking to be added to packets matching the policy. */
+ private final int mDscp;
+
+ /** The source IP address. */
+ private final @Nullable InetAddress mSrcAddr;
+
+ /** The destination IP address. */
+ private final @Nullable InetAddress mDstAddr;
+
+ /** The source port. */
+ private final int mSrcPort;
+
+ /** The IP protocol that the policy requires. */
+ private final int mProtocol;
+
+ /** Destination port range. Inclusive range. */
+ private final @Nullable Range<Integer> mDstPortRange;
+
+ /**
+ * Implement the Parcelable interface
+ *
+ * @hide
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @IntDef(prefix = "STATUS_", value = {
+ STATUS_SUCCESS,
+ STATUS_REQUEST_DECLINED,
+ STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED,
+ STATUS_INSUFFICIENT_PROCESSING_RESOURCES,
+ STATUS_DELETED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DscpPolicyStatus {}
+
+ /* package */ DscpPolicy(
+ int policyId,
+ int dscp,
+ @Nullable InetAddress srcAddr,
+ @Nullable InetAddress dstAddr,
+ int srcPort,
+ int protocol,
+ Range<Integer> dstPortRange) {
+ this.mPolicyId = policyId;
+ this.mDscp = dscp;
+ this.mSrcAddr = srcAddr;
+ this.mDstAddr = dstAddr;
+ this.mSrcPort = srcPort;
+ this.mProtocol = protocol;
+ this.mDstPortRange = dstPortRange;
+
+ if (mPolicyId < 1 || mPolicyId > 255) {
+ throw new IllegalArgumentException("Policy ID not in valid range: " + mPolicyId);
+ }
+ if (mDscp < 0 || mDscp > 63) {
+ throw new IllegalArgumentException("DSCP value not in valid range: " + mDscp);
+ }
+ // Since SOURCE_PORT_ANY is the default source port value need to allow it as well.
+ // TODO: Move the default value into this constructor or throw an error from the
+ // instead.
+ if (mSrcPort < -1 || mSrcPort > 65535) {
+ throw new IllegalArgumentException("Source port not in valid range: " + mSrcPort);
+ }
+ if (mDstPortRange != null
+ && (dstPortRange.getLower() < 0 || mDstPortRange.getLower() > 65535)
+ && (mDstPortRange.getUpper() < 0 || mDstPortRange.getUpper() > 65535)) {
+ throw new IllegalArgumentException("Destination port not in valid range");
+ }
+ if (mSrcAddr != null && mDstAddr != null && (mSrcAddr instanceof Inet6Address)
+ != (mDstAddr instanceof Inet6Address)) {
+ throw new IllegalArgumentException("Source/destination address of different family");
+ }
+ }
+
+ /**
+ * The unique policy ID.
+ *
+ * Each requesting network is responsible for maintaining unique
+ * policy IDs. In the case where a policy with an existing ID is created, the new
+ * policy will update the existing policy with the same ID
+ *
+ * @return Policy ID set in Builder.
+ */
+ public int getPolicyId() {
+ return mPolicyId;
+ }
+
+ /**
+ * The QoS DSCP marking to be added to packets matching the policy.
+ *
+ * @return DSCP value set in Builder.
+ */
+ public int getDscpValue() {
+ return mDscp;
+ }
+
+ /**
+ * The source IP address.
+ *
+ * @return Source IP address set in Builder or {@code null} if none was set.
+ */
+ public @Nullable InetAddress getSourceAddress() {
+ return mSrcAddr;
+ }
+
+ /**
+ * The destination IP address.
+ *
+ * @return Destination IP address set in Builder or {@code null} if none was set.
+ */
+ public @Nullable InetAddress getDestinationAddress() {
+ return mDstAddr;
+ }
+
+ /**
+ * The source port.
+ *
+ * @return Source port set in Builder or {@link #SOURCE_PORT_ANY} if no port was set.
+ */
+ public int getSourcePort() {
+ return mSrcPort;
+ }
+
+ /**
+ * The IP protocol that the policy requires.
+ *
+ * @return Protocol set in Builder or {@link #PROTOCOL_ANY} if no protocol was set.
+ * {@link #PROTOCOL_ANY} indicates that any protocol will be matched.
+ */
+ public int getProtocol() {
+ return mProtocol;
+ }
+
+ /**
+ * Destination port range. Inclusive range.
+ *
+ * @return Range<Integer> set in Builder or {@code null} if none was set.
+ */
+ public @Nullable Range<Integer> getDestinationPortRange() {
+ return mDstPortRange;
+ }
+
+ @Override
+ public String toString() {
+ return "DscpPolicy { "
+ + "policyId = " + mPolicyId + ", "
+ + "dscp = " + mDscp + ", "
+ + "srcAddr = " + mSrcAddr + ", "
+ + "dstAddr = " + mDstAddr + ", "
+ + "srcPort = " + mSrcPort + ", "
+ + "protocol = " + mProtocol + ", "
+ + "dstPortRange = "
+ + (mDstPortRange == null ? "none" : mDstPortRange.toString())
+ + " }";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DscpPolicy)) return false;
+ DscpPolicy that = (DscpPolicy) o;
+ return true
+ && mPolicyId == that.mPolicyId
+ && mDscp == that.mDscp
+ && Objects.equals(mSrcAddr, that.mSrcAddr)
+ && Objects.equals(mDstAddr, that.mDstAddr)
+ && mSrcPort == that.mSrcPort
+ && mProtocol == that.mProtocol
+ && Objects.equals(mDstPortRange, that.mDstPortRange);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPolicyId, mDscp, mSrcAddr.hashCode(),
+ mDstAddr.hashCode(), mSrcPort, mProtocol, mDstPortRange.hashCode());
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mPolicyId);
+ dest.writeInt(mDscp);
+ InetAddressUtils.parcelInetAddress(dest, mSrcAddr, flags);
+ InetAddressUtils.parcelInetAddress(dest, mDstAddr, flags);
+ dest.writeInt(mSrcPort);
+ dest.writeInt(mProtocol);
+ dest.writeBoolean(mDstPortRange != null ? true : false);
+ if (mDstPortRange != null) {
+ dest.writeInt(mDstPortRange.getLower());
+ dest.writeInt(mDstPortRange.getUpper());
+ }
+ }
+
+ /** @hide */
+ DscpPolicy(@NonNull Parcel in) {
+ this.mPolicyId = in.readInt();
+ this.mDscp = in.readInt();
+ this.mSrcAddr = InetAddressUtils.unparcelInetAddress(in);
+ this.mDstAddr = InetAddressUtils.unparcelInetAddress(in);
+ this.mSrcPort = in.readInt();
+ this.mProtocol = in.readInt();
+ if (in.readBoolean()) {
+ this.mDstPortRange = new Range<Integer>(in.readInt(), in.readInt());
+ } else {
+ this.mDstPortRange = null;
+ }
+ }
+
+ /** @hide */
+ public @SystemApi static final @NonNull Parcelable.Creator<DscpPolicy> CREATOR =
+ new Parcelable.Creator<DscpPolicy>() {
+ @Override
+ public DscpPolicy[] newArray(int size) {
+ return new DscpPolicy[size];
+ }
+
+ @Override
+ public DscpPolicy createFromParcel(@NonNull android.os.Parcel in) {
+ return new DscpPolicy(in);
+ }
+ };
+
+ /**
+ * A builder for {@link DscpPolicy}
+ *
+ */
+ public static final class Builder {
+
+ private final int mPolicyId;
+ private final int mDscp;
+ private @Nullable InetAddress mSrcAddr;
+ private @Nullable InetAddress mDstAddr;
+ private int mSrcPort = SOURCE_PORT_ANY;
+ private int mProtocol = PROTOCOL_ANY;
+ private @Nullable Range<Integer> mDstPortRange;
+
+ private long mBuilderFieldsSet = 0L;
+
+ /**
+ * Creates a new Builder.
+ *
+ * @param policyId The unique policy ID. Each requesting network is responsible for
+ * maintaining unique policy IDs. In the case where a policy with an
+ * existing ID is created, the new policy will update the existing
+ * policy with the same ID
+ * @param dscpValue The DSCP value to set.
+ */
+ public Builder(int policyId, int dscpValue) {
+ mPolicyId = policyId;
+ mDscp = dscpValue;
+ }
+
+ /**
+ * Specifies that this policy matches packets with the specified source IP address.
+ */
+ public @NonNull Builder setSourceAddress(@NonNull InetAddress value) {
+ mSrcAddr = value;
+ return this;
+ }
+
+ /**
+ * Specifies that this policy matches packets with the specified destination IP address.
+ */
+ public @NonNull Builder setDestinationAddress(@NonNull InetAddress value) {
+ mDstAddr = value;
+ return this;
+ }
+
+ /**
+ * Specifies that this policy matches packets with the specified source port.
+ */
+ public @NonNull Builder setSourcePort(int value) {
+ mSrcPort = value;
+ return this;
+ }
+
+ /**
+ * Specifies that this policy matches packets with the specified protocol.
+ */
+ public @NonNull Builder setProtocol(int value) {
+ mProtocol = value;
+ return this;
+ }
+
+ /**
+ * Specifies that this policy matches packets with the specified destination port range.
+ */
+ public @NonNull Builder setDestinationPortRange(@NonNull Range<Integer> range) {
+ mDstPortRange = range;
+ return this;
+ }
+
+ /**
+ * Constructs a DscpPolicy with the specified parameters.
+ */
+ public @NonNull DscpPolicy build() {
+ return new DscpPolicy(
+ mPolicyId,
+ mDscp,
+ mSrcAddr,
+ mDstAddr,
+ mSrcPort,
+ mProtocol,
+ mDstPortRange);
+ }
+ }
+}
diff --git a/framework/src/android/net/INetworkAgent.aidl b/framework/src/android/net/INetworkAgent.aidl
index d941d4b..fa5175c 100644
--- a/framework/src/android/net/INetworkAgent.aidl
+++ b/framework/src/android/net/INetworkAgent.aidl
@@ -48,4 +48,5 @@
void onQosCallbackUnregistered(int qosCallbackId);
void onNetworkCreated();
void onNetworkDestroyed();
+ void onDscpPolicyStatusUpdated(int policyId, int status);
}
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl
index 9a58add..08536ca 100644
--- a/framework/src/android/net/INetworkAgentRegistry.aidl
+++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -15,6 +15,7 @@
*/
package android.net;
+import android.net.DscpPolicy;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
@@ -43,4 +44,7 @@
void sendQosCallbackError(int qosCallbackId, int exceptionType);
void sendTeardownDelayMs(int teardownDelayMs);
void sendLingerDuration(int durationMs);
+ void sendAddDscpPolicy(in DscpPolicy policy);
+ void sendRemoveDscpPolicy(int policyId);
+ void sendRemoveAllDscpPolicies();
}
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index adcf338..945e670 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -25,6 +25,7 @@
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
+import android.net.DscpPolicy.DscpPolicyStatus;
import android.os.Build;
import android.os.Bundle;
import android.os.ConditionVariable;
@@ -404,6 +405,35 @@
*/
public static final int EVENT_LINGER_DURATION_CHANGED = BASE + 24;
+ /**
+ * Sent by the NetworkAgent to ConnectivityService to set add a DSCP policy.
+ *
+ * @hide
+ */
+ public static final int EVENT_ADD_DSCP_POLICY = BASE + 25;
+
+ /**
+ * Sent by the NetworkAgent to ConnectivityService to set remove a DSCP policy.
+ *
+ * @hide
+ */
+ public static final int EVENT_REMOVE_DSCP_POLICY = BASE + 26;
+
+ /**
+ * Sent by the NetworkAgent to ConnectivityService to remove all DSCP policies.
+ *
+ * @hide
+ */
+ public static final int EVENT_REMOVE_ALL_DSCP_POLICIES = BASE + 27;
+
+ /**
+ * Sent by ConnectivityService to {@link NetworkAgent} to inform the agent of an updated
+ * status for a DSCP policy.
+ *
+ * @hide
+ */
+ public static final int CMD_DSCP_POLICY_STATUS = BASE + 28;
+
private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) {
final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType,
config.legacyTypeName, config.legacySubTypeName);
@@ -611,6 +641,12 @@
onNetworkDestroyed();
break;
}
+ case CMD_DSCP_POLICY_STATUS: {
+ onDscpPolicyStatusUpdated(
+ msg.arg1 /* Policy ID */,
+ msg.arg2 /* DSCP Policy Status */);
+ break;
+ }
}
}
}
@@ -761,6 +797,13 @@
public void onNetworkDestroyed() {
mHandler.sendMessage(mHandler.obtainMessage(CMD_NETWORK_DESTROYED));
}
+
+ @Override
+ public void onDscpPolicyStatusUpdated(final int policyId,
+ @DscpPolicyStatus final int status) {
+ mHandler.sendMessage(mHandler.obtainMessage(
+ CMD_DSCP_POLICY_STATUS, policyId, status));
+ }
}
/**
@@ -1104,6 +1147,11 @@
public void onNetworkDestroyed() {}
/**
+ * Called when when the DSCP Policy status has changed.
+ */
+ public void onDscpPolicyStatusUpdated(int policyId, @DscpPolicyStatus int status) {}
+
+ /**
* Requests that the network hardware send the specified packet at the specified interval.
*
* @param slot the hardware slot on which to start the keepalive.
@@ -1317,6 +1365,30 @@
queueOrSendMessage(ra -> ra.sendLingerDuration((int) durationMs));
}
+ /**
+ * Add a DSCP Policy.
+ * @param policy the DSCP policy to be added.
+ */
+ public void sendAddDscpPolicy(@NonNull final DscpPolicy policy) {
+ Objects.requireNonNull(policy);
+ queueOrSendMessage(ra -> ra.sendAddDscpPolicy(policy));
+ }
+
+ /**
+ * Remove the specified DSCP policy.
+ * @param policyId the ID corresponding to a specific DSCP Policy.
+ */
+ public void sendRemoveDscpPolicy(final int policyId) {
+ queueOrSendMessage(ra -> ra.sendRemoveDscpPolicy(policyId));
+ }
+
+ /**
+ * Remove all the DSCP policies on this network.
+ */
+ public void sendRemoveAllDscpPolicies() {
+ queueOrSendMessage(ra -> ra.sendRemoveAllDscpPolicies());
+ }
+
/** @hide */
protected void log(final String s) {
Log.d(LOG_TAG, "NetworkAgent: " + s);
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
index 93fc379..040bf31 100644
--- a/framework/src/android/net/NetworkAgentConfig.java
+++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -425,8 +425,10 @@
* Sets whether the local traffic is exempted from VPN.
*
* @return this builder, to facilitate chaining.
- * @hide TODO(184750836): Unhide once the implementation is completed.
+ * @hide
*/
+ @NonNull
+ @SystemApi(client = MODULE_LIBRARIES)
public Builder setExcludeLocalRoutesVpn(boolean excludeLocalRoutes) {
mConfig.excludeLocalRouteVpn = excludeLocalRoutes;
return this;
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 84f7cbb..4ae3a06 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -16,8 +16,6 @@
package android.net;
-import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
-
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import android.annotation.IntDef;
@@ -149,67 +147,83 @@
private String mRequestorPackageName;
/**
- * enterprise capability sub level 1
- * @hide
+ * Enterprise capability identifier 1.
*/
- @SystemApi(client = MODULE_LIBRARIES)
- public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1 = 1;
+ public static final int NET_ENTERPRISE_ID_1 = 1;
/**
- * enterprise capability sub level 2
- * @hide
+ * Enterprise capability identifier 2.
*/
- @SystemApi(client = MODULE_LIBRARIES)
- public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2 = 2;
+ public static final int NET_ENTERPRISE_ID_2 = 2;
/**
- * enterprise capability sub level 3
- * @hide
+ * Enterprise capability identifier 3.
*/
- @SystemApi(client = MODULE_LIBRARIES)
- public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3 = 3;
+ public static final int NET_ENTERPRISE_ID_3 = 3;
/**
- * enterprise capability sub level 4
- * @hide
+ * Enterprise capability identifier 4.
*/
- @SystemApi(client = MODULE_LIBRARIES)
- public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4 = 4;
+ public static final int NET_ENTERPRISE_ID_4 = 4;
/**
- * enterprise capability sub level 5
- * @hide
+ * Enterprise capability identifier 5.
*/
- @SystemApi(client = MODULE_LIBRARIES)
- public static final int NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5 = 5;
+ public static final int NET_ENTERPRISE_ID_5 = 5;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "NET_CAPABILITY_ENTERPRISE_SUB_LEVEL" }, value = {
- NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1,
- NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2,
- NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3,
- NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4,
- NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5,
+ NET_ENTERPRISE_ID_1,
+ NET_ENTERPRISE_ID_2,
+ NET_ENTERPRISE_ID_3,
+ NET_ENTERPRISE_ID_4,
+ NET_ENTERPRISE_ID_5,
})
- public @interface EnterpriseCapabilitySubLevel {
+ public @interface EnterpriseId {
}
/**
- * Bitfield representing the network's enterprise capability sublevel. If any are specified
+ * Bitfield representing the network's enterprise capability identifier. If any are specified
* they will be satisfied by any Network that matches all of them.
- * {@see addEnterpriseCapabilitySubLevel} for details on how masks are added
+ * {@see addEnterpriseId} for details on how masks are added
*/
- private int mEnterpriseCapabilitySubLevel;
+ private int mEnterpriseId;
/**
- * @return all the enteprise capabilities sub level set on this {@code NetworkCapability}
- * instance.
+ * Get enteprise identifiers set.
+ *
+ * Get all the enterprise capabilities identifier set on this {@code NetworkCapability}
+ * If NET_CAPABILITY_ENTERPRISE is set and no enterprise ID is set, it is
+ * considered to have NET_CAPABILITY_ENTERPRISE by default.
+ * @return all the enterprise capabilities identifier set.
*
*/
- public @NonNull @EnterpriseCapabilitySubLevel int[] getEnterpriseCapabilitySubLevels() {
- return NetworkCapabilitiesUtils.unpackBits(mEnterpriseCapabilitySubLevel);
+ public @NonNull @EnterpriseId int[] getEnterpriseIds() {
+ if (hasCapability(NET_CAPABILITY_ENTERPRISE) && mEnterpriseId == 0) {
+ return new int[]{NET_ENTERPRISE_ID_1};
+ }
+ return NetworkCapabilitiesUtils.unpackBits(mEnterpriseId);
+ }
+
+ /**
+ * Tests for the presence of an enterprise capability identifier on this instance.
+ *
+ * If NET_CAPABILITY_ENTERPRISE is set and no enterprise ID is set, it is
+ * considered to have NET_CAPABILITY_ENTERPRISE by default.
+ * @param enterpriseId the enterprise capability identifier to be tested for.
+ * @return {@code true} if set on this instance.
+ */
+ public boolean hasEnterpriseId(
+ @EnterpriseId int enterpriseId) {
+ if (enterpriseId == NET_ENTERPRISE_ID_1) {
+ if (hasCapability(NET_CAPABILITY_ENTERPRISE) && mEnterpriseId == 0) {
+ return true;
+ }
+ }
+ return isValidEnterpriseId(enterpriseId)
+ && ((mEnterpriseId & (1L << enterpriseId)) != 0);
}
public NetworkCapabilities() {
@@ -258,7 +272,7 @@
mRequestorPackageName = null;
mSubIds = new ArraySet<>();
mUnderlyingNetworks = null;
- mEnterpriseCapabilitySubLevel = 0;
+ mEnterpriseId = 0;
}
/**
@@ -291,7 +305,7 @@
// mUnderlyingNetworks is an unmodifiable list if non-null, so a defensive copy is not
// necessary.
mUnderlyingNetworks = nc.mUnderlyingNetworks;
- mEnterpriseCapabilitySubLevel = nc.mEnterpriseCapabilitySubLevel;
+ mEnterpriseId = nc.mEnterpriseId;
}
/**
@@ -343,6 +357,8 @@
NET_CAPABILITY_BIP,
NET_CAPABILITY_HEAD_UNIT,
NET_CAPABILITY_MMTEL,
+ NET_CAPABILITY_PRIORITIZE_LATENCY,
+ NET_CAPABILITY_PRIORITIZE_BANDWIDTH,
})
public @interface NetCapability { }
@@ -586,8 +602,18 @@
*/
public static final int NET_CAPABILITY_MMTEL = 33;
+ /**
+ * Indicates that this network should be able to prioritize latency for the internet.
+ */
+ public static final int NET_CAPABILITY_PRIORITIZE_LATENCY = 34;
+
+ /**
+ * Indicates that this network should be able to prioritize bandwidth for the internet.
+ */
+ public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35;
+
private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS;
- private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_MMTEL;
+ private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
/**
* Network capabilities that are expected to be mutable, i.e., can change while a particular
@@ -784,34 +810,34 @@
}
/**
- * Adds the given enterprise capability sub level to this {@code NetworkCapability} instance.
- * Note that when searching for a network to satisfy a request, all capabilities sub level
+ * Adds the given enterprise capability identifier to this {@code NetworkCapability} instance.
+ * Note that when searching for a network to satisfy a request, all capabilities identifier
* requested must be satisfied.
*
- * @param enterpriseCapabilitySubLevel the enterprise capability sub level to be added.
+ * @param enterpriseId the enterprise capability identifier to be added.
* @return This NetworkCapabilities instance, to facilitate chaining.
* @hide
*/
- private @NonNull NetworkCapabilities addEnterpriseCapabilitySubLevel(
- @EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) {
- checkValidEnterpriseCapabilitySublevel(enterpriseCapabilitySubLevel);
- mEnterpriseCapabilitySubLevel |= 1 << enterpriseCapabilitySubLevel;
+ public @NonNull NetworkCapabilities addEnterpriseId(
+ @EnterpriseId int enterpriseId) {
+ checkValidEnterpriseId(enterpriseId);
+ mEnterpriseId |= 1 << enterpriseId;
return this;
}
/**
- * Removes (if found) the given enterprise capability sublevel from this
- * {@code NetworkCapability} instance that were added via addEnterpriseCapabilitySubLevel(int)
+ * Removes (if found) the given enterprise capability identifier from this
+ * {@code NetworkCapability} instance that were added via addEnterpriseId(int)
*
- * @param enterpriseCapabilitySubLevel the enterprise capability sublevel to be removed.
+ * @param enterpriseId the enterprise capability identifier to be removed.
* @return This NetworkCapabilities instance, to facilitate chaining.
* @hide
*/
- private @NonNull NetworkCapabilities removeEnterpriseCapabilitySubLevel(
- @EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) {
- checkValidEnterpriseCapabilitySublevel(enterpriseCapabilitySubLevel);
- final int mask = ~(1 << enterpriseCapabilitySubLevel);
- mEnterpriseCapabilitySubLevel &= mask;
+ private @NonNull NetworkCapabilities removeEnterpriseId(
+ @EnterpriseId int enterpriseId) {
+ checkValidEnterpriseId(enterpriseId);
+ final int mask = ~(1 << enterpriseId);
+ mEnterpriseId &= mask;
return this;
}
@@ -915,16 +941,19 @@
return null;
}
- private boolean equalsEnterpriseCapabilitiesSubLevel(@NonNull NetworkCapabilities nc) {
- return nc.mEnterpriseCapabilitySubLevel == this.mEnterpriseCapabilitySubLevel;
+ private boolean equalsEnterpriseCapabilitiesId(@NonNull NetworkCapabilities nc) {
+ return nc.mEnterpriseId == this.mEnterpriseId;
}
- private boolean satisfiedByEnterpriseCapabilitiesSubLevel(@NonNull NetworkCapabilities nc) {
- final int requestedEnterpriseCapabilitiesSubLevel = mEnterpriseCapabilitySubLevel;
- final int providedEnterpriseCapabailitiesSubLevel = nc.mEnterpriseCapabilitySubLevel;
+ private boolean satisfiedByEnterpriseCapabilitiesId(@NonNull NetworkCapabilities nc) {
+ final int requestedEnterpriseCapabilitiesId = mEnterpriseId;
+ final int providedEnterpriseCapabailitiesId = nc.mEnterpriseId;
- if ((providedEnterpriseCapabailitiesSubLevel & requestedEnterpriseCapabilitiesSubLevel)
- == requestedEnterpriseCapabilitiesSubLevel) {
+ if ((providedEnterpriseCapabailitiesId & requestedEnterpriseCapabilitiesId)
+ == requestedEnterpriseCapabilitiesId) {
+ return true;
+ } else if (providedEnterpriseCapabailitiesId == 0
+ && (requestedEnterpriseCapabilitiesId == (1L << NET_ENTERPRISE_ID_1))) {
return true;
} else {
return false;
@@ -1836,7 +1865,7 @@
&& satisfiedByTransportTypes(nc)
&& (onlyImmutable || satisfiedByLinkBandwidths(nc))
&& satisfiedBySpecifier(nc)
- && satisfiedByEnterpriseCapabilitiesSubLevel(nc)
+ && satisfiedByEnterpriseCapabilitiesId(nc)
&& (onlyImmutable || satisfiedBySignalStrength(nc))
&& (onlyImmutable || satisfiedByUids(nc))
&& (onlyImmutable || satisfiedBySSID(nc))
@@ -1940,7 +1969,7 @@
&& equalsAdministratorUids(that)
&& equalsSubscriptionIds(that)
&& equalsUnderlyingNetworks(that)
- && equalsEnterpriseCapabilitiesSubLevel(that);
+ && equalsEnterpriseCapabilitiesId(that);
}
@Override
@@ -1965,7 +1994,7 @@
+ Arrays.hashCode(mAdministratorUids) * 61
+ Objects.hashCode(mSubIds) * 67
+ Objects.hashCode(mUnderlyingNetworks) * 71
- + mEnterpriseCapabilitySubLevel * 73;
+ + mEnterpriseId * 73;
}
@Override
@@ -2001,7 +2030,7 @@
dest.writeString(mRequestorPackageName);
dest.writeIntArray(CollectionUtils.toIntArray(mSubIds));
dest.writeTypedList(mUnderlyingNetworks);
- dest.writeInt(mEnterpriseCapabilitySubLevel);
+ dest.writeInt(mEnterpriseId);
}
public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
@@ -2031,7 +2060,7 @@
netCap.mSubIds.add(subIdInts[i]);
}
netCap.setUnderlyingNetworks(in.createTypedArrayList(Network.CREATOR));
- netCap.mEnterpriseCapabilitySubLevel = in.readInt();
+ netCap.mEnterpriseId = in.readInt();
return netCap;
}
@Override
@@ -2123,10 +2152,10 @@
sb.append(" SubscriptionIds: ").append(mSubIds);
}
- if (0 != mEnterpriseCapabilitySubLevel) {
- sb.append(" EnterpriseCapabilitySublevel: ");
- appendStringRepresentationOfBitMaskToStringBuilder(sb, mEnterpriseCapabilitySubLevel,
- NetworkCapabilities::enterpriseCapabilitySublevelNameOf, "&");
+ if (0 != mEnterpriseId) {
+ sb.append(" EnterpriseId: ");
+ appendStringRepresentationOfBitMaskToStringBuilder(sb, mEnterpriseId,
+ NetworkCapabilities::enterpriseIdNameOf, "&");
}
sb.append(" UnderlyingNetworks: ");
@@ -2224,11 +2253,13 @@
case NET_CAPABILITY_BIP: return "BIP";
case NET_CAPABILITY_HEAD_UNIT: return "HEAD_UNIT";
case NET_CAPABILITY_MMTEL: return "MMTEL";
+ case NET_CAPABILITY_PRIORITIZE_LATENCY: return "PRIORITIZE_LATENCY";
+ case NET_CAPABILITY_PRIORITIZE_BANDWIDTH: return "PRIORITIZE_BANDWIDTH";
default: return Integer.toString(capability);
}
}
- private static @NonNull String enterpriseCapabilitySublevelNameOf(
+ private static @NonNull String enterpriseIdNameOf(
@NetCapability int capability) {
return Integer.toString(capability);
}
@@ -2273,17 +2304,17 @@
}
}
- private static boolean isValidEnterpriseCapabilitySubLevel(
- @NetworkCapabilities.EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) {
- return enterpriseCapabilitySubLevel >= NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1
- && enterpriseCapabilitySubLevel <= NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5;
+ private static boolean isValidEnterpriseId(
+ @NetworkCapabilities.EnterpriseId int enterpriseId) {
+ return enterpriseId >= NET_ENTERPRISE_ID_1
+ && enterpriseId <= NET_ENTERPRISE_ID_5;
}
- private static void checkValidEnterpriseCapabilitySublevel(
- @NetworkCapabilities.EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) {
- if (!isValidEnterpriseCapabilitySubLevel(enterpriseCapabilitySubLevel)) {
- throw new IllegalArgumentException("enterprise capability sublevel "
- + enterpriseCapabilitySubLevel + " is out of range");
+ private static void checkValidEnterpriseId(
+ @NetworkCapabilities.EnterpriseId int enterpriseId) {
+ if (!isValidEnterpriseId(enterpriseId)) {
+ throw new IllegalArgumentException("enterprise capability identifier "
+ + enterpriseId + " is out of range");
}
}
@@ -2613,33 +2644,33 @@
}
/**
- * Adds the given enterprise capability sub level.
- * Note that when searching for a network to satisfy a request, all capabilities sub level
- * requested must be satisfied. Enterprise capability sub-level is applicable only
+ * Adds the given enterprise capability identifier.
+ * Note that when searching for a network to satisfy a request, all capabilities identifier
+ * requested must be satisfied. Enterprise capability identifier is applicable only
* for NET_CAPABILITY_ENTERPRISE capability
*
- * @param enterpriseCapabilitySubLevel enterprise capability sub-level.
+ * @param enterpriseId enterprise capability identifier.
*
* @return this builder
*/
@NonNull
- public Builder addEnterpriseCapabilitySubLevel(
- @EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) {
- mCaps.addEnterpriseCapabilitySubLevel(enterpriseCapabilitySubLevel);
+ public Builder addEnterpriseId(
+ @EnterpriseId int enterpriseId) {
+ mCaps.addEnterpriseId(enterpriseId);
return this;
}
/**
- * Removes the given enterprise capability sub level. Enterprise capability sub-level is
+ * Removes the given enterprise capability identifier. Enterprise capability identifier is
* applicable only for NET_CAPABILITY_ENTERPRISE capability
*
- * @param enterpriseCapabilitySubLevel the enterprise capability subLevel
+ * @param enterpriseId the enterprise capability identifier
* @return this builder
*/
@NonNull
- public Builder removeEnterpriseCapabilitySubLevel(
- @EnterpriseCapabilitySubLevel int enterpriseCapabilitySubLevel) {
- mCaps.removeEnterpriseCapabilitySubLevel(enterpriseCapabilitySubLevel);
+ public Builder removeEnterpriseId(
+ @EnterpriseId int enterpriseId) {
+ mCaps.removeEnterpriseId(enterpriseId);
return this;
}
@@ -2902,10 +2933,10 @@
}
}
- if ((mCaps.getEnterpriseCapabilitySubLevels().length != 0)
+ if ((mCaps.getEnterpriseIds().length != 0)
&& !mCaps.hasCapability(NET_CAPABILITY_ENTERPRISE)) {
- throw new IllegalStateException("Enterprise capability sublevel is applicable only"
- + " with ENTERPRISE capability.");
+ throw new IllegalStateException("Enterprise capability identifier is applicable"
+ + " only with ENTERPRISE capability.");
}
return new NetworkCapabilities(mCaps);
}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index afc76d6..b7a6076 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -725,6 +725,33 @@
}
/**
+ * Get the enteprise identifiers.
+ *
+ * Get all the enterprise identifiers set on this {@code NetworkCapability}
+ * @return array of all the enterprise identifiers.
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public @NonNull @NetworkCapabilities.EnterpriseId int[] getEnterpriseIds() {
+ // No need to make a defensive copy here as NC#getCapabilities() already returns
+ // a new array.
+ return networkCapabilities.getEnterpriseIds();
+ }
+
+ /**
+ * Tests for the presence of an enterprise identifier on this instance.
+ *
+ * @param enterpriseId the enterprise capability identifier to be tested for.
+ * @return {@code true} if set on this instance.
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public boolean hasEnterpriseId(
+ @NetworkCapabilities.EnterpriseId int enterpriseId) {
+ return networkCapabilities.hasEnterpriseId(enterpriseId);
+ }
+
+ /**
* Gets all the forbidden capabilities set on this {@code NetworkRequest} instance.
*
* @return an array of forbidden capability values for this instance.
diff --git a/framework/src/android/net/ProfileNetworkPreference.java b/framework/src/android/net/ProfileNetworkPreference.java
index 2ce1698..f43acce 100644
--- a/framework/src/android/net/ProfileNetworkPreference.java
+++ b/framework/src/android/net/ProfileNetworkPreference.java
@@ -18,13 +18,20 @@
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.net.ConnectivityManager.ProfileNetworkPreferencePolicy;
import android.os.Parcel;
import android.os.Parcelable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
/**
* Network preferences to be set for the user profile
* {@link ProfileNetworkPreferencePolicy}.
@@ -33,23 +40,91 @@
@SystemApi(client = MODULE_LIBRARIES)
public final class ProfileNetworkPreference implements Parcelable {
private final @ProfileNetworkPreferencePolicy int mPreference;
+ private final @NetworkCapabilities.EnterpriseId int mPreferenceEnterpriseId;
+ private final List<Integer> mIncludedUids;
+ private final List<Integer> mExcludedUids;
- private ProfileNetworkPreference(int preference) {
+ private ProfileNetworkPreference(int preference, List<Integer> includedUids,
+ List<Integer> excludedUids,
+ @NetworkCapabilities.EnterpriseId int preferenceEnterpriseId) {
mPreference = preference;
+ mPreferenceEnterpriseId = preferenceEnterpriseId;
+ if (includedUids != null) {
+ mIncludedUids = new ArrayList<>(includedUids);
+ } else {
+ mIncludedUids = new ArrayList<>();
+ }
+
+ if (excludedUids != null) {
+ mExcludedUids = new ArrayList<>(excludedUids);
+ } else {
+ mExcludedUids = new ArrayList<>();
+ }
}
private ProfileNetworkPreference(Parcel in) {
mPreference = in.readInt();
+ mIncludedUids = in.readArrayList(Integer.class.getClassLoader());
+ mExcludedUids = in.readArrayList(Integer.class.getClassLoader());
+ mPreferenceEnterpriseId = in.readInt();
}
public int getPreference() {
return mPreference;
}
+ /**
+ * Get the list of UIDs subject to this preference.
+ *
+ * Included UIDs and Excluded UIDs can't both be non-empty.
+ * if both are empty, it means this request applies to all uids in the user profile.
+ * if included is not empty, then only included UIDs are applied.
+ * if excluded is not empty, then it is all uids in the user profile except these UIDs.
+ * @return List of uids included for the profile preference.
+ * {@see #getExcludedUids()}
+ */
+ public @NonNull List<Integer> getIncludedUids() {
+ return new ArrayList<>(mIncludedUids);
+ }
+
+ /**
+ * Get the list of UIDS excluded from this preference.
+ *
+ * <ul>Included UIDs and Excluded UIDs can't both be non-empty.</ul>
+ * <ul>If both are empty, it means this request applies to all uids in the user profile.</ul>
+ * <ul>If included is not empty, then only included UIDs are applied.</ul>
+ * <ul>If excluded is not empty, then it is all uids in the user profile except these UIDs.</ul>
+ * @return List of uids not included for the profile preference.
+ * {@see #getIncludedUids()}
+ */
+ public @NonNull List<Integer> getExcludedUids() {
+ return new ArrayList<>(mExcludedUids);
+ }
+
+ /**
+ * Get preference enterprise identifier.
+ *
+ * Preference enterprise identifier will be used to create different network preferences
+ * within enterprise preference category.
+ * Valid values starts from PROFILE_NETWORK_PREFERENCE_ENTERPRISE_ID_1 to
+ * NetworkCapabilities.NET_ENTERPRISE_ID_5.
+ * Preference identifier is not applicable if preference is set as
+ * PROFILE_NETWORK_PREFERENCE_DEFAULT. Default value is
+ * NetworkCapabilities.NET_ENTERPRISE_ID_1.
+ * @return Preference enterprise identifier.
+ *
+ */
+ public @NetworkCapabilities.EnterpriseId int getPreferenceEnterpriseId() {
+ return mPreferenceEnterpriseId;
+ }
+
@Override
public String toString() {
return "ProfileNetworkPreference{"
+ "mPreference=" + getPreference()
+ + "mIncludedUids=" + mIncludedUids.toString()
+ + "mExcludedUids=" + mExcludedUids.toString()
+ + "mPreferenceEnterpriseId=" + mPreferenceEnterpriseId
+ '}';
}
@@ -58,12 +133,18 @@
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final ProfileNetworkPreference that = (ProfileNetworkPreference) o;
- return mPreference == that.mPreference;
+ return mPreference == that.mPreference
+ && (Objects.equals(mIncludedUids, that.mIncludedUids))
+ && (Objects.equals(mExcludedUids, that.mExcludedUids))
+ && mPreferenceEnterpriseId == that.mPreferenceEnterpriseId;
}
@Override
public int hashCode() {
- return mPreference;
+ return mPreference
+ + mPreferenceEnterpriseId * 2
+ + (Objects.hashCode(mIncludedUids) * 11)
+ + (Objects.hashCode(mExcludedUids) * 13);
}
/**
@@ -73,6 +154,9 @@
public static final class Builder {
private @ProfileNetworkPreferencePolicy int mPreference =
PROFILE_NETWORK_PREFERENCE_DEFAULT;
+ private @NonNull List<Integer> mIncludedUids = new ArrayList<>();
+ private @NonNull List<Integer> mExcludedUids = new ArrayList<>();
+ private int mPreferenceEnterpriseId;
/**
* Constructs an empty Builder with PROFILE_NETWORK_PREFERENCE_DEFAULT profile preference
@@ -93,18 +177,112 @@
}
/**
+ * This is a list of uids for which profile perefence is set.
+ * Null would mean that this preference applies to all uids in the profile.
+ * {@see #setExcludedUids(List<Integer>)}
+ * Included UIDs and Excluded UIDs can't both be non-empty.
+ * if both are empty, it means this request applies to all uids in the user profile.
+ * if included is not empty, then only included UIDs are applied.
+ * if excluded is not empty, then it is all uids in the user profile except these UIDs.
+ * @param uids list of uids that are included
+ * @return The builder to facilitate chaining.
+ */
+ @NonNull
+ public Builder setIncludedUids(@Nullable List<Integer> uids) {
+ if (uids != null) {
+ mIncludedUids = new ArrayList<Integer>(uids);
+ } else {
+ mIncludedUids = new ArrayList<Integer>();
+ }
+ return this;
+ }
+
+
+ /**
+ * This is a list of uids that are excluded for the profile perefence.
+ * {@see #setIncludedUids(List<Integer>)}
+ * Included UIDs and Excluded UIDs can't both be non-empty.
+ * if both are empty, it means this request applies to all uids in the user profile.
+ * if included is not empty, then only included UIDs are applied.
+ * if excluded is not empty, then it is all uids in the user profile except these UIDs.
+ * @param uids list of uids that are not included
+ * @return The builder to facilitate chaining.
+ */
+ @NonNull
+ public Builder setExcludedUids(@Nullable List<Integer> uids) {
+ if (uids != null) {
+ mExcludedUids = new ArrayList<Integer>(uids);
+ } else {
+ mExcludedUids = new ArrayList<Integer>();
+ }
+ return this;
+ }
+
+ /**
+ * Check if given preference enterprise identifier is valid
+ *
+ * Valid values starts from PROFILE_NETWORK_PREFERENCE_ENTERPRISE_ID_1 to
+ * NetworkCapabilities.NET_ENTERPRISE_ID_5.
+ * @return True if valid else false
+ * @hide
+ */
+ private boolean isEnterpriseIdentifierValid(
+ @NetworkCapabilities.EnterpriseId int identifier) {
+ if ((identifier >= NET_ENTERPRISE_ID_1)
+ && (identifier <= NET_ENTERPRISE_ID_5)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
* Returns an instance of {@link ProfileNetworkPreference} created from the
* fields set on this builder.
*/
@NonNull
public ProfileNetworkPreference build() {
- return new ProfileNetworkPreference(mPreference);
+ if (mIncludedUids.size() > 0 && mExcludedUids.size() > 0) {
+ throw new IllegalArgumentException("Both includedUids and excludedUids "
+ + "cannot be nonempty");
+ }
+
+ if (((mPreference != PROFILE_NETWORK_PREFERENCE_DEFAULT)
+ && (!isEnterpriseIdentifierValid(mPreferenceEnterpriseId)))
+ || ((mPreference == PROFILE_NETWORK_PREFERENCE_DEFAULT)
+ && (mPreferenceEnterpriseId != 0))) {
+ throw new IllegalStateException("Invalid preference enterprise identifier");
+ }
+ return new ProfileNetworkPreference(mPreference, mIncludedUids,
+ mExcludedUids, mPreferenceEnterpriseId);
+ }
+
+ /**
+ * Set the preference enterprise identifier.
+ *
+ * Preference enterprise identifier will be used to create different network preferences
+ * within enterprise preference category.
+ * Valid values starts from NetworkCapabilities.NET_ENTERPRISE_ID_1 to
+ * NetworkCapabilities.NET_ENTERPRISE_ID_5.
+ * Preference identifier is not applicable if preference is set as
+ * PROFILE_NETWORK_PREFERENCE_DEFAULT. Default value is
+ * NetworkCapabilities.NET_ENTERPRISE_ID_1.
+ * @param preferenceId preference sub level
+ * @return The builder to facilitate chaining.
+ */
+ @NonNull
+ public Builder setPreferenceEnterpriseId(
+ @NetworkCapabilities.EnterpriseId int preferenceId) {
+ mPreferenceEnterpriseId = preferenceId;
+ return this;
}
}
@Override
public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
dest.writeInt(mPreference);
+ dest.writeList(mIncludedUids);
+ dest.writeList(mExcludedUids);
+ dest.writeInt(mPreferenceEnterpriseId);
}
@Override
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
new file mode 100644
index 0000000..bceeefa
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -0,0 +1,71 @@
+//
+// Copyright (C) 2017 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library {
+ name: "libnetworkstats",
+ vendor_available: false,
+ host_supported: false,
+ header_libs: ["bpf_connectivity_headers"],
+ srcs: [
+ "BpfNetworkStats.cpp"
+ ],
+ shared_libs: [
+ "libbase",
+ "liblog",
+ ],
+ export_include_dirs: ["include"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ "-Wthread-safety",
+ ],
+ sanitize: {
+ cfi: true,
+ },
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.tethering",
+ ],
+ min_sdk_version: "30",
+}
+
+cc_test {
+ name: "libnetworkstats_test",
+ test_suites: ["general-tests"],
+ require_root: true, // required by setrlimitForTest()
+ header_libs: ["bpf_connectivity_headers"],
+ srcs: [
+ "BpfNetworkStatsTest.cpp",
+ ],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ "-Wthread-safety",
+ ],
+ static_libs: ["libgmock"],
+ shared_libs: [
+ "libbase",
+ "liblog",
+ "libnetworkstats",
+ "libutils",
+ ],
+}
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
new file mode 100644
index 0000000..4d605ce
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#include <inttypes.h>
+#include <net/if.h>
+#include <string.h>
+#include <unordered_set>
+
+#include <utils/Log.h>
+#include <utils/misc.h>
+
+#include "android-base/file.h"
+#include "android-base/strings.h"
+#include "android-base/unique_fd.h"
+#include "bpf/BpfMap.h"
+#include "bpf_shared.h"
+#include "netdbpf/BpfNetworkStats.h"
+
+#ifdef LOG_TAG
+#undef LOG_TAG
+#endif
+
+#define LOG_TAG "BpfNetworkStats"
+
+namespace android {
+namespace bpf {
+
+using base::Result;
+
+// The target map for stats reading should be the inactive map, which is opposite
+// from the config value.
+static constexpr char const* STATS_MAP_PATH[] = {STATS_MAP_B_PATH, STATS_MAP_A_PATH};
+
+int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
+ const BpfMap<uint32_t, StatsValue>& appUidStatsMap) {
+ auto statsEntry = appUidStatsMap.readValue(uid);
+ if (statsEntry.ok()) {
+ stats->rxPackets = statsEntry.value().rxPackets;
+ stats->txPackets = statsEntry.value().txPackets;
+ stats->rxBytes = statsEntry.value().rxBytes;
+ stats->txBytes = statsEntry.value().txBytes;
+ }
+ return (statsEntry.ok() || statsEntry.error().code() == ENOENT) ? 0
+ : -statsEntry.error().code();
+}
+
+int bpfGetUidStats(uid_t uid, Stats* stats) {
+ BpfMapRO<uint32_t, StatsValue> appUidStatsMap(APP_UID_STATS_MAP_PATH);
+
+ if (!appUidStatsMap.isValid()) {
+ int ret = -errno;
+ ALOGE("Opening appUidStatsMap(%s) failed: %s", APP_UID_STATS_MAP_PATH, strerror(errno));
+ return ret;
+ }
+ return bpfGetUidStatsInternal(uid, stats, appUidStatsMap);
+}
+
+int bpfGetIfaceStatsInternal(const char* iface, Stats* stats,
+ const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
+ const BpfMap<uint32_t, IfaceValue>& ifaceNameMap) {
+ int64_t unknownIfaceBytesTotal = 0;
+ stats->tcpRxPackets = -1;
+ stats->tcpTxPackets = -1;
+ const auto processIfaceStats =
+ [iface, stats, &ifaceNameMap, &unknownIfaceBytesTotal](
+ const uint32_t& key,
+ const BpfMap<uint32_t, StatsValue>& ifaceStatsMap) -> Result<void> {
+ char ifname[IFNAMSIZ];
+ if (getIfaceNameFromMap(ifaceNameMap, ifaceStatsMap, key, ifname, key,
+ &unknownIfaceBytesTotal)) {
+ return Result<void>();
+ }
+ if (!iface || !strcmp(iface, ifname)) {
+ Result<StatsValue> statsEntry = ifaceStatsMap.readValue(key);
+ if (!statsEntry.ok()) {
+ return statsEntry.error();
+ }
+ stats->rxPackets += statsEntry.value().rxPackets;
+ stats->txPackets += statsEntry.value().txPackets;
+ stats->rxBytes += statsEntry.value().rxBytes;
+ stats->txBytes += statsEntry.value().txBytes;
+ }
+ return Result<void>();
+ };
+ auto res = ifaceStatsMap.iterate(processIfaceStats);
+ return res.ok() ? 0 : -res.error().code();
+}
+
+int bpfGetIfaceStats(const char* iface, Stats* stats) {
+ BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+ int ret;
+ if (!ifaceStatsMap.isValid()) {
+ ret = -errno;
+ ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
+ return ret;
+ }
+ BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+ if (!ifaceIndexNameMap.isValid()) {
+ ret = -errno;
+ ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
+ return ret;
+ }
+ return bpfGetIfaceStatsInternal(iface, stats, ifaceStatsMap, ifaceIndexNameMap);
+}
+
+stats_line populateStatsEntry(const StatsKey& statsKey, const StatsValue& statsEntry,
+ const char* ifname) {
+ stats_line newLine;
+ strlcpy(newLine.iface, ifname, sizeof(newLine.iface));
+ newLine.uid = (int32_t)statsKey.uid;
+ newLine.set = (int32_t)statsKey.counterSet;
+ newLine.tag = (int32_t)statsKey.tag;
+ newLine.rxPackets = statsEntry.rxPackets;
+ newLine.txPackets = statsEntry.txPackets;
+ newLine.rxBytes = statsEntry.rxBytes;
+ newLine.txBytes = statsEntry.txBytes;
+ return newLine;
+}
+
+int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>* lines,
+ const std::vector<std::string>& limitIfaces, int limitTag,
+ int limitUid, const BpfMap<StatsKey, StatsValue>& statsMap,
+ const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
+ int64_t unknownIfaceBytesTotal = 0;
+ const auto processDetailUidStats =
+ [lines, &limitIfaces, &limitTag, &limitUid, &unknownIfaceBytesTotal, &ifaceMap](
+ const StatsKey& key,
+ const BpfMap<StatsKey, StatsValue>& statsMap) -> Result<void> {
+ char ifname[IFNAMSIZ];
+ if (getIfaceNameFromMap(ifaceMap, statsMap, key.ifaceIndex, ifname, key,
+ &unknownIfaceBytesTotal)) {
+ return Result<void>();
+ }
+ std::string ifnameStr(ifname);
+ if (limitIfaces.size() > 0 &&
+ std::find(limitIfaces.begin(), limitIfaces.end(), ifnameStr) == limitIfaces.end()) {
+ // Nothing matched; skip this line.
+ return Result<void>();
+ }
+ if (limitTag != TAG_ALL && uint32_t(limitTag) != key.tag) {
+ return Result<void>();
+ }
+ if (limitUid != UID_ALL && uint32_t(limitUid) != key.uid) {
+ return Result<void>();
+ }
+ Result<StatsValue> statsEntry = statsMap.readValue(key);
+ if (!statsEntry.ok()) {
+ return base::ResultError(statsEntry.error().message(), statsEntry.error().code());
+ }
+ lines->push_back(populateStatsEntry(key, statsEntry.value(), ifname));
+ return Result<void>();
+ };
+ Result<void> res = statsMap.iterate(processDetailUidStats);
+ if (!res.ok()) {
+ ALOGE("failed to iterate per uid Stats map for detail traffic stats: %s",
+ strerror(res.error().code()));
+ return -res.error().code();
+ }
+
+ // Since eBPF use hash map to record stats, network stats collected from
+ // eBPF will be out of order. And the performance of findIndexHinted in
+ // NetworkStats will also be impacted.
+ //
+ // Furthermore, since the StatsKey contains iface index, the network stats
+ // reported to framework would create items with the same iface, uid, tag
+ // and set, which causes NetworkStats maps wrong item to subtract.
+ //
+ // Thus, the stats needs to be properly sorted and grouped before reported.
+ groupNetworkStats(lines);
+ return 0;
+}
+
+int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines,
+ const std::vector<std::string>& limitIfaces, int limitTag,
+ int limitUid) {
+ BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+ if (!ifaceIndexNameMap.isValid()) {
+ int ret = -errno;
+ ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
+ return ret;
+ }
+
+ BpfMapRO<uint32_t, uint8_t> configurationMap(CONFIGURATION_MAP_PATH);
+ if (!configurationMap.isValid()) {
+ int ret = -errno;
+ ALOGE("get configuration map fd failed: %s", strerror(errno));
+ return ret;
+ }
+ auto configuration = configurationMap.readValue(CURRENT_STATS_MAP_CONFIGURATION_KEY);
+ if (!configuration.ok()) {
+ ALOGE("Cannot read the old configuration from map: %s",
+ configuration.error().message().c_str());
+ return -configuration.error().code();
+ }
+ const char* statsMapPath = STATS_MAP_PATH[configuration.value()];
+ BpfMap<StatsKey, StatsValue> statsMap(statsMapPath);
+ if (!statsMap.isValid()) {
+ int ret = -errno;
+ ALOGE("get stats map fd failed: %s, path: %s", strerror(errno), statsMapPath);
+ return ret;
+ }
+
+ // It is safe to read and clear the old map now since the
+ // networkStatsFactory should call netd to swap the map in advance already.
+ int ret = parseBpfNetworkStatsDetailInternal(lines, limitIfaces, limitTag, limitUid, statsMap,
+ ifaceIndexNameMap);
+ if (ret) {
+ ALOGE("parse detail network stats failed: %s", strerror(errno));
+ return ret;
+ }
+
+ Result<void> res = statsMap.clear();
+ if (!res.ok()) {
+ ALOGE("Clean up current stats map failed: %s", strerror(res.error().code()));
+ return -res.error().code();
+ }
+
+ return 0;
+}
+
+int parseBpfNetworkStatsDevInternal(std::vector<stats_line>* lines,
+ const BpfMap<uint32_t, StatsValue>& statsMap,
+ const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
+ int64_t unknownIfaceBytesTotal = 0;
+ const auto processDetailIfaceStats = [lines, &unknownIfaceBytesTotal, &ifaceMap, &statsMap](
+ const uint32_t& key, const StatsValue& value,
+ const BpfMap<uint32_t, StatsValue>&) {
+ char ifname[IFNAMSIZ];
+ if (getIfaceNameFromMap(ifaceMap, statsMap, key, ifname, key, &unknownIfaceBytesTotal)) {
+ return Result<void>();
+ }
+ StatsKey fakeKey = {
+ .uid = (uint32_t)UID_ALL,
+ .tag = (uint32_t)TAG_NONE,
+ .counterSet = (uint32_t)SET_ALL,
+ };
+ lines->push_back(populateStatsEntry(fakeKey, value, ifname));
+ return Result<void>();
+ };
+ Result<void> res = statsMap.iterateWithValue(processDetailIfaceStats);
+ if (!res.ok()) {
+ ALOGE("failed to iterate per uid Stats map for detail traffic stats: %s",
+ strerror(res.error().code()));
+ return -res.error().code();
+ }
+
+ groupNetworkStats(lines);
+ return 0;
+}
+
+int parseBpfNetworkStatsDev(std::vector<stats_line>* lines) {
+ int ret = 0;
+ BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
+ if (!ifaceIndexNameMap.isValid()) {
+ ret = -errno;
+ ALOGE("get ifaceIndexName map fd failed: %s", strerror(errno));
+ return ret;
+ }
+
+ BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
+ if (!ifaceStatsMap.isValid()) {
+ ret = -errno;
+ ALOGE("get ifaceStats map fd failed: %s", strerror(errno));
+ return ret;
+ }
+ return parseBpfNetworkStatsDevInternal(lines, ifaceStatsMap, ifaceIndexNameMap);
+}
+
+uint64_t combineUidTag(const uid_t uid, const uint32_t tag) {
+ return (uint64_t)uid << 32 | tag;
+}
+
+void groupNetworkStats(std::vector<stats_line>* lines) {
+ if (lines->size() <= 1) return;
+ std::sort(lines->begin(), lines->end());
+
+ // Similar to std::unique(), but aggregates the duplicates rather than discarding them.
+ size_t nextOutput = 0;
+ for (size_t i = 1; i < lines->size(); i++) {
+ if (lines->at(nextOutput) == lines->at(i)) {
+ lines->at(nextOutput) += lines->at(i);
+ } else {
+ nextOutput++;
+ if (nextOutput != i) {
+ lines->at(nextOutput) = lines->at(i);
+ }
+ }
+ }
+
+ if (lines->size() != nextOutput + 1) {
+ lines->resize(nextOutput + 1);
+ }
+}
+
+// True if lhs equals to rhs, only compare iface, uid, tag and set.
+bool operator==(const stats_line& lhs, const stats_line& rhs) {
+ return ((lhs.uid == rhs.uid) && (lhs.tag == rhs.tag) && (lhs.set == rhs.set) &&
+ !strncmp(lhs.iface, rhs.iface, sizeof(lhs.iface)));
+}
+
+// True if lhs is smaller than rhs, only compare iface, uid, tag and set.
+bool operator<(const stats_line& lhs, const stats_line& rhs) {
+ int ret = strncmp(lhs.iface, rhs.iface, sizeof(lhs.iface));
+ if (ret != 0) return ret < 0;
+ if (lhs.uid < rhs.uid) return true;
+ if (lhs.uid > rhs.uid) return false;
+ if (lhs.tag < rhs.tag) return true;
+ if (lhs.tag > rhs.tag) return false;
+ if (lhs.set < rhs.set) return true;
+ if (lhs.set > rhs.set) return false;
+ return false;
+}
+
+stats_line& stats_line::operator=(const stats_line& rhs) {
+ if (this == &rhs) return *this;
+
+ strlcpy(iface, rhs.iface, sizeof(iface));
+ uid = rhs.uid;
+ set = rhs.set;
+ tag = rhs.tag;
+ rxPackets = rhs.rxPackets;
+ txPackets = rhs.txPackets;
+ rxBytes = rhs.rxBytes;
+ txBytes = rhs.txBytes;
+ return *this;
+}
+
+stats_line& stats_line::operator+=(const stats_line& rhs) {
+ rxPackets += rhs.rxPackets;
+ txPackets += rhs.txPackets;
+ rxBytes += rhs.rxBytes;
+ txBytes += rhs.txBytes;
+ return *this;
+}
+
+} // namespace bpf
+} // namespace android
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
new file mode 100644
index 0000000..4974b96
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -0,0 +1,569 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <vector>
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/inet_diag.h>
+#include <linux/sock_diag.h>
+#include <net/if.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <gtest/gtest.h>
+
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+
+#include "bpf/BpfMap.h"
+#include "bpf/BpfUtils.h"
+#include "netdbpf/BpfNetworkStats.h"
+
+using ::testing::Test;
+
+namespace android {
+namespace bpf {
+
+using base::Result;
+using base::unique_fd;
+
+constexpr int TEST_MAP_SIZE = 10;
+constexpr uid_t TEST_UID1 = 10086;
+constexpr uid_t TEST_UID2 = 12345;
+constexpr uint32_t TEST_TAG = 42;
+constexpr int TEST_COUNTERSET0 = 0;
+constexpr int TEST_COUNTERSET1 = 1;
+constexpr uint64_t TEST_BYTES0 = 1000;
+constexpr uint64_t TEST_BYTES1 = 2000;
+constexpr uint64_t TEST_PACKET0 = 100;
+constexpr uint64_t TEST_PACKET1 = 200;
+constexpr const char IFACE_NAME1[] = "lo";
+constexpr const char IFACE_NAME2[] = "wlan0";
+constexpr const char IFACE_NAME3[] = "rmnet_data0";
+// A iface name that the size is bigger than IFNAMSIZ
+constexpr const char LONG_IFACE_NAME[] = "wlanWithALongName";
+constexpr const char TRUNCATED_IFACE_NAME[] = "wlanWithALongNa";
+constexpr uint32_t IFACE_INDEX1 = 1;
+constexpr uint32_t IFACE_INDEX2 = 2;
+constexpr uint32_t IFACE_INDEX3 = 3;
+constexpr uint32_t IFACE_INDEX4 = 4;
+constexpr uint32_t UNKNOWN_IFACE = 0;
+
+class BpfNetworkStatsHelperTest : public testing::Test {
+ protected:
+ BpfNetworkStatsHelperTest() {}
+ BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
+ BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
+ BpfMap<StatsKey, StatsValue> mFakeStatsMap;
+ BpfMap<uint32_t, IfaceValue> mFakeIfaceIndexNameMap;
+ BpfMap<uint32_t, StatsValue> mFakeIfaceStatsMap;
+
+ void SetUp() {
+ ASSERT_EQ(0, setrlimitForTest());
+
+ mFakeCookieTagMap = BpfMap<uint64_t, UidTagValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+ ASSERT_LE(0, mFakeCookieTagMap.getMap());
+
+ mFakeAppUidStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+ ASSERT_LE(0, mFakeAppUidStatsMap.getMap());
+
+ mFakeStatsMap = BpfMap<StatsKey, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+ ASSERT_LE(0, mFakeStatsMap.getMap());
+
+ mFakeIfaceIndexNameMap = BpfMap<uint32_t, IfaceValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+ ASSERT_LE(0, mFakeIfaceIndexNameMap.getMap());
+
+ mFakeIfaceStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+ ASSERT_LE(0, mFakeIfaceStatsMap.getMap());
+ }
+
+ void expectUidTag(uint64_t cookie, uid_t uid, uint32_t tag) {
+ auto tagResult = mFakeCookieTagMap.readValue(cookie);
+ EXPECT_RESULT_OK(tagResult);
+ EXPECT_EQ(uid, tagResult.value().uid);
+ EXPECT_EQ(tag, tagResult.value().tag);
+ }
+
+ void populateFakeStats(uid_t uid, uint32_t tag, uint32_t ifaceIndex, uint32_t counterSet,
+ StatsValue value, BpfMap<StatsKey, StatsValue>& map) {
+ StatsKey key = {
+ .uid = (uint32_t)uid, .tag = tag, .counterSet = counterSet, .ifaceIndex = ifaceIndex};
+ EXPECT_RESULT_OK(map.writeValue(key, value, BPF_ANY));
+ }
+
+ void updateIfaceMap(const char* ifaceName, uint32_t ifaceIndex) {
+ IfaceValue iface;
+ strlcpy(iface.name, ifaceName, IFNAMSIZ);
+ EXPECT_RESULT_OK(mFakeIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY));
+ }
+
+ void expectStatsEqual(const StatsValue& target, const Stats& result) {
+ EXPECT_EQ(target.rxPackets, result.rxPackets);
+ EXPECT_EQ(target.rxBytes, result.rxBytes);
+ EXPECT_EQ(target.txPackets, result.txPackets);
+ EXPECT_EQ(target.txBytes, result.txBytes);
+ }
+
+ void expectStatsLineEqual(const StatsValue target, const char* iface, uint32_t uid,
+ int counterSet, uint32_t tag, const stats_line& result) {
+ EXPECT_EQ(0, strcmp(iface, result.iface));
+ EXPECT_EQ(uid, (uint32_t)result.uid);
+ EXPECT_EQ((uint32_t) counterSet, result.set);
+ EXPECT_EQ(tag, (uint32_t)result.tag);
+ EXPECT_EQ(target.rxPackets, (uint64_t)result.rxPackets);
+ EXPECT_EQ(target.rxBytes, (uint64_t)result.rxBytes);
+ EXPECT_EQ(target.txPackets, (uint64_t)result.txPackets);
+ EXPECT_EQ(target.txBytes, (uint64_t)result.txBytes);
+ }
+};
+
+// TEST to verify the behavior of bpf map when cocurrent deletion happens when
+// iterating the same map.
+TEST_F(BpfNetworkStatsHelperTest, TestIterateMapWithDeletion) {
+ for (int i = 0; i < 5; i++) {
+ uint64_t cookie = i + 1;
+ UidTagValue tag = {.uid = TEST_UID1, .tag = TEST_TAG};
+ EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, tag, BPF_ANY));
+ }
+ uint64_t curCookie = 0;
+ auto nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+ EXPECT_RESULT_OK(nextCookie);
+ uint64_t headOfMap = nextCookie.value();
+ curCookie = nextCookie.value();
+ // Find the second entry in the map, then immediately delete it.
+ nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+ EXPECT_RESULT_OK(nextCookie);
+ EXPECT_RESULT_OK(mFakeCookieTagMap.deleteValue((nextCookie.value())));
+ // Find the entry that is now immediately after headOfMap, then delete that.
+ nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+ EXPECT_RESULT_OK(nextCookie);
+ EXPECT_RESULT_OK(mFakeCookieTagMap.deleteValue((nextCookie.value())));
+ // Attempting to read an entry that has been deleted fails with ENOENT.
+ curCookie = nextCookie.value();
+ auto tagResult = mFakeCookieTagMap.readValue(curCookie);
+ EXPECT_EQ(ENOENT, tagResult.error().code());
+ // Finding the entry after our deleted entry restarts iteration from the beginning of the map.
+ nextCookie = mFakeCookieTagMap.getNextKey(curCookie);
+ EXPECT_RESULT_OK(nextCookie);
+ EXPECT_EQ(headOfMap, nextCookie.value());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestBpfIterateMap) {
+ for (int i = 0; i < 5; i++) {
+ uint64_t cookie = i + 1;
+ UidTagValue tag = {.uid = TEST_UID1, .tag = TEST_TAG};
+ EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, tag, BPF_ANY));
+ }
+ int totalCount = 0;
+ int totalSum = 0;
+ const auto iterateWithoutDeletion =
+ [&totalCount, &totalSum](const uint64_t& key, const BpfMap<uint64_t, UidTagValue>&) {
+ EXPECT_GE((uint64_t)5, key);
+ totalCount++;
+ totalSum += key;
+ return Result<void>();
+ };
+ EXPECT_RESULT_OK(mFakeCookieTagMap.iterate(iterateWithoutDeletion));
+ EXPECT_EQ(5, totalCount);
+ EXPECT_EQ(1 + 2 + 3 + 4 + 5, totalSum);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestUidStatsNoTraffic) {
+ StatsValue value1 = {
+ .rxPackets = 0,
+ .rxBytes = 0,
+ .txPackets = 0,
+ .txBytes = 0,
+ };
+ Stats result1 = {};
+ ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID1, &result1, mFakeAppUidStatsMap));
+ expectStatsEqual(value1, result1);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetUidStatsTotal) {
+ updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+ updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+ updateIfaceMap(IFACE_NAME3, IFACE_INDEX3);
+ StatsValue value1 = {
+ .rxPackets = TEST_PACKET0,
+ .rxBytes = TEST_BYTES0,
+ .txPackets = TEST_PACKET1,
+ .txBytes = TEST_BYTES1,
+ };
+ StatsValue value2 = {
+ .rxPackets = TEST_PACKET0 * 2,
+ .rxBytes = TEST_BYTES0 * 2,
+ .txPackets = TEST_PACKET1 * 2,
+ .txBytes = TEST_BYTES1 * 2,
+ };
+ ASSERT_RESULT_OK(mFakeAppUidStatsMap.writeValue(TEST_UID1, value1, BPF_ANY));
+ ASSERT_RESULT_OK(mFakeAppUidStatsMap.writeValue(TEST_UID2, value2, BPF_ANY));
+ Stats result1 = {};
+ ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID1, &result1, mFakeAppUidStatsMap));
+ expectStatsEqual(value1, result1);
+
+ Stats result2 = {};
+ ASSERT_EQ(0, bpfGetUidStatsInternal(TEST_UID2, &result2, mFakeAppUidStatsMap));
+ expectStatsEqual(value2, result2);
+ std::vector<stats_line> lines;
+ std::vector<std::string> ifaces;
+ populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET1, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID2, 0, IFACE_INDEX3, TEST_COUNTERSET1, value1, mFakeStatsMap);
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+ mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ((unsigned long)2, lines.size());
+ lines.clear();
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID2,
+ mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ((unsigned long)1, lines.size());
+ expectStatsLineEqual(value1, IFACE_NAME3, TEST_UID2, TEST_COUNTERSET1, 0, lines.front());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetIfaceStatsInternal) {
+ updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+ updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+ updateIfaceMap(IFACE_NAME3, IFACE_INDEX3);
+ StatsValue value1 = {
+ .rxPackets = TEST_PACKET0,
+ .rxBytes = TEST_BYTES0,
+ .txPackets = TEST_PACKET1,
+ .txBytes = TEST_BYTES1,
+ };
+ StatsValue value2 = {
+ .rxPackets = TEST_PACKET1,
+ .rxBytes = TEST_BYTES1,
+ .txPackets = TEST_PACKET0,
+ .txBytes = TEST_BYTES0,
+ };
+ uint32_t ifaceStatsKey = IFACE_INDEX1;
+ EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+ ifaceStatsKey = IFACE_INDEX2;
+ EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+ ifaceStatsKey = IFACE_INDEX3;
+ EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+
+ Stats result1 = {};
+ ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME1, &result1, mFakeIfaceStatsMap,
+ mFakeIfaceIndexNameMap));
+ expectStatsEqual(value1, result1);
+ Stats result2 = {};
+ ASSERT_EQ(0, bpfGetIfaceStatsInternal(IFACE_NAME2, &result2, mFakeIfaceStatsMap,
+ mFakeIfaceIndexNameMap));
+ expectStatsEqual(value2, result2);
+ Stats totalResult = {};
+ ASSERT_EQ(0, bpfGetIfaceStatsInternal(NULL, &totalResult, mFakeIfaceStatsMap,
+ mFakeIfaceIndexNameMap));
+ StatsValue totalValue = {
+ .rxPackets = TEST_PACKET0 * 2 + TEST_PACKET1,
+ .rxBytes = TEST_BYTES0 * 2 + TEST_BYTES1,
+ .txPackets = TEST_PACKET1 * 2 + TEST_PACKET0,
+ .txBytes = TEST_BYTES1 * 2 + TEST_BYTES0,
+ };
+ expectStatsEqual(totalValue, totalResult);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsDetail) {
+ updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+ updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+ StatsValue value1 = {
+ .rxPackets = TEST_PACKET0,
+ .rxBytes = TEST_BYTES0,
+ .txPackets = TEST_PACKET1,
+ .txBytes = TEST_BYTES1,
+ };
+ populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX2, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value1,
+ mFakeStatsMap);
+ populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ std::vector<stats_line> lines;
+ std::vector<std::string> ifaces;
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+ mFakeIfaceIndexNameMap));
+ ASSERT_EQ((unsigned long)4, lines.size());
+ lines.clear();
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+ mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ((unsigned long)3, lines.size());
+ lines.clear();
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TEST_TAG, TEST_UID1,
+ mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ((unsigned long)2, lines.size());
+ lines.clear();
+ ifaces.push_back(std::string(IFACE_NAME1));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TEST_TAG, TEST_UID1,
+ mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ((unsigned long)1, lines.size());
+ expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines.front());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsWithSkippedIface) {
+ updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+ updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+ StatsValue value1 = {
+ .rxPackets = TEST_PACKET0,
+ .rxBytes = TEST_BYTES0,
+ .txPackets = TEST_PACKET1,
+ .txBytes = TEST_BYTES1,
+ };
+ populateFakeStats(0, 0, 0, OVERFLOW_COUNTERSET, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET1, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID2, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ std::vector<stats_line> lines;
+ std::vector<std::string> ifaces;
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+ mFakeIfaceIndexNameMap));
+ ASSERT_EQ((unsigned long)4, lines.size());
+ lines.clear();
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+ mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ((unsigned long)3, lines.size());
+ lines.clear();
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID2,
+ mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ((unsigned long)1, lines.size());
+ expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID2, TEST_COUNTERSET0, 0, lines.front());
+ lines.clear();
+ ifaces.push_back(std::string(IFACE_NAME1));
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, TEST_UID1,
+ mFakeStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ((unsigned long)2, lines.size());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestUnknownIfaceError) {
+ updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+ StatsValue value1 = {
+ .rxPackets = TEST_PACKET0,
+ .rxBytes = TEST_BYTES0 * 20,
+ .txPackets = TEST_PACKET1,
+ .txBytes = TEST_BYTES1 * 20,
+ };
+ uint32_t ifaceIndex = UNKNOWN_IFACE;
+ populateFakeStats(TEST_UID1, 0, ifaceIndex, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ StatsValue value2 = {
+ .rxPackets = TEST_PACKET0,
+ .rxBytes = TEST_BYTES0 * 40,
+ .txPackets = TEST_PACKET1,
+ .txBytes = TEST_BYTES1 * 40,
+ };
+ populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET0, value2, mFakeStatsMap);
+ StatsKey curKey = {
+ .uid = TEST_UID1,
+ .tag = 0,
+ .counterSet = TEST_COUNTERSET0,
+ .ifaceIndex = ifaceIndex,
+ };
+ char ifname[IFNAMSIZ];
+ int64_t unknownIfaceBytesTotal = 0;
+ ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
+ ifname, curKey, &unknownIfaceBytesTotal));
+ ASSERT_EQ(((int64_t)(TEST_BYTES0 * 20 + TEST_BYTES1 * 20)), unknownIfaceBytesTotal);
+ curKey.ifaceIndex = IFACE_INDEX2;
+ ASSERT_EQ(-ENODEV, getIfaceNameFromMap(mFakeIfaceIndexNameMap, mFakeStatsMap, ifaceIndex,
+ ifname, curKey, &unknownIfaceBytesTotal));
+ ASSERT_EQ(-1, unknownIfaceBytesTotal);
+ std::vector<stats_line> lines;
+ std::vector<std::string> ifaces;
+ // TODO: find a way to test the total of unknown Iface Bytes go above limit.
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+ mFakeIfaceIndexNameMap));
+ ASSERT_EQ((unsigned long)1, lines.size());
+ expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines.front());
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetIfaceStatsDetail) {
+ updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+ updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+ updateIfaceMap(IFACE_NAME3, IFACE_INDEX3);
+ updateIfaceMap(LONG_IFACE_NAME, IFACE_INDEX4);
+ StatsValue value1 = {
+ .rxPackets = TEST_PACKET0,
+ .rxBytes = TEST_BYTES0,
+ .txPackets = TEST_PACKET1,
+ .txBytes = TEST_BYTES1,
+ };
+ StatsValue value2 = {
+ .rxPackets = TEST_PACKET1,
+ .rxBytes = TEST_BYTES1,
+ .txPackets = TEST_PACKET0,
+ .txBytes = TEST_BYTES0,
+ };
+ uint32_t ifaceStatsKey = IFACE_INDEX1;
+ EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+ ifaceStatsKey = IFACE_INDEX2;
+ EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+ ifaceStatsKey = IFACE_INDEX3;
+ EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+ ifaceStatsKey = IFACE_INDEX4;
+ EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+ std::vector<stats_line> lines;
+ ASSERT_EQ(0,
+ parseBpfNetworkStatsDevInternal(&lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ((unsigned long)4, lines.size());
+
+ expectStatsLineEqual(value1, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
+ expectStatsLineEqual(value1, IFACE_NAME3, UID_ALL, SET_ALL, TAG_NONE, lines[1]);
+ expectStatsLineEqual(value2, IFACE_NAME2, UID_ALL, SET_ALL, TAG_NONE, lines[2]);
+ ASSERT_EQ(0, strcmp(TRUNCATED_IFACE_NAME, lines[3].iface));
+ expectStatsLineEqual(value2, TRUNCATED_IFACE_NAME, UID_ALL, SET_ALL, TAG_NONE, lines[3]);
+}
+
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsSortedAndGrouped) {
+ // Create iface indexes with duplicate iface name.
+ updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+ updateIfaceMap(IFACE_NAME2, IFACE_INDEX2);
+ updateIfaceMap(IFACE_NAME1, IFACE_INDEX3); // Duplicate!
+
+ StatsValue value1 = {
+ .rxPackets = TEST_PACKET0,
+ .rxBytes = TEST_BYTES0,
+ .txPackets = TEST_PACKET1,
+ .txBytes = TEST_BYTES1,
+ };
+ StatsValue value2 = {
+ .rxPackets = TEST_PACKET1,
+ .rxBytes = TEST_BYTES1,
+ .txPackets = TEST_PACKET0,
+ .txBytes = TEST_BYTES0,
+ };
+ StatsValue value3 = {
+ .rxPackets = TEST_PACKET0 * 2,
+ .rxBytes = TEST_BYTES0 * 2,
+ .txPackets = TEST_PACKET1 * 2,
+ .txBytes = TEST_BYTES1 * 2,
+ };
+
+ std::vector<stats_line> lines;
+ std::vector<std::string> ifaces;
+
+ // Test empty stats.
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+ mFakeIfaceIndexNameMap));
+ ASSERT_EQ((size_t) 0, lines.size());
+ lines.clear();
+
+ // Test 1 line stats.
+ populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+ mFakeIfaceIndexNameMap));
+ ASSERT_EQ((size_t) 1, lines.size());
+ expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+ lines.clear();
+
+ // These items should not be grouped.
+ populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX2, TEST_COUNTERSET0, value2, mFakeStatsMap);
+ populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET1, value2, mFakeStatsMap);
+ populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value2,
+ mFakeStatsMap);
+ populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+ mFakeIfaceIndexNameMap));
+ ASSERT_EQ((size_t) 5, lines.size());
+ lines.clear();
+
+ // These items should be grouped.
+ populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
+
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+ mFakeIfaceIndexNameMap));
+ ASSERT_EQ((size_t) 5, lines.size());
+
+ // Verify Sorted & Grouped.
+ expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+ expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET1, TEST_TAG, lines[1]);
+ expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG + 1, lines[2]);
+ expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID2, TEST_COUNTERSET0, TEST_TAG, lines[3]);
+ expectStatsLineEqual(value2, IFACE_NAME2, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[4]);
+ lines.clear();
+
+ // Perform test on IfaceStats.
+ uint32_t ifaceStatsKey = IFACE_INDEX2;
+ EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
+ ifaceStatsKey = IFACE_INDEX1;
+ EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+
+ // This should be grouped.
+ ifaceStatsKey = IFACE_INDEX3;
+ EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
+
+ ASSERT_EQ(0,
+ parseBpfNetworkStatsDevInternal(&lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+ ASSERT_EQ((size_t) 2, lines.size());
+
+ expectStatsLineEqual(value3, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
+ expectStatsLineEqual(value2, IFACE_NAME2, UID_ALL, SET_ALL, TAG_NONE, lines[1]);
+ lines.clear();
+}
+
+// Test to verify that subtract overflow will not be triggered by the compare function invoked from
+// sorting. See http:/b/119193941.
+TEST_F(BpfNetworkStatsHelperTest, TestGetStatsSortAndOverflow) {
+ updateIfaceMap(IFACE_NAME1, IFACE_INDEX1);
+
+ StatsValue value1 = {
+ .rxPackets = TEST_PACKET0,
+ .rxBytes = TEST_BYTES0,
+ .txPackets = TEST_PACKET1,
+ .txBytes = TEST_BYTES1,
+ };
+
+ // Mutate uid, 0 < TEST_UID1 < INT_MAX < INT_MIN < UINT_MAX.
+ populateFakeStats(0, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(UINT_MAX, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(INT_MIN, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(INT_MAX, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+
+ // Mutate tag, 0 < TEST_TAG < INT_MAX < INT_MIN < UINT_MAX.
+ populateFakeStats(TEST_UID1, INT_MAX, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID1, INT_MIN, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+ populateFakeStats(TEST_UID1, UINT_MAX, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+
+ // TODO: Mutate counterSet and enlarge TEST_MAP_SIZE if overflow on counterSet is possible.
+
+ std::vector<stats_line> lines;
+ std::vector<std::string> ifaces;
+ ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, ifaces, TAG_ALL, UID_ALL, mFakeStatsMap,
+ mFakeIfaceIndexNameMap));
+ ASSERT_EQ((size_t) 8, lines.size());
+
+ // Uid 0 first
+ expectStatsLineEqual(value1, IFACE_NAME1, 0, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+
+ // Test uid, mutate tag.
+ expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines[1]);
+ expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, INT_MAX, lines[2]);
+ expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, INT_MIN, lines[3]);
+ expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, UINT_MAX, lines[4]);
+
+ // Mutate uid.
+ expectStatsLineEqual(value1, IFACE_NAME1, INT_MAX, TEST_COUNTERSET0, TEST_TAG, lines[5]);
+ expectStatsLineEqual(value1, IFACE_NAME1, INT_MIN, TEST_COUNTERSET0, TEST_TAG, lines[6]);
+ expectStatsLineEqual(value1, IFACE_NAME1, UINT_MAX, TEST_COUNTERSET0, TEST_TAG, lines[7]);
+ lines.clear();
+}
+} // namespace bpf
+} // namespace android
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
new file mode 100644
index 0000000..8ab7e25
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#ifndef _BPF_NETWORKSTATS_H
+#define _BPF_NETWORKSTATS_H
+
+#include <bpf/BpfMap.h>
+#include "bpf_shared.h"
+
+namespace android {
+namespace bpf {
+
+// TODO: set this to a proper value based on the map size;
+constexpr int TAG_STATS_MAP_SOFT_LIMIT = 3;
+constexpr int UID_ALL = -1;
+constexpr int TAG_ALL = -1;
+constexpr int TAG_NONE = 0;
+constexpr int SET_ALL = -1;
+constexpr int SET_DEFAULT = 0;
+constexpr int SET_FOREGROUND = 1;
+
+// The limit for stats received by a unknown interface;
+constexpr const int64_t MAX_UNKNOWN_IFACE_BYTES = 100 * 1000;
+
+// This is used by
+// frameworks/base/core/jni/com_android_internal_net_NetworkStatsFactory.cpp
+// make sure it is consistent with the JNI code before changing this.
+struct stats_line {
+ char iface[32];
+ uint32_t uid;
+ uint32_t set;
+ uint32_t tag;
+ int64_t rxBytes;
+ int64_t rxPackets;
+ int64_t txBytes;
+ int64_t txPackets;
+
+ stats_line& operator=(const stats_line& rhs);
+ stats_line& operator+=(const stats_line& rhs);
+};
+
+bool operator==(const stats_line& lhs, const stats_line& rhs);
+bool operator<(const stats_line& lhs, const stats_line& rhs);
+
+// For test only
+int bpfGetUidStatsInternal(uid_t uid, Stats* stats,
+ const BpfMap<uint32_t, StatsValue>& appUidStatsMap);
+// For test only
+int bpfGetIfaceStatsInternal(const char* iface, Stats* stats,
+ const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
+ const BpfMap<uint32_t, IfaceValue>& ifaceNameMap);
+// For test only
+int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>* lines,
+ const std::vector<std::string>& limitIfaces, int limitTag,
+ int limitUid, const BpfMap<StatsKey, StatsValue>& statsMap,
+ const BpfMap<uint32_t, IfaceValue>& ifaceMap);
+// For test only
+int cleanStatsMapInternal(const base::unique_fd& cookieTagMap, const base::unique_fd& tagStatsMap);
+// For test only
+template <class Key>
+int getIfaceNameFromMap(const BpfMap<uint32_t, IfaceValue>& ifaceMap,
+ const BpfMap<Key, StatsValue>& statsMap, uint32_t ifaceIndex, char* ifname,
+ const Key& curKey, int64_t* unknownIfaceBytesTotal) {
+ auto iface = ifaceMap.readValue(ifaceIndex);
+ if (!iface.ok()) {
+ maybeLogUnknownIface(ifaceIndex, statsMap, curKey, unknownIfaceBytesTotal);
+ return -ENODEV;
+ }
+ strlcpy(ifname, iface.value().name, sizeof(IfaceValue));
+ return 0;
+}
+
+template <class Key>
+void maybeLogUnknownIface(int ifaceIndex, const BpfMap<Key, StatsValue>& statsMap,
+ const Key& curKey, int64_t* unknownIfaceBytesTotal) {
+ // Have we already logged an error?
+ if (*unknownIfaceBytesTotal == -1) {
+ return;
+ }
+
+ // Are we undercounting enough data to be worth logging?
+ auto statsEntry = statsMap.readValue(curKey);
+ if (!statsEntry.ok()) {
+ // No data is being undercounted.
+ return;
+ }
+
+ *unknownIfaceBytesTotal += (statsEntry.value().rxBytes + statsEntry.value().txBytes);
+ if (*unknownIfaceBytesTotal >= MAX_UNKNOWN_IFACE_BYTES) {
+ ALOGE("Unknown name for ifindex %d with more than %" PRId64 " bytes of traffic", ifaceIndex,
+ *unknownIfaceBytesTotal);
+ *unknownIfaceBytesTotal = -1;
+ }
+}
+
+// For test only
+int parseBpfNetworkStatsDevInternal(std::vector<stats_line>* lines,
+ const BpfMap<uint32_t, StatsValue>& statsMap,
+ const BpfMap<uint32_t, IfaceValue>& ifaceMap);
+
+int bpfGetUidStats(uid_t uid, Stats* stats);
+int bpfGetIfaceStats(const char* iface, Stats* stats);
+int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines,
+ const std::vector<std::string>& limitIfaces, int limitTag,
+ int limitUid);
+
+int parseBpfNetworkStatsDev(std::vector<stats_line>* lines);
+void groupNetworkStats(std::vector<stats_line>* lines);
+int cleanStatsMap();
+} // namespace bpf
+} // namespace android
+
+#endif // _BPF_NETWORKSTATS_H
diff --git a/service/Android.bp b/service/Android.bp
index 76f9153..33d4b66 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -36,6 +36,7 @@
stl: "libc++_static",
static_libs: [
"libnet_utils_device_common_bpfjni",
+ "libtcutils",
],
shared_libs: [
"liblog",
@@ -57,12 +58,19 @@
],
srcs: [
"jni/com_android_server_TestNetworkService.cpp",
+ "jni/com_android_server_connectivity_ClatCoordinator.cpp",
"jni/onload.cpp",
],
stl: "libc++_static",
header_libs: [
"libbase_headers",
],
+ static_libs: [
+ "libbase",
+ "libclat",
+ "libip_checksum",
+ "libnetjniutils",
+ ],
shared_libs: [
"liblog",
"libnativehelper",
@@ -103,6 +111,7 @@
"networkstack-client",
"PlatformProperties",
"service-connectivity-protos",
+ "NetworkStackApiCurrentShims",
],
apex_available: [
"com.android.tethering",
@@ -156,3 +165,11 @@
srcs: ["jarjar-rules.txt"],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
+
+// TODO: This filegroup temporary exposes for NetworkStats. It should be
+// removed right after NetworkStats moves into mainline module.
+filegroup {
+ name: "traffic-controller-utils",
+ srcs: ["src/com/android/server/BpfNetMaps.java"],
+ visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
index e5d1a88..f658a5e 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -90,5 +90,12 @@
# 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
+
# 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_net_module_util/onload.cpp b/service/jni/com_android_net_module_util/onload.cpp
index 1d17622..07ae31c 100644
--- a/service/jni/com_android_net_module_util/onload.cpp
+++ b/service/jni/com_android_net_module_util/onload.cpp
@@ -20,6 +20,7 @@
namespace android {
int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
+int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv *env;
@@ -31,6 +32,9 @@
if (register_com_android_net_module_util_BpfMap(env,
"com/android/connectivity/com/android/net/module/util/BpfMap") < 0) return JNI_ERR;
+ if (register_com_android_net_module_util_TcUtils(env,
+ "com/android/connectivity/com/android/net/module/util/TcUtils") < 0) return JNI_ERR;
+
return JNI_VERSION_1_6;
}
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
new file mode 100644
index 0000000..b445462
--- /dev/null
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -0,0 +1,527 @@
+/*
+ * 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.
+ */
+#define LOG_TAG "jniClatCoordinator"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/if_tun.h>
+#include <linux/ioctl.h>
+#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <net/if.h>
+#include <spawn.h>
+#include <sys/wait.h>
+#include <string>
+
+#include <netjniutils/netjniutils.h>
+
+#include "libclat/bpfhelper.h"
+#include "libclat/clatutils.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+// Sync from system/netd/include/netid_client.h
+#define MARK_UNSET 0u
+
+// Sync from system/netd/server/NetdConstants.h
+#define __INT_STRLEN(i) sizeof(#i)
+#define _INT_STRLEN(i) __INT_STRLEN(i)
+#define INT32_STRLEN _INT_STRLEN(INT32_MIN)
+
+#define DEVICEPREFIX "v4-"
+
+namespace android {
+static const char* kClatdPath = "/apex/com.android.tethering/bin/for-system/clatd";
+
+static void throwIOException(JNIEnv* env, const char* msg, int error) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "%s: %s", msg, strerror(error));
+}
+
+jstring com_android_server_connectivity_ClatCoordinator_selectIpv4Address(JNIEnv* env,
+ jobject clazz,
+ jstring v4addr,
+ jint prefixlen) {
+ ScopedUtfChars address(env, v4addr);
+ in_addr ip;
+ if (inet_pton(AF_INET, address.c_str(), &ip) != 1) {
+ throwIOException(env, "invalid address", EINVAL);
+ return nullptr;
+ }
+
+ // Pick an IPv4 address.
+ // TODO: this picks the address based on other addresses that are assigned to interfaces, but
+ // the address is only actually assigned to an interface once clatd starts up. So we could end
+ // up with two clatd instances with the same IPv4 address.
+ // Stop doing this and instead pick a free one from the kV4Addr pool.
+ in_addr v4 = {net::clat::selectIpv4Address(ip, prefixlen)};
+ if (v4.s_addr == INADDR_NONE) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "No free IPv4 address in %s/%d",
+ address.c_str(), prefixlen);
+ return nullptr;
+ }
+
+ char addrstr[INET_ADDRSTRLEN];
+ if (!inet_ntop(AF_INET, (void*)&v4, addrstr, sizeof(addrstr))) {
+ throwIOException(env, "invalid address", EADDRNOTAVAIL);
+ return nullptr;
+ }
+ return env->NewStringUTF(addrstr);
+}
+
+// Picks a random interface ID that is checksum neutral with the IPv4 address and the NAT64 prefix.
+jstring com_android_server_connectivity_ClatCoordinator_generateIpv6Address(
+ JNIEnv* env, jobject clazz, jstring ifaceStr, jstring v4Str, jstring prefix64Str) {
+ ScopedUtfChars iface(env, ifaceStr);
+ ScopedUtfChars addr4(env, v4Str);
+ ScopedUtfChars prefix64(env, prefix64Str);
+
+ if (iface.c_str() == nullptr) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid null interface name");
+ return nullptr;
+ }
+
+ in_addr v4;
+ if (inet_pton(AF_INET, addr4.c_str(), &v4) != 1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid clat v4 address %s",
+ addr4.c_str());
+ return nullptr;
+ }
+
+ in6_addr nat64Prefix;
+ if (inet_pton(AF_INET6, prefix64.c_str(), &nat64Prefix) != 1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid prefix %s", prefix64.c_str());
+ return nullptr;
+ }
+
+ in6_addr v6;
+ if (net::clat::generateIpv6Address(iface.c_str(), v4, nat64Prefix, &v6)) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "Unable to find global source address on %s for %s", iface.c_str(),
+ prefix64.c_str());
+ return nullptr;
+ }
+
+ char addrstr[INET6_ADDRSTRLEN];
+ if (!inet_ntop(AF_INET6, (void*)&v6, addrstr, sizeof(addrstr))) {
+ throwIOException(env, "invalid address", EADDRNOTAVAIL);
+ return nullptr;
+ }
+ return env->NewStringUTF(addrstr);
+}
+
+static jint com_android_server_connectivity_ClatCoordinator_createTunInterface(JNIEnv* env,
+ jobject clazz,
+ jstring tuniface) {
+ ScopedUtfChars v4interface(env, tuniface);
+
+ // open the tun device in non blocking mode as required by clatd
+ jint fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (fd == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "open tun device failed (%s)",
+ strerror(errno));
+ return -1;
+ }
+
+ struct ifreq ifr = {
+ .ifr_flags = IFF_TUN,
+ };
+ strlcpy(ifr.ifr_name, v4interface.c_str(), sizeof(ifr.ifr_name));
+
+ if (ioctl(fd, TUNSETIFF, &ifr, sizeof(ifr))) {
+ close(fd);
+ jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(TUNSETIFF) failed (%s)",
+ strerror(errno));
+ return -1;
+ }
+
+ return fd;
+}
+
+static jint com_android_server_connectivity_ClatCoordinator_detectMtu(JNIEnv* env, jobject clazz,
+ jstring platSubnet,
+ jint plat_suffix, jint mark) {
+ ScopedUtfChars platSubnetStr(env, platSubnet);
+
+ in6_addr plat_subnet;
+ if (inet_pton(AF_INET6, platSubnetStr.c_str(), &plat_subnet) != 1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid plat prefix address %s",
+ platSubnetStr.c_str());
+ return -1;
+ }
+
+ int ret = net::clat::detect_mtu(&plat_subnet, plat_suffix, mark);
+ if (ret < 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "detect mtu failed: %s", strerror(-ret));
+ return -1;
+ }
+
+ return ret;
+}
+
+static jint com_android_server_connectivity_ClatCoordinator_openPacketSocket(JNIEnv* env,
+ jobject clazz) {
+ // Will eventually be bound to htons(ETH_P_IPV6) protocol,
+ // but only after appropriate bpf filter is attached.
+ int sock = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (sock < 0) {
+ throwIOException(env, "packet socket failed", errno);
+ return -1;
+ }
+ return sock;
+}
+
+static jint com_android_server_connectivity_ClatCoordinator_openRawSocket6(JNIEnv* env,
+ jobject clazz,
+ jint mark) {
+ int sock = socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_RAW);
+ if (sock < 0) {
+ throwIOException(env, "raw socket failed", errno);
+ return -1;
+ }
+
+ // TODO: check the mark validation
+ if (mark != MARK_UNSET && setsockopt(sock, SOL_SOCKET, SO_MARK, &mark, sizeof(mark)) < 0) {
+ throwIOException(env, "could not set mark on raw socket", errno);
+ close(sock);
+ return -1;
+ }
+
+ return sock;
+}
+
+static void com_android_server_connectivity_ClatCoordinator_addAnycastSetsockopt(
+ JNIEnv* env, jobject clazz, jobject javaFd, jstring addr6, jint ifindex) {
+ int sock = netjniutils::GetNativeFileDescriptor(env, javaFd);
+ if (sock < 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor");
+ return;
+ }
+
+ ScopedUtfChars addrStr(env, addr6);
+
+ in6_addr addr;
+ if (inet_pton(AF_INET6, addrStr.c_str(), &addr) != 1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid IPv6 address %s",
+ addrStr.c_str());
+ return;
+ }
+
+ struct ipv6_mreq mreq = {addr, ifindex};
+ int ret = setsockopt(sock, SOL_IPV6, IPV6_JOIN_ANYCAST, &mreq, sizeof(mreq));
+ if (ret) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "setsockopt IPV6_JOIN_ANYCAST failed: %s",
+ strerror(errno));
+ return;
+ }
+}
+
+static void com_android_server_connectivity_ClatCoordinator_configurePacketSocket(
+ JNIEnv* env, jobject clazz, jobject javaFd, jstring addr6, jint ifindex) {
+ ScopedUtfChars addrStr(env, addr6);
+
+ int sock = netjniutils::GetNativeFileDescriptor(env, javaFd);
+ if (sock < 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor");
+ return;
+ }
+
+ in6_addr addr;
+ if (inet_pton(AF_INET6, addrStr.c_str(), &addr) != 1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid IPv6 address %s",
+ addrStr.c_str());
+ return;
+ }
+
+ int ret = net::clat::configure_packet_socket(sock, &addr, ifindex);
+ if (ret < 0) {
+ throwIOException(env, "configure packet socket failed", -ret);
+ return;
+ }
+}
+
+int initTracker(const std::string& iface, const std::string& pfx96, const std::string& v4,
+ const std::string& v6, net::clat::ClatdTracker* output) {
+ strlcpy(output->iface, iface.c_str(), sizeof(output->iface));
+ output->ifIndex = if_nametoindex(iface.c_str());
+ if (output->ifIndex == 0) {
+ ALOGE("interface %s not found", output->iface);
+ return -1;
+ }
+
+ unsigned len = snprintf(output->v4iface, sizeof(output->v4iface),
+ "%s%s", DEVICEPREFIX, iface.c_str());
+ if (len >= sizeof(output->v4iface)) {
+ ALOGE("interface name too long '%s'", output->v4iface);
+ return -1;
+ }
+
+ output->v4ifIndex = if_nametoindex(output->v4iface);
+ if (output->v4ifIndex == 0) {
+ ALOGE("v4-interface %s not found", output->v4iface);
+ return -1;
+ }
+
+ if (!inet_pton(AF_INET6, pfx96.c_str(), &output->pfx96)) {
+ ALOGE("invalid IPv6 address specified for plat prefix: %s", pfx96.c_str());
+ return -1;
+ }
+
+ if (!inet_pton(AF_INET, v4.c_str(), &output->v4)) {
+ ALOGE("Invalid IPv4 address %s", v4.c_str());
+ return -1;
+ }
+
+ if (!inet_pton(AF_INET6, v6.c_str(), &output->v6)) {
+ ALOGE("Invalid source address %s", v6.c_str());
+ return -1;
+ }
+
+ return 0;
+}
+
+static jint com_android_server_connectivity_ClatCoordinator_startClatd(
+ JNIEnv* env, jobject clazz, jobject tunJavaFd, jobject readSockJavaFd,
+ jobject writeSockJavaFd, jstring iface, jstring pfx96, jstring v4, jstring v6) {
+ ScopedUtfChars ifaceStr(env, iface);
+ ScopedUtfChars pfx96Str(env, pfx96);
+ ScopedUtfChars v4Str(env, v4);
+ ScopedUtfChars v6Str(env, v6);
+
+ int tunFd = netjniutils::GetNativeFileDescriptor(env, tunJavaFd);
+ if (tunFd < 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid tun file descriptor");
+ return -1;
+ }
+
+ int readSock = netjniutils::GetNativeFileDescriptor(env, readSockJavaFd);
+ if (readSock < 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid read socket");
+ return -1;
+ }
+
+ int writeSock = netjniutils::GetNativeFileDescriptor(env, writeSockJavaFd);
+ if (writeSock < 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid write socket");
+ return -1;
+ }
+
+ // 1. create a throwaway socket to reserve a file descriptor number
+ int passedTunFd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (passedTunFd == -1) {
+ throwIOException(env, "socket(ipv6/udp) for tun fd failed", errno);
+ return -1;
+ }
+ int passedSockRead = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (passedSockRead == -1) {
+ throwIOException(env, "socket(ipv6/udp) for read socket failed", errno);
+ return -1;
+ }
+ int passedSockWrite = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (passedSockWrite == -1) {
+ throwIOException(env, "socket(ipv6/udp) for write socket failed", errno);
+ return -1;
+ }
+
+ // these are the FD we'll pass to clatd on the cli, so need it as a string
+ char passedTunFdStr[INT32_STRLEN];
+ char passedSockReadStr[INT32_STRLEN];
+ char passedSockWriteStr[INT32_STRLEN];
+ snprintf(passedTunFdStr, sizeof(passedTunFdStr), "%d", passedTunFd);
+ snprintf(passedSockReadStr, sizeof(passedSockReadStr), "%d", passedSockRead);
+ snprintf(passedSockWriteStr, sizeof(passedSockWriteStr), "%d", passedSockWrite);
+
+ // 2. we're going to use this as argv[0] to clatd to make ps output more useful
+ std::string progname("clatd-");
+ progname += ifaceStr.c_str();
+
+ // clang-format off
+ const char* args[] = {progname.c_str(),
+ "-i", ifaceStr.c_str(),
+ "-p", pfx96Str.c_str(),
+ "-4", v4Str.c_str(),
+ "-6", v6Str.c_str(),
+ "-t", passedTunFdStr,
+ "-r", passedSockReadStr,
+ "-w", passedSockWriteStr,
+ nullptr};
+ // clang-format on
+
+ // 3. register vfork requirement
+ posix_spawnattr_t attr;
+ if (int ret = posix_spawnattr_init(&attr)) {
+ throwIOException(env, "posix_spawnattr_init failed", ret);
+ return -1;
+ }
+
+ // TODO: use android::base::ScopeGuard.
+ if (int ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK)) {
+ posix_spawnattr_destroy(&attr);
+ throwIOException(env, "posix_spawnattr_setflags failed", ret);
+ return -1;
+ }
+
+ // 4. register dup2() action: this is what 'clears' the CLOEXEC flag
+ // on the tun fd that we want the child clatd process to inherit
+ // (this will happen after the vfork, and before the execve)
+ posix_spawn_file_actions_t fa;
+ if (int ret = posix_spawn_file_actions_init(&fa)) {
+ posix_spawnattr_destroy(&attr);
+ throwIOException(env, "posix_spawn_file_actions_init failed", ret);
+ return -1;
+ }
+
+ if (int ret = posix_spawn_file_actions_adddup2(&fa, tunFd, passedTunFd)) {
+ posix_spawnattr_destroy(&attr);
+ posix_spawn_file_actions_destroy(&fa);
+ throwIOException(env, "posix_spawn_file_actions_adddup2 for tun fd failed", ret);
+ return -1;
+ }
+ if (int ret = posix_spawn_file_actions_adddup2(&fa, readSock, passedSockRead)) {
+ posix_spawnattr_destroy(&attr);
+ posix_spawn_file_actions_destroy(&fa);
+ throwIOException(env, "posix_spawn_file_actions_adddup2 for read socket failed", ret);
+ return -1;
+ }
+ if (int ret = posix_spawn_file_actions_adddup2(&fa, writeSock, passedSockWrite)) {
+ posix_spawnattr_destroy(&attr);
+ posix_spawn_file_actions_destroy(&fa);
+ throwIOException(env, "posix_spawn_file_actions_adddup2 for write socket failed", ret);
+ return -1;
+ }
+
+ // 5. actually perform vfork/dup2/execve
+ pid_t pid;
+ if (int ret = posix_spawn(&pid, kClatdPath, &fa, &attr, (char* const*)args, nullptr)) {
+ posix_spawnattr_destroy(&attr);
+ posix_spawn_file_actions_destroy(&fa);
+ throwIOException(env, "posix_spawn failed", ret);
+ return -1;
+ }
+
+ posix_spawnattr_destroy(&attr);
+ posix_spawn_file_actions_destroy(&fa);
+
+ // 5. Start BPF if any
+ if (!net::clat::initMaps()) {
+ net::clat::ClatdTracker tracker = {};
+ if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
+ &tracker)) {
+ net::clat::maybeStartBpf(tracker);
+ }
+ }
+
+ return pid;
+}
+
+// Stop clatd process. SIGTERM with timeout first, if fail, SIGKILL.
+// See stopProcess() in system/netd/server/NetdConstants.cpp.
+// TODO: have a function stopProcess(int pid, const char *name) in common location and call it.
+static constexpr int WAITPID_ATTEMPTS = 50;
+static constexpr int WAITPID_RETRY_INTERVAL_US = 100000;
+
+static void stopClatdProcess(int pid) {
+ int err = kill(pid, SIGTERM);
+ if (err) {
+ err = errno;
+ }
+ if (err == ESRCH) {
+ ALOGE("clatd child process %d unexpectedly disappeared", pid);
+ return;
+ }
+ if (err) {
+ ALOGE("Error killing clatd child process %d: %s", pid, strerror(err));
+ }
+ int status = 0;
+ int ret = 0;
+ for (int count = 0; ret == 0 && count < WAITPID_ATTEMPTS; count++) {
+ usleep(WAITPID_RETRY_INTERVAL_US);
+ ret = waitpid(pid, &status, WNOHANG);
+ }
+ if (ret == 0) {
+ ALOGE("Failed to SIGTERM clatd pid=%d, try SIGKILL", pid);
+ // TODO: fix that kill failed or waitpid doesn't return.
+ kill(pid, SIGKILL);
+ ret = waitpid(pid, &status, 0);
+ }
+ if (ret == -1) {
+ ALOGE("Error waiting for clatd child process %d: %s", pid, strerror(errno));
+ } else {
+ ALOGD("clatd process %d terminated status=%d", pid, status);
+ }
+}
+
+static void com_android_server_connectivity_ClatCoordinator_stopClatd(JNIEnv* env, jobject clazz,
+ jstring iface, jstring pfx96,
+ jstring v4, jstring v6,
+ jint pid) {
+ ScopedUtfChars ifaceStr(env, iface);
+ ScopedUtfChars pfx96Str(env, pfx96);
+ ScopedUtfChars v4Str(env, v4);
+ ScopedUtfChars v6Str(env, v6);
+
+ if (pid <= 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "Invalid pid");
+ return;
+ }
+
+ if (!net::clat::initMaps()) {
+ net::clat::ClatdTracker tracker = {};
+ if (!initTracker(ifaceStr.c_str(), pfx96Str.c_str(), v4Str.c_str(), v6Str.c_str(),
+ &tracker)) {
+ net::clat::maybeStopBpf(tracker);
+ }
+ }
+
+ stopClatdProcess(pid);
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"native_selectIpv4Address", "(Ljava/lang/String;I)Ljava/lang/String;",
+ (void*)com_android_server_connectivity_ClatCoordinator_selectIpv4Address},
+ {"native_generateIpv6Address",
+ "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
+ (void*)com_android_server_connectivity_ClatCoordinator_generateIpv6Address},
+ {"native_createTunInterface", "(Ljava/lang/String;)I",
+ (void*)com_android_server_connectivity_ClatCoordinator_createTunInterface},
+ {"native_detectMtu", "(Ljava/lang/String;II)I",
+ (void*)com_android_server_connectivity_ClatCoordinator_detectMtu},
+ {"native_openPacketSocket", "()I",
+ (void*)com_android_server_connectivity_ClatCoordinator_openPacketSocket},
+ {"native_openRawSocket6", "(I)I",
+ (void*)com_android_server_connectivity_ClatCoordinator_openRawSocket6},
+ {"native_addAnycastSetsockopt", "(Ljava/io/FileDescriptor;Ljava/lang/String;I)V",
+ (void*)com_android_server_connectivity_ClatCoordinator_addAnycastSetsockopt},
+ {"native_configurePacketSocket", "(Ljava/io/FileDescriptor;Ljava/lang/String;I)V",
+ (void*)com_android_server_connectivity_ClatCoordinator_configurePacketSocket},
+ {"native_startClatd",
+ "(Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/io/FileDescriptor;Ljava/lang/"
+ "String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I",
+ (void*)com_android_server_connectivity_ClatCoordinator_startClatd},
+ {"native_stopClatd",
+ "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V",
+ (void*)com_android_server_connectivity_ClatCoordinator_stopClatd},
+};
+
+int register_android_server_connectivity_ClatCoordinator(JNIEnv* env) {
+ return jniRegisterNativeMethods(env, "com/android/server/connectivity/ClatCoordinator",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
index 0012879..04d9671 100644
--- a/service/jni/onload.cpp
+++ b/service/jni/onload.cpp
@@ -20,6 +20,7 @@
namespace android {
int register_android_server_TestNetworkService(JNIEnv* env);
+int register_android_server_connectivity_ClatCoordinator(JNIEnv* env);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv *env;
@@ -32,6 +33,10 @@
return JNI_ERR;
}
+ if (register_android_server_connectivity_ClatCoordinator(env) < 0) {
+ return JNI_ERR;
+ }
+
return JNI_VERSION_1_6;
}
diff --git a/service/native/Android.bp b/service/native/Android.bp
new file mode 100644
index 0000000..b49457d
--- /dev/null
+++ b/service/native/Android.bp
@@ -0,0 +1,106 @@
+/*
+ * 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library {
+ name: "libtraffic_controller",
+ defaults: ["netd_defaults"],
+ srcs: [
+ "TrafficController.cpp",
+ ],
+ header_libs: [
+ "bpf_connectivity_headers",
+ "bpf_headers",
+ "bpf_syscall_wrappers",
+ ],
+ static_libs: [
+ "libnetdutils",
+ // TrafficController would use the constants of INetd so that add
+ // netd_aidl_interface-lateststable-ndk.
+ "netd_aidl_interface-lateststable-ndk",
+ ],
+ shared_libs: [
+ // TODO: Find a good way to remove libbase.
+ "libbase",
+ "libcutils",
+ "libutils",
+ "liblog",
+ ],
+ export_include_dirs: ["include"],
+ sanitize: {
+ cfi: true,
+ },
+ apex_available: [
+ "com.android.tethering",
+ ],
+ min_sdk_version: "30",
+}
+
+cc_library_shared {
+ name: "libtraffic_controller_jni",
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ "-Wthread-safety",
+ ],
+ srcs: [
+ "jni/*.cpp",
+ ],
+ header_libs: [
+ "bpf_connectivity_headers",
+ ],
+ static_libs: [
+ "libnetdutils",
+ "libtraffic_controller",
+ "netd_aidl_interface-lateststable-ndk",
+ ],
+ shared_libs: [
+ "libbase",
+ "liblog",
+ "libutils",
+ "libnativehelper",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ ],
+ min_sdk_version: "30",
+}
+
+cc_test {
+ name: "traffic_controller_unit_test",
+ test_suites: ["general-tests"],
+ require_root: true,
+ local_include_dirs: ["include"],
+ header_libs: [
+ "bpf_connectivity_headers",
+ ],
+ srcs: [
+ "TrafficControllerTest.cpp",
+ ],
+ static_libs: [
+ "libbase",
+ "libgmock",
+ "liblog",
+ "libnetdutils",
+ "libtraffic_controller",
+ "libutils",
+ "netd_aidl_interface-lateststable-ndk",
+ ],
+}
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
new file mode 100644
index 0000000..97fcc43
--- /dev/null
+++ b/service/native/TrafficController.cpp
@@ -0,0 +1,904 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "TrafficController"
+#include <inttypes.h>
+#include <linux/if_ether.h>
+#include <linux/in.h>
+#include <linux/inet_diag.h>
+#include <linux/netlink.h>
+#include <linux/sock_diag.h>
+#include <linux/unistd.h>
+#include <net/if.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+#include <map>
+#include <mutex>
+#include <unordered_set>
+#include <vector>
+
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+#include <netdutils/StatusOr.h>
+#include <netdutils/Syscalls.h>
+#include <netdutils/UidConstants.h>
+#include <netdutils/Utils.h>
+#include <private/android_filesystem_config.h>
+
+#include "TrafficController.h"
+#include "bpf/BpfMap.h"
+#include "netdutils/DumpWriter.h"
+
+namespace android {
+namespace net {
+
+using base::StringPrintf;
+using base::unique_fd;
+using bpf::BpfMap;
+using bpf::OVERFLOW_COUNTERSET;
+using bpf::synchronizeKernelRCU;
+using netdutils::DumpWriter;
+using netdutils::getIfaceList;
+using netdutils::NetlinkListener;
+using netdutils::NetlinkListenerInterface;
+using netdutils::ScopedIndent;
+using netdutils::Slice;
+using netdutils::sSyscalls;
+using netdutils::Status;
+using netdutils::statusFromErrno;
+using netdutils::StatusOr;
+using netdutils::status::ok;
+
+constexpr int kSockDiagMsgType = SOCK_DIAG_BY_FAMILY;
+constexpr int kSockDiagDoneMsgType = NLMSG_DONE;
+
+const char* TrafficController::LOCAL_DOZABLE = "fw_dozable";
+const char* TrafficController::LOCAL_STANDBY = "fw_standby";
+const char* TrafficController::LOCAL_POWERSAVE = "fw_powersave";
+const char* TrafficController::LOCAL_RESTRICTED = "fw_restricted";
+
+static_assert(BPF_PERMISSION_INTERNET == INetd::PERMISSION_INTERNET,
+ "Mismatch between BPF and AIDL permissions: PERMISSION_INTERNET");
+static_assert(BPF_PERMISSION_UPDATE_DEVICE_STATS == INetd::PERMISSION_UPDATE_DEVICE_STATS,
+ "Mismatch between BPF and AIDL permissions: PERMISSION_UPDATE_DEVICE_STATS");
+
+#define FLAG_MSG_TRANS(result, flag, value) \
+ do { \
+ if ((value) & (flag)) { \
+ (result).append(" " #flag); \
+ (value) &= ~(flag); \
+ } \
+ } while (0)
+
+const std::string uidMatchTypeToString(uint8_t match) {
+ std::string matchType;
+ FLAG_MSG_TRANS(matchType, HAPPY_BOX_MATCH, match);
+ FLAG_MSG_TRANS(matchType, PENALTY_BOX_MATCH, match);
+ FLAG_MSG_TRANS(matchType, DOZABLE_MATCH, match);
+ FLAG_MSG_TRANS(matchType, STANDBY_MATCH, match);
+ FLAG_MSG_TRANS(matchType, POWERSAVE_MATCH, match);
+ FLAG_MSG_TRANS(matchType, RESTRICTED_MATCH, match);
+ FLAG_MSG_TRANS(matchType, IIF_MATCH, match);
+ if (match) {
+ return StringPrintf("Unknown match: %u", match);
+ }
+ return matchType;
+}
+
+bool TrafficController::hasUpdateDeviceStatsPermission(uid_t uid) {
+ // This implementation is the same logic as method ActivityManager#checkComponentPermission.
+ // It implies that the calling uid can never be the same as PER_USER_RANGE.
+ uint32_t appId = uid % PER_USER_RANGE;
+ return ((appId == AID_ROOT) || (appId == AID_SYSTEM) ||
+ mPrivilegedUser.find(appId) != mPrivilegedUser.end());
+}
+
+const std::string UidPermissionTypeToString(int permission) {
+ if (permission == INetd::PERMISSION_NONE) {
+ return "PERMISSION_NONE";
+ }
+ if (permission == INetd::PERMISSION_UNINSTALLED) {
+ // This should never appear in the map, complain loudly if it does.
+ return "PERMISSION_UNINSTALLED error!";
+ }
+ std::string permissionType;
+ FLAG_MSG_TRANS(permissionType, BPF_PERMISSION_INTERNET, permission);
+ FLAG_MSG_TRANS(permissionType, BPF_PERMISSION_UPDATE_DEVICE_STATS, permission);
+ if (permission) {
+ return StringPrintf("Unknown permission: %u", permission);
+ }
+ return permissionType;
+}
+
+StatusOr<std::unique_ptr<NetlinkListenerInterface>> TrafficController::makeSkDestroyListener() {
+ const auto& sys = sSyscalls.get();
+ ASSIGN_OR_RETURN(auto event, sys.eventfd(0, EFD_CLOEXEC));
+ const int domain = AF_NETLINK;
+ const int type = SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK;
+ const int protocol = NETLINK_INET_DIAG;
+ ASSIGN_OR_RETURN(auto sock, sys.socket(domain, type, protocol));
+
+ // TODO: if too many sockets are closed too quickly, we can overflow the socket buffer, and
+ // some entries in mCookieTagMap will not be freed. In order to fix this we would need to
+ // periodically dump all sockets and remove the tag entries for sockets that have been closed.
+ // For now, set a large-enough buffer that we can close hundreds of sockets without getting
+ // ENOBUFS and leaking mCookieTagMap entries.
+ int rcvbuf = 512 * 1024;
+ auto ret = sys.setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
+ if (!ret.ok()) {
+ ALOGW("Failed to set SkDestroyListener buffer size to %d: %s", rcvbuf, ret.msg().c_str());
+ }
+
+ sockaddr_nl addr = {
+ .nl_family = AF_NETLINK,
+ .nl_groups = 1 << (SKNLGRP_INET_TCP_DESTROY - 1) | 1 << (SKNLGRP_INET_UDP_DESTROY - 1) |
+ 1 << (SKNLGRP_INET6_TCP_DESTROY - 1) | 1 << (SKNLGRP_INET6_UDP_DESTROY - 1)};
+ RETURN_IF_NOT_OK(sys.bind(sock, addr));
+
+ const sockaddr_nl kernel = {.nl_family = AF_NETLINK};
+ RETURN_IF_NOT_OK(sys.connect(sock, kernel));
+
+ std::unique_ptr<NetlinkListenerInterface> listener =
+ std::make_unique<NetlinkListener>(std::move(event), std::move(sock), "SkDestroyListen");
+
+ return listener;
+}
+
+Status TrafficController::initMaps() {
+ std::lock_guard guard(mMutex);
+
+ RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH));
+ RETURN_IF_NOT_OK(mUidCounterSetMap.init(UID_COUNTERSET_MAP_PATH));
+ RETURN_IF_NOT_OK(mAppUidStatsMap.init(APP_UID_STATS_MAP_PATH));
+ RETURN_IF_NOT_OK(mStatsMapA.init(STATS_MAP_A_PATH));
+ RETURN_IF_NOT_OK(mStatsMapB.init(STATS_MAP_B_PATH));
+ RETURN_IF_NOT_OK(mIfaceIndexNameMap.init(IFACE_INDEX_NAME_MAP_PATH));
+ RETURN_IF_NOT_OK(mIfaceStatsMap.init(IFACE_STATS_MAP_PATH));
+
+ RETURN_IF_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
+ RETURN_IF_NOT_OK(
+ mConfigurationMap.writeValue(UID_RULES_CONFIGURATION_KEY, DEFAULT_CONFIG, BPF_ANY));
+ RETURN_IF_NOT_OK(mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, SELECT_MAP_A,
+ BPF_ANY));
+
+ RETURN_IF_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
+ RETURN_IF_NOT_OK(mUidOwnerMap.clear());
+ RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
+
+ return netdutils::status::ok;
+}
+
+Status TrafficController::start() {
+ RETURN_IF_NOT_OK(initMaps());
+
+ // Fetch the list of currently-existing interfaces. At this point NetlinkHandler is
+ // already running, so it will call addInterface() when any new interface appears.
+ // TODO: Clean-up addInterface() after interface monitoring is in
+ // NetworkStatsService.
+ std::map<std::string, uint32_t> ifacePairs;
+ ASSIGN_OR_RETURN(ifacePairs, getIfaceList());
+ for (const auto& ifacePair:ifacePairs) {
+ addInterface(ifacePair.first.c_str(), ifacePair.second);
+ }
+
+ auto result = makeSkDestroyListener();
+ if (!isOk(result)) {
+ ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str());
+ } else {
+ mSkDestroyListener = std::move(result.value());
+ }
+ // Rx handler extracts nfgenmsg looks up and invokes registered dispatch function.
+ const auto rxHandler = [this](const nlmsghdr&, const Slice msg) {
+ std::lock_guard guard(mMutex);
+ inet_diag_msg diagmsg = {};
+ if (extract(msg, diagmsg) < sizeof(inet_diag_msg)) {
+ ALOGE("Unrecognized netlink message: %s", toString(msg).c_str());
+ return;
+ }
+ uint64_t sock_cookie = static_cast<uint64_t>(diagmsg.id.idiag_cookie[0]) |
+ (static_cast<uint64_t>(diagmsg.id.idiag_cookie[1]) << 32);
+
+ Status s = mCookieTagMap.deleteValue(sock_cookie);
+ if (!isOk(s) && s.code() != ENOENT) {
+ ALOGE("Failed to delete cookie %" PRIx64 ": %s", sock_cookie, toString(s).c_str());
+ return;
+ }
+ };
+ expectOk(mSkDestroyListener->subscribe(kSockDiagMsgType, rxHandler));
+
+ // In case multiple netlink message comes in as a stream, we need to handle the rxDone message
+ // properly.
+ const auto rxDoneHandler = [](const nlmsghdr&, const Slice msg) {
+ // Ignore NLMSG_DONE messages
+ inet_diag_msg diagmsg = {};
+ extract(msg, diagmsg);
+ };
+ expectOk(mSkDestroyListener->subscribe(kSockDiagDoneMsgType, rxDoneHandler));
+
+ return netdutils::status::ok;
+}
+
+int TrafficController::setCounterSet(int counterSetNum, uid_t uid, uid_t callingUid) {
+ if (counterSetNum < 0 || counterSetNum >= OVERFLOW_COUNTERSET) return -EINVAL;
+
+ std::lock_guard guard(mMutex);
+ if (!hasUpdateDeviceStatsPermission(callingUid)) return -EPERM;
+
+ // The default counter set for all uid is 0, so deleting the current counterset for that uid
+ // will automatically set it to 0.
+ if (counterSetNum == 0) {
+ Status res = mUidCounterSetMap.deleteValue(uid);
+ if (isOk(res) || (!isOk(res) && res.code() == ENOENT)) {
+ return 0;
+ } else {
+ ALOGE("Failed to delete the counterSet: %s\n", strerror(res.code()));
+ return -res.code();
+ }
+ }
+ uint8_t tmpCounterSetNum = (uint8_t)counterSetNum;
+ Status res = mUidCounterSetMap.writeValue(uid, tmpCounterSetNum, BPF_ANY);
+ if (!isOk(res)) {
+ ALOGE("Failed to set the counterSet: %s, fd: %d", strerror(res.code()),
+ mUidCounterSetMap.getMap().get());
+ return -res.code();
+ }
+ return 0;
+}
+
+// This method only get called by system_server when an app get uinstalled, it
+// is called inside removeUidsLocked() while holding mStatsLock. So it is safe
+// to iterate and modify the stats maps.
+int TrafficController::deleteTagData(uint32_t tag, uid_t uid, uid_t callingUid) {
+ std::lock_guard guard(mMutex);
+ if (!hasUpdateDeviceStatsPermission(callingUid)) return -EPERM;
+
+ // First we go through the cookieTagMap to delete the target uid tag combination. Or delete all
+ // the tags related to the uid if the tag is 0.
+ const auto deleteMatchedCookieEntries = [uid, tag](const uint64_t& key,
+ const UidTagValue& value,
+ BpfMap<uint64_t, UidTagValue>& map) {
+ if (value.uid == uid && (value.tag == tag || tag == 0)) {
+ auto res = map.deleteValue(key);
+ if (res.ok() || (res.error().code() == ENOENT)) {
+ return base::Result<void>();
+ }
+ ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s\n", key,
+ strerror(res.error().code()));
+ }
+ // Move forward to next cookie in the map.
+ return base::Result<void>();
+ };
+ mCookieTagMap.iterateWithValue(deleteMatchedCookieEntries);
+ // Now we go through the Tag stats map and delete the data entry with correct uid and tag
+ // combination. Or all tag stats under that uid if the target tag is 0.
+ const auto deleteMatchedUidTagEntries = [uid, tag](const StatsKey& key,
+ BpfMap<StatsKey, StatsValue>& map) {
+ if (key.uid == uid && (key.tag == tag || tag == 0)) {
+ auto res = map.deleteValue(key);
+ if (res.ok() || (res.error().code() == ENOENT)) {
+ //Entry is deleted, use the current key to get a new nextKey;
+ return base::Result<void>();
+ }
+ ALOGE("Failed to delete data(uid=%u, tag=%u): %s\n", key.uid, key.tag,
+ strerror(res.error().code()));
+ }
+ return base::Result<void>();
+ };
+ mStatsMapB.iterate(deleteMatchedUidTagEntries);
+ mStatsMapA.iterate(deleteMatchedUidTagEntries);
+ // If the tag is not zero, we already deleted all the data entry required. If tag is 0, we also
+ // need to delete the stats stored in uidStatsMap and counterSet map.
+ if (tag != 0) return 0;
+
+ auto res = mUidCounterSetMap.deleteValue(uid);
+ if (!res.ok() && res.error().code() != ENOENT) {
+ ALOGE("Failed to delete counterSet data(uid=%u, tag=%u): %s\n", uid, tag,
+ strerror(res.error().code()));
+ }
+
+ auto deleteAppUidStatsEntry = [uid](const uint32_t& key,
+ BpfMap<uint32_t, StatsValue>& map) -> base::Result<void> {
+ if (key == uid) {
+ auto res = map.deleteValue(key);
+ if (res.ok() || (res.error().code() == ENOENT)) {
+ return {};
+ }
+ ALOGE("Failed to delete data(uid=%u): %s", key, strerror(res.error().code()));
+ }
+ return {};
+ };
+ mAppUidStatsMap.iterate(deleteAppUidStatsEntry);
+ return 0;
+}
+
+int TrafficController::addInterface(const char* name, uint32_t ifaceIndex) {
+ IfaceValue iface;
+ if (ifaceIndex == 0) {
+ ALOGE("Unknown interface %s(%d)", name, ifaceIndex);
+ return -1;
+ }
+
+ strlcpy(iface.name, name, sizeof(IfaceValue));
+ Status res = mIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY);
+ if (!isOk(res)) {
+ ALOGE("Failed to add iface %s(%d): %s", name, ifaceIndex, strerror(res.code()));
+ return -res.code();
+ }
+ return 0;
+}
+
+Status TrafficController::updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
+ FirewallType type) {
+ std::lock_guard guard(mMutex);
+ if ((rule == ALLOW && type == ALLOWLIST) || (rule == DENY && type == DENYLIST)) {
+ RETURN_IF_NOT_OK(addRule(uid, match));
+ } else if ((rule == ALLOW && type == DENYLIST) || (rule == DENY && type == ALLOWLIST)) {
+ RETURN_IF_NOT_OK(removeRule(uid, match));
+ } else {
+ //Cannot happen.
+ return statusFromErrno(EINVAL, "");
+ }
+ return netdutils::status::ok;
+}
+
+Status TrafficController::removeRule(uint32_t uid, UidOwnerMatchType match) {
+ auto oldMatch = mUidOwnerMap.readValue(uid);
+ if (oldMatch.ok()) {
+ UidOwnerValue newMatch = {
+ .iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif,
+ .rule = static_cast<uint8_t>(oldMatch.value().rule & ~match),
+ };
+ if (newMatch.rule == 0) {
+ RETURN_IF_NOT_OK(mUidOwnerMap.deleteValue(uid));
+ } else {
+ RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
+ }
+ } else {
+ return statusFromErrno(ENOENT, StringPrintf("uid: %u does not exist in map", uid));
+ }
+ return netdutils::status::ok;
+}
+
+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) {
+ 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,
+ .rule = static_cast<uint8_t>(oldMatch.value().rule | match),
+ };
+ RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
+ } else {
+ UidOwnerValue newMatch = {
+ .iif = iif,
+ .rule = static_cast<uint8_t>(match),
+ };
+ RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
+ }
+ return netdutils::status::ok;
+}
+
+Status TrafficController::updateUidOwnerMap(const uint32_t uid,
+ UidOwnerMatchType matchType, IptOp op) {
+ std::lock_guard guard(mMutex);
+ if (op == IptOpDelete) {
+ RETURN_IF_NOT_OK(removeRule(uid, matchType));
+ } else if (op == IptOpInsert) {
+ RETURN_IF_NOT_OK(addRule(uid, matchType));
+ } else {
+ // Cannot happen.
+ return statusFromErrno(EINVAL, StringPrintf("invalid IptOp: %d, %d", op, matchType));
+ }
+ return netdutils::status::ok;
+}
+
+FirewallType TrafficController::getFirewallType(ChildChain chain) {
+ switch (chain) {
+ case DOZABLE:
+ return ALLOWLIST;
+ case STANDBY:
+ return DENYLIST;
+ case POWERSAVE:
+ return ALLOWLIST;
+ case RESTRICTED:
+ return ALLOWLIST;
+ case NONE:
+ default:
+ return DENYLIST;
+ }
+}
+
+int TrafficController::changeUidOwnerRule(ChildChain chain, uid_t uid, FirewallRule rule,
+ FirewallType type) {
+ Status res;
+ switch (chain) {
+ case DOZABLE:
+ res = updateOwnerMapEntry(DOZABLE_MATCH, uid, rule, type);
+ break;
+ case STANDBY:
+ res = updateOwnerMapEntry(STANDBY_MATCH, uid, rule, type);
+ break;
+ case POWERSAVE:
+ res = updateOwnerMapEntry(POWERSAVE_MATCH, uid, rule, type);
+ break;
+ case RESTRICTED:
+ res = updateOwnerMapEntry(RESTRICTED_MATCH, uid, rule, type);
+ break;
+ case NONE:
+ default:
+ ALOGW("Unknown child chain: %d", chain);
+ return -EINVAL;
+ }
+ if (!isOk(res)) {
+ ALOGE("change uid(%u) rule of %d failed: %s, rule: %d, type: %d", uid, chain,
+ res.msg().c_str(), rule, type);
+ return -res.code();
+ }
+ return 0;
+}
+
+Status TrafficController::replaceRulesInMap(const UidOwnerMatchType match,
+ const std::vector<int32_t>& uids) {
+ std::lock_guard guard(mMutex);
+ std::set<int32_t> uidSet(uids.begin(), uids.end());
+ std::vector<uint32_t> uidsToDelete;
+ auto getUidsToDelete = [&uidsToDelete, &uidSet](const uint32_t& key,
+ const BpfMap<uint32_t, UidOwnerValue>&) {
+ if (uidSet.find((int32_t) key) == uidSet.end()) {
+ uidsToDelete.push_back(key);
+ }
+ return base::Result<void>();
+ };
+ RETURN_IF_NOT_OK(mUidOwnerMap.iterate(getUidsToDelete));
+
+ for(auto uid : uidsToDelete) {
+ RETURN_IF_NOT_OK(removeRule(uid, match));
+ }
+
+ for (auto uid : uids) {
+ RETURN_IF_NOT_OK(addRule(uid, match));
+ }
+ return netdutils::status::ok;
+}
+
+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) {
+ netdutils::Status result = addRule(uid, IIF_MATCH, iif);
+ if (!isOk(result)) {
+ ALOGW("addRule failed(%d): uid=%d iif=%d", result.code(), uid, iif);
+ }
+ }
+ return netdutils::status::ok;
+}
+
+Status TrafficController::removeUidInterfaceRules(const std::vector<int32_t>& uidsToDelete) {
+ std::lock_guard guard(mMutex);
+
+ for (auto uid : uidsToDelete) {
+ netdutils::Status result = removeRule(uid, IIF_MATCH);
+ if (!isOk(result)) {
+ ALOGW("removeRule failed(%d): uid=%d", result.code(), uid);
+ }
+ }
+ return netdutils::status::ok;
+}
+
+int TrafficController::replaceUidOwnerMap(const std::string& name, bool isAllowlist __unused,
+ const std::vector<int32_t>& uids) {
+ // FirewallRule rule = isAllowlist ? ALLOW : DENY;
+ // FirewallType type = isAllowlist ? ALLOWLIST : DENYLIST;
+ Status res;
+ if (!name.compare(LOCAL_DOZABLE)) {
+ res = replaceRulesInMap(DOZABLE_MATCH, uids);
+ } else if (!name.compare(LOCAL_STANDBY)) {
+ res = replaceRulesInMap(STANDBY_MATCH, uids);
+ } else if (!name.compare(LOCAL_POWERSAVE)) {
+ res = replaceRulesInMap(POWERSAVE_MATCH, uids);
+ } else if (!name.compare(LOCAL_RESTRICTED)) {
+ res = replaceRulesInMap(RESTRICTED_MATCH, uids);
+ } else {
+ ALOGE("unknown chain name: %s", name.c_str());
+ return -EINVAL;
+ }
+ if (!isOk(res)) {
+ ALOGE("Failed to clean up chain: %s: %s", name.c_str(), res.msg().c_str());
+ return -res.code();
+ }
+ return 0;
+}
+
+int TrafficController::toggleUidOwnerMap(ChildChain chain, bool enable) {
+ std::lock_guard guard(mMutex);
+ uint32_t key = UID_RULES_CONFIGURATION_KEY;
+ auto oldConfiguration = mConfigurationMap.readValue(key);
+ if (!oldConfiguration.ok()) {
+ ALOGE("Cannot read the old configuration from map: %s",
+ oldConfiguration.error().message().c_str());
+ return -oldConfiguration.error().code();
+ }
+ Status res;
+ BpfConfig newConfiguration;
+ uint8_t match;
+ switch (chain) {
+ case DOZABLE:
+ match = DOZABLE_MATCH;
+ break;
+ case STANDBY:
+ match = STANDBY_MATCH;
+ break;
+ case POWERSAVE:
+ match = POWERSAVE_MATCH;
+ break;
+ case RESTRICTED:
+ match = RESTRICTED_MATCH;
+ break;
+ default:
+ return -EINVAL;
+ }
+ newConfiguration =
+ enable ? (oldConfiguration.value() | match) : (oldConfiguration.value() & (~match));
+ res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
+ if (!isOk(res)) {
+ ALOGE("Failed to toggleUidOwnerMap(%d): %s", chain, res.msg().c_str());
+ }
+ return -res.code();
+}
+
+Status TrafficController::swapActiveStatsMap() {
+ std::lock_guard guard(mMutex);
+
+ uint32_t key = CURRENT_STATS_MAP_CONFIGURATION_KEY;
+ auto oldConfiguration = mConfigurationMap.readValue(key);
+ if (!oldConfiguration.ok()) {
+ ALOGE("Cannot read the old configuration from map: %s",
+ oldConfiguration.error().message().c_str());
+ return Status(oldConfiguration.error().code(), oldConfiguration.error().message());
+ }
+
+ // Write to the configuration map to inform the kernel eBPF program to switch
+ // from using one map to the other. Use flag BPF_EXIST here since the map should
+ // be already populated in initMaps.
+ uint8_t newConfigure = (oldConfiguration.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A;
+ auto res = mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, newConfigure,
+ BPF_EXIST);
+ if (!res.ok()) {
+ ALOGE("Failed to toggle the stats map: %s", strerror(res.error().code()));
+ return res;
+ }
+ // After changing the config, we need to make sure all the current running
+ // eBPF programs are finished and all the CPUs are aware of this config change
+ // before we modify the old map. So we do a special hack here to wait for
+ // the kernel to do a synchronize_rcu(). Once the kernel called
+ // synchronize_rcu(), the config we just updated will be available to all cores
+ // and the next eBPF programs triggered inside the kernel will use the new
+ // map configuration. So once this function returns we can safely modify the
+ // old stats map without concerning about race between the kernel and
+ // userspace.
+ int ret = synchronizeKernelRCU();
+ if (ret) {
+ ALOGE("map swap synchronize_rcu() ended with failure: %s", strerror(-ret));
+ return statusFromErrno(-ret, "map swap synchronize_rcu() failed");
+ }
+ return netdutils::status::ok;
+}
+
+void TrafficController::setPermissionForUids(int permission, const std::vector<uid_t>& uids) {
+ std::lock_guard guard(mMutex);
+ if (permission == INetd::PERMISSION_UNINSTALLED) {
+ for (uid_t uid : uids) {
+ // Clean up all permission information for the related uid if all the
+ // packages related to it are uninstalled.
+ mPrivilegedUser.erase(uid);
+ Status ret = mUidPermissionMap.deleteValue(uid);
+ if (!isOk(ret) && ret.code() != ENOENT) {
+ ALOGE("Failed to clean up the permission for %u: %s", uid, strerror(ret.code()));
+ }
+ }
+ return;
+ }
+
+ bool privileged = (permission & INetd::PERMISSION_UPDATE_DEVICE_STATS);
+
+ for (uid_t uid : uids) {
+ if (privileged) {
+ mPrivilegedUser.insert(uid);
+ } else {
+ mPrivilegedUser.erase(uid);
+ }
+
+ // The map stores all the permissions that the UID has, except if the only permission
+ // the UID has is the INTERNET permission, then the UID should not appear in the map.
+ if (permission != INetd::PERMISSION_INTERNET) {
+ Status ret = mUidPermissionMap.writeValue(uid, permission, BPF_ANY);
+ if (!isOk(ret)) {
+ ALOGE("Failed to set permission: %s of uid(%u) to permission map: %s",
+ UidPermissionTypeToString(permission).c_str(), uid, strerror(ret.code()));
+ }
+ } else {
+ Status ret = mUidPermissionMap.deleteValue(uid);
+ if (!isOk(ret) && ret.code() != ENOENT) {
+ ALOGE("Failed to remove uid %u from permission map: %s", uid, strerror(ret.code()));
+ }
+ }
+ }
+}
+
+std::string getProgramStatus(const char *path) {
+ int ret = access(path, R_OK);
+ if (ret == 0) {
+ return StringPrintf("OK");
+ }
+ if (ret != 0 && errno == ENOENT) {
+ return StringPrintf("program is missing at: %s", path);
+ }
+ return StringPrintf("check Program %s error: %s", path, strerror(errno));
+}
+
+std::string getMapStatus(const base::unique_fd& map_fd, const char* path) {
+ if (map_fd.get() < 0) {
+ return StringPrintf("map fd lost");
+ }
+ if (access(path, F_OK) != 0) {
+ return StringPrintf("map not pinned to location: %s", path);
+ }
+ return StringPrintf("OK");
+}
+
+// NOLINTNEXTLINE(google-runtime-references): grandfathered pass by non-const reference
+void dumpBpfMap(const std::string& mapName, DumpWriter& dw, const std::string& header) {
+ dw.blankline();
+ dw.println("%s:", mapName.c_str());
+ if (!header.empty()) {
+ dw.println(header);
+ }
+}
+
+const String16 TrafficController::DUMP_KEYWORD = String16("trafficcontroller");
+
+void TrafficController::dump(DumpWriter& dw, bool verbose) {
+ std::lock_guard guard(mMutex);
+ ScopedIndent indentTop(dw);
+ dw.println("TrafficController");
+
+ ScopedIndent indentPreBpfModule(dw);
+
+ dw.blankline();
+ dw.println("mCookieTagMap status: %s",
+ getMapStatus(mCookieTagMap.getMap(), COOKIE_TAG_MAP_PATH).c_str());
+ dw.println("mUidCounterSetMap status: %s",
+ getMapStatus(mUidCounterSetMap.getMap(), UID_COUNTERSET_MAP_PATH).c_str());
+ dw.println("mAppUidStatsMap status: %s",
+ getMapStatus(mAppUidStatsMap.getMap(), APP_UID_STATS_MAP_PATH).c_str());
+ dw.println("mStatsMapA status: %s",
+ getMapStatus(mStatsMapA.getMap(), STATS_MAP_A_PATH).c_str());
+ dw.println("mStatsMapB status: %s",
+ getMapStatus(mStatsMapB.getMap(), STATS_MAP_B_PATH).c_str());
+ dw.println("mIfaceIndexNameMap status: %s",
+ getMapStatus(mIfaceIndexNameMap.getMap(), IFACE_INDEX_NAME_MAP_PATH).c_str());
+ dw.println("mIfaceStatsMap status: %s",
+ getMapStatus(mIfaceStatsMap.getMap(), IFACE_STATS_MAP_PATH).c_str());
+ dw.println("mConfigurationMap status: %s",
+ getMapStatus(mConfigurationMap.getMap(), CONFIGURATION_MAP_PATH).c_str());
+ dw.println("mUidOwnerMap status: %s",
+ getMapStatus(mUidOwnerMap.getMap(), UID_OWNER_MAP_PATH).c_str());
+
+ dw.blankline();
+ dw.println("Cgroup ingress program status: %s",
+ getProgramStatus(BPF_INGRESS_PROG_PATH).c_str());
+ dw.println("Cgroup egress program status: %s", getProgramStatus(BPF_EGRESS_PROG_PATH).c_str());
+ dw.println("xt_bpf ingress program status: %s",
+ getProgramStatus(XT_BPF_INGRESS_PROG_PATH).c_str());
+ dw.println("xt_bpf egress program status: %s",
+ getProgramStatus(XT_BPF_EGRESS_PROG_PATH).c_str());
+ dw.println("xt_bpf bandwidth allowlist program status: %s",
+ getProgramStatus(XT_BPF_ALLOWLIST_PROG_PATH).c_str());
+ dw.println("xt_bpf bandwidth denylist program status: %s",
+ getProgramStatus(XT_BPF_DENYLIST_PROG_PATH).c_str());
+
+ if (!verbose) {
+ return;
+ }
+
+ dw.blankline();
+ dw.println("BPF map content:");
+
+ ScopedIndent indentForMapContent(dw);
+
+ // Print CookieTagMap content.
+ dumpBpfMap("mCookieTagMap", dw, "");
+ const auto printCookieTagInfo = [&dw](const uint64_t& key, const UidTagValue& value,
+ const BpfMap<uint64_t, UidTagValue>&) {
+ dw.println("cookie=%" PRIu64 " tag=0x%x uid=%u", key, value.tag, value.uid);
+ return base::Result<void>();
+ };
+ base::Result<void> res = mCookieTagMap.iterateWithValue(printCookieTagInfo);
+ if (!res.ok()) {
+ dw.println("mCookieTagMap print end with error: %s", res.error().message().c_str());
+ }
+
+ // Print UidCounterSetMap content.
+ dumpBpfMap("mUidCounterSetMap", dw, "");
+ const auto printUidInfo = [&dw](const uint32_t& key, const uint8_t& value,
+ const BpfMap<uint32_t, uint8_t>&) {
+ dw.println("%u %u", key, value);
+ return base::Result<void>();
+ };
+ res = mUidCounterSetMap.iterateWithValue(printUidInfo);
+ if (!res.ok()) {
+ dw.println("mUidCounterSetMap print end with error: %s", res.error().message().c_str());
+ }
+
+ // Print AppUidStatsMap content.
+ std::string appUidStatsHeader = StringPrintf("uid rxBytes rxPackets txBytes txPackets");
+ dumpBpfMap("mAppUidStatsMap:", dw, appUidStatsHeader);
+ auto printAppUidStatsInfo = [&dw](const uint32_t& key, const StatsValue& value,
+ const BpfMap<uint32_t, StatsValue>&) {
+ dw.println("%u %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64, key, value.rxBytes,
+ value.rxPackets, value.txBytes, value.txPackets);
+ return base::Result<void>();
+ };
+ res = mAppUidStatsMap.iterateWithValue(printAppUidStatsInfo);
+ if (!res.ok()) {
+ dw.println("mAppUidStatsMap print end with error: %s", res.error().message().c_str());
+ }
+
+ // Print uidStatsMap content.
+ std::string statsHeader = StringPrintf("ifaceIndex ifaceName tag_hex uid_int cnt_set rxBytes"
+ " rxPackets txBytes txPackets");
+ dumpBpfMap("mStatsMapA", dw, statsHeader);
+ const auto printStatsInfo = [&dw, this](const StatsKey& key, const StatsValue& value,
+ const BpfMap<StatsKey, StatsValue>&) {
+ uint32_t ifIndex = key.ifaceIndex;
+ auto ifname = mIfaceIndexNameMap.readValue(ifIndex);
+ if (!ifname.ok()) {
+ ifname = IfaceValue{"unknown"};
+ }
+ dw.println("%u %s 0x%x %u %u %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64, ifIndex,
+ ifname.value().name, key.tag, key.uid, key.counterSet, value.rxBytes,
+ value.rxPackets, value.txBytes, value.txPackets);
+ return base::Result<void>();
+ };
+ res = mStatsMapA.iterateWithValue(printStatsInfo);
+ if (!res.ok()) {
+ dw.println("mStatsMapA print end with error: %s", res.error().message().c_str());
+ }
+
+ // Print TagStatsMap content.
+ dumpBpfMap("mStatsMapB", dw, statsHeader);
+ res = mStatsMapB.iterateWithValue(printStatsInfo);
+ if (!res.ok()) {
+ dw.println("mStatsMapB print end with error: %s", res.error().message().c_str());
+ }
+
+ // Print ifaceIndexToNameMap content.
+ dumpBpfMap("mIfaceIndexNameMap", dw, "");
+ const auto printIfaceNameInfo = [&dw](const uint32_t& key, const IfaceValue& value,
+ const BpfMap<uint32_t, IfaceValue>&) {
+ const char* ifname = value.name;
+ dw.println("ifaceIndex=%u ifaceName=%s", key, ifname);
+ return base::Result<void>();
+ };
+ res = mIfaceIndexNameMap.iterateWithValue(printIfaceNameInfo);
+ if (!res.ok()) {
+ dw.println("mIfaceIndexNameMap print end with error: %s", res.error().message().c_str());
+ }
+
+ // Print ifaceStatsMap content
+ std::string ifaceStatsHeader = StringPrintf("ifaceIndex ifaceName rxBytes rxPackets txBytes"
+ " txPackets");
+ dumpBpfMap("mIfaceStatsMap:", dw, ifaceStatsHeader);
+ const auto printIfaceStatsInfo = [&dw, this](const uint32_t& key, const StatsValue& value,
+ const BpfMap<uint32_t, StatsValue>&) {
+ auto ifname = mIfaceIndexNameMap.readValue(key);
+ if (!ifname.ok()) {
+ ifname = IfaceValue{"unknown"};
+ }
+ dw.println("%u %s %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64, key, ifname.value().name,
+ value.rxBytes, value.rxPackets, value.txBytes, value.txPackets);
+ return base::Result<void>();
+ };
+ res = mIfaceStatsMap.iterateWithValue(printIfaceStatsInfo);
+ if (!res.ok()) {
+ dw.println("mIfaceStatsMap print end with error: %s", res.error().message().c_str());
+ }
+
+ dw.blankline();
+
+ uint32_t key = UID_RULES_CONFIGURATION_KEY;
+ auto configuration = mConfigurationMap.readValue(key);
+ if (configuration.ok()) {
+ dw.println("current ownerMatch configuration: %d%s", configuration.value(),
+ uidMatchTypeToString(configuration.value()).c_str());
+ } else {
+ dw.println("mConfigurationMap read ownerMatch configure failed with error: %s",
+ configuration.error().message().c_str());
+ }
+
+ key = CURRENT_STATS_MAP_CONFIGURATION_KEY;
+ configuration = mConfigurationMap.readValue(key);
+ if (configuration.ok()) {
+ const char* statsMapDescription = "???";
+ switch (configuration.value()) {
+ case SELECT_MAP_A:
+ statsMapDescription = "SELECT_MAP_A";
+ break;
+ case SELECT_MAP_B:
+ statsMapDescription = "SELECT_MAP_B";
+ break;
+ // No default clause, so if we ever add a third map, this code will fail to build.
+ }
+ dw.println("current statsMap configuration: %d %s", configuration.value(),
+ statsMapDescription);
+ } else {
+ dw.println("mConfigurationMap read stats map configure failed with error: %s",
+ configuration.error().message().c_str());
+ }
+ dumpBpfMap("mUidOwnerMap", dw, "");
+ const auto printUidMatchInfo = [&dw, this](const uint32_t& key, const UidOwnerValue& value,
+ const BpfMap<uint32_t, UidOwnerValue>&) {
+ if (value.rule & IIF_MATCH) {
+ auto ifname = mIfaceIndexNameMap.readValue(value.iif);
+ if (ifname.ok()) {
+ dw.println("%u %s %s", key, uidMatchTypeToString(value.rule).c_str(),
+ ifname.value().name);
+ } else {
+ dw.println("%u %s %u", key, uidMatchTypeToString(value.rule).c_str(), value.iif);
+ }
+ } else {
+ dw.println("%u %s", key, uidMatchTypeToString(value.rule).c_str());
+ }
+ return base::Result<void>();
+ };
+ res = mUidOwnerMap.iterateWithValue(printUidMatchInfo);
+ if (!res.ok()) {
+ dw.println("mUidOwnerMap print end with error: %s", res.error().message().c_str());
+ }
+ dumpBpfMap("mUidPermissionMap", dw, "");
+ const auto printUidPermissionInfo = [&dw](const uint32_t& key, const int& value,
+ const BpfMap<uint32_t, uint8_t>&) {
+ dw.println("%u %s", key, UidPermissionTypeToString(value).c_str());
+ return base::Result<void>();
+ };
+ res = mUidPermissionMap.iterateWithValue(printUidPermissionInfo);
+ if (!res.ok()) {
+ dw.println("mUidPermissionMap print end with error: %s", res.error().message().c_str());
+ }
+
+ dumpBpfMap("mPrivilegedUser", dw, "");
+ for (uid_t uid : mPrivilegedUser) {
+ dw.println("%u ALLOW_UPDATE_DEVICE_STATS", (uint32_t)uid);
+ }
+}
+
+} // namespace net
+} // namespace android
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
new file mode 100644
index 0000000..f401636
--- /dev/null
+++ b/service/native/TrafficControllerTest.cpp
@@ -0,0 +1,750 @@
+/*
+ * Copyright 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.
+ *
+ * TrafficControllerTest.cpp - unit tests for TrafficController.cpp
+ */
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/inet_diag.h>
+#include <linux/sock_diag.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <gtest/gtest.h>
+
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <binder/Status.h>
+
+#include <netdutils/MockSyscalls.h>
+
+#include "TrafficController.h"
+#include "bpf/BpfUtils.h"
+
+using namespace android::bpf; // NOLINT(google-build-using-namespace): grandfathered
+
+namespace android {
+namespace net {
+
+using android::netdutils::Status;
+using base::Result;
+using netdutils::isOk;
+
+constexpr int TEST_MAP_SIZE = 10;
+constexpr uid_t TEST_UID = 10086;
+constexpr uid_t TEST_UID2 = 54321;
+constexpr uid_t TEST_UID3 = 98765;
+constexpr uint32_t TEST_TAG = 42;
+constexpr uint32_t TEST_COUNTERSET = 1;
+constexpr uint32_t DEFAULT_COUNTERSET = 0;
+
+#define ASSERT_VALID(x) ASSERT_TRUE((x).isValid())
+
+class TrafficControllerTest : public ::testing::Test {
+ protected:
+ TrafficControllerTest() {}
+ TrafficController mTc;
+ BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
+ BpfMap<uint32_t, uint8_t> mFakeUidCounterSetMap;
+ BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
+ BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
+ BpfMap<uint32_t, uint8_t> mFakeConfigurationMap;
+ BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap;
+ BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
+
+ void SetUp() {
+ std::lock_guard guard(mTc.mMutex);
+ ASSERT_EQ(0, setrlimitForTest());
+
+ mFakeCookieTagMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint64_t), sizeof(UidTagValue),
+ TEST_MAP_SIZE, 0));
+ ASSERT_VALID(mFakeCookieTagMap);
+
+ mFakeUidCounterSetMap.reset(
+ createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0));
+ ASSERT_VALID(mFakeUidCounterSetMap);
+
+ mFakeAppUidStatsMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(StatsValue),
+ TEST_MAP_SIZE, 0));
+ ASSERT_VALID(mFakeAppUidStatsMap);
+
+ mFakeStatsMapA.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(StatsKey), sizeof(StatsValue),
+ TEST_MAP_SIZE, 0));
+ ASSERT_VALID(mFakeStatsMapA);
+
+ mFakeConfigurationMap.reset(
+ createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), 1, 0));
+ ASSERT_VALID(mFakeConfigurationMap);
+
+ mFakeUidOwnerMap.reset(createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(UidOwnerValue),
+ TEST_MAP_SIZE, 0));
+ ASSERT_VALID(mFakeUidOwnerMap);
+ mFakeUidPermissionMap.reset(
+ createMap(BPF_MAP_TYPE_HASH, sizeof(uint32_t), sizeof(uint8_t), TEST_MAP_SIZE, 0));
+ ASSERT_VALID(mFakeUidPermissionMap);
+
+ mTc.mCookieTagMap.reset(dupFd(mFakeCookieTagMap.getMap()));
+ ASSERT_VALID(mTc.mCookieTagMap);
+ mTc.mUidCounterSetMap.reset(dupFd(mFakeUidCounterSetMap.getMap()));
+ ASSERT_VALID(mTc.mUidCounterSetMap);
+ mTc.mAppUidStatsMap.reset(dupFd(mFakeAppUidStatsMap.getMap()));
+ ASSERT_VALID(mTc.mAppUidStatsMap);
+ mTc.mStatsMapA.reset(dupFd(mFakeStatsMapA.getMap()));
+ ASSERT_VALID(mTc.mStatsMapA);
+ mTc.mConfigurationMap.reset(dupFd(mFakeConfigurationMap.getMap()));
+ ASSERT_VALID(mTc.mConfigurationMap);
+
+ // Always write to stats map A by default.
+ ASSERT_RESULT_OK(mTc.mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY,
+ SELECT_MAP_A, BPF_ANY));
+ mTc.mUidOwnerMap.reset(dupFd(mFakeUidOwnerMap.getMap()));
+ ASSERT_VALID(mTc.mUidOwnerMap);
+ mTc.mUidPermissionMap.reset(dupFd(mFakeUidPermissionMap.getMap()));
+ ASSERT_VALID(mTc.mUidPermissionMap);
+ mTc.mPrivilegedUser.clear();
+ }
+
+ int dupFd(const android::base::unique_fd& mapFd) {
+ return fcntl(mapFd.get(), F_DUPFD_CLOEXEC, 0);
+ }
+
+ void populateFakeStats(uint64_t cookie, uint32_t uid, uint32_t tag, StatsKey* key) {
+ UidTagValue cookieMapkey = {.uid = (uint32_t)uid, .tag = tag};
+ EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, cookieMapkey, BPF_ANY));
+ *key = {.uid = uid, .tag = tag, .counterSet = TEST_COUNTERSET, .ifaceIndex = 1};
+ StatsValue statsMapValue = {.rxPackets = 1, .rxBytes = 100};
+ uint8_t counterSet = TEST_COUNTERSET;
+ EXPECT_RESULT_OK(mFakeUidCounterSetMap.writeValue(uid, counterSet, BPF_ANY));
+ EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY));
+ key->tag = 0;
+ EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY));
+ EXPECT_RESULT_OK(mFakeAppUidStatsMap.writeValue(uid, statsMapValue, BPF_ANY));
+ // put tag information back to statsKey
+ key->tag = tag;
+ }
+
+ void checkUidOwnerRuleForChain(ChildChain chain, UidOwnerMatchType match) {
+ uint32_t uid = TEST_UID;
+ EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, DENY, DENYLIST));
+ Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
+ EXPECT_RESULT_OK(value);
+ EXPECT_TRUE(value.value().rule & match);
+
+ uid = TEST_UID2;
+ EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, ALLOW, ALLOWLIST));
+ value = mFakeUidOwnerMap.readValue(uid);
+ EXPECT_RESULT_OK(value);
+ EXPECT_TRUE(value.value().rule & match);
+
+ EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, DENY, ALLOWLIST));
+ value = mFakeUidOwnerMap.readValue(uid);
+ EXPECT_FALSE(value.ok());
+ EXPECT_EQ(ENOENT, value.error().code());
+
+ uid = TEST_UID;
+ EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, ALLOW, DENYLIST));
+ value = mFakeUidOwnerMap.readValue(uid);
+ EXPECT_FALSE(value.ok());
+ EXPECT_EQ(ENOENT, value.error().code());
+
+ uid = TEST_UID3;
+ EXPECT_EQ(-ENOENT, mTc.changeUidOwnerRule(chain, uid, ALLOW, DENYLIST));
+ value = mFakeUidOwnerMap.readValue(uid);
+ EXPECT_FALSE(value.ok());
+ EXPECT_EQ(ENOENT, value.error().code());
+ }
+
+ void checkEachUidValue(const std::vector<int32_t>& uids, UidOwnerMatchType match) {
+ for (uint32_t uid : uids) {
+ Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
+ EXPECT_RESULT_OK(value);
+ EXPECT_TRUE(value.value().rule & match);
+ }
+ std::set<uint32_t> uidSet(uids.begin(), uids.end());
+ const auto checkNoOtherUid = [&uidSet](const int32_t& key,
+ const BpfMap<uint32_t, UidOwnerValue>&) {
+ EXPECT_NE(uidSet.end(), uidSet.find(key));
+ return Result<void>();
+ };
+ EXPECT_RESULT_OK(mFakeUidOwnerMap.iterate(checkNoOtherUid));
+ }
+
+ void checkUidMapReplace(const std::string& name, const std::vector<int32_t>& uids,
+ UidOwnerMatchType match) {
+ bool isAllowlist = true;
+ EXPECT_EQ(0, mTc.replaceUidOwnerMap(name, isAllowlist, uids));
+ checkEachUidValue(uids, match);
+
+ isAllowlist = false;
+ EXPECT_EQ(0, mTc.replaceUidOwnerMap(name, isAllowlist, uids));
+ checkEachUidValue(uids, match);
+ }
+
+ void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint8_t expectedRule,
+ uint32_t expectedIif) {
+ for (uint32_t uid : appUids) {
+ Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
+ EXPECT_RESULT_OK(value);
+ EXPECT_EQ(expectedRule, value.value().rule)
+ << "Expected rule for UID " << uid << " to be " << expectedRule << ", but was "
+ << value.value().rule;
+ EXPECT_EQ(expectedIif, value.value().iif)
+ << "Expected iif for UID " << uid << " to be " << expectedIif << ", but was "
+ << value.value().iif;
+ }
+ }
+
+ template <class Key, class Value>
+ void expectMapEmpty(BpfMap<Key, Value>& map) {
+ auto isEmpty = map.isEmpty();
+ EXPECT_RESULT_OK(isEmpty);
+ EXPECT_TRUE(isEmpty.value());
+ }
+
+ void expectUidPermissionMapValues(const std::vector<uid_t>& appUids, uint8_t expectedValue) {
+ for (uid_t uid : appUids) {
+ Result<uint8_t> value = mFakeUidPermissionMap.readValue(uid);
+ EXPECT_RESULT_OK(value);
+ EXPECT_EQ(expectedValue, value.value())
+ << "Expected value for UID " << uid << " to be " << expectedValue
+ << ", but was " << value.value();
+ }
+ }
+
+ void expectPrivilegedUserSet(const std::vector<uid_t>& appUids) {
+ std::lock_guard guard(mTc.mMutex);
+ EXPECT_EQ(appUids.size(), mTc.mPrivilegedUser.size());
+ for (uid_t uid : appUids) {
+ EXPECT_NE(mTc.mPrivilegedUser.end(), mTc.mPrivilegedUser.find(uid));
+ }
+ }
+
+ void expectPrivilegedUserSetEmpty() {
+ std::lock_guard guard(mTc.mMutex);
+ EXPECT_TRUE(mTc.mPrivilegedUser.empty());
+ }
+
+ void addPrivilegedUid(uid_t uid) {
+ std::vector privilegedUid = {uid};
+ mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, privilegedUid);
+ }
+
+ void removePrivilegedUid(uid_t uid) {
+ std::vector privilegedUid = {uid};
+ mTc.setPermissionForUids(INetd::PERMISSION_NONE, privilegedUid);
+ }
+
+ void expectFakeStatsUnchanged(uint64_t cookie, uint32_t tag, uint32_t uid,
+ StatsKey tagStatsMapKey) {
+ Result<UidTagValue> cookieMapResult = mFakeCookieTagMap.readValue(cookie);
+ EXPECT_RESULT_OK(cookieMapResult);
+ EXPECT_EQ(uid, cookieMapResult.value().uid);
+ EXPECT_EQ(tag, cookieMapResult.value().tag);
+ Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid);
+ EXPECT_RESULT_OK(counterSetResult);
+ EXPECT_EQ(TEST_COUNTERSET, counterSetResult.value());
+ Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
+ EXPECT_RESULT_OK(statsMapResult);
+ EXPECT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
+ EXPECT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
+ tagStatsMapKey.tag = 0;
+ statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
+ EXPECT_RESULT_OK(statsMapResult);
+ EXPECT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
+ EXPECT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
+ auto appStatsResult = mFakeAppUidStatsMap.readValue(uid);
+ EXPECT_RESULT_OK(appStatsResult);
+ EXPECT_EQ((uint64_t)1, appStatsResult.value().rxPackets);
+ EXPECT_EQ((uint64_t)100, appStatsResult.value().rxBytes);
+ }
+
+ Status updateUidOwnerMaps(const std::vector<uint32_t>& appUids,
+ UidOwnerMatchType matchType, TrafficController::IptOp op) {
+ Status ret(0);
+ for (auto uid : appUids) {
+ ret = mTc.updateUidOwnerMap(uid, matchType, op);
+ if(!isOk(ret)) break;
+ }
+ return ret;
+ }
+
+};
+
+TEST_F(TrafficControllerTest, TestSetCounterSet) {
+ uid_t callingUid = TEST_UID2;
+ addPrivilegedUid(callingUid);
+ ASSERT_EQ(0, mTc.setCounterSet(TEST_COUNTERSET, TEST_UID, callingUid));
+ uid_t uid = TEST_UID;
+ Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid);
+ ASSERT_RESULT_OK(counterSetResult);
+ ASSERT_EQ(TEST_COUNTERSET, counterSetResult.value());
+ ASSERT_EQ(0, mTc.setCounterSet(DEFAULT_COUNTERSET, TEST_UID, callingUid));
+ ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid).ok());
+ expectMapEmpty(mFakeUidCounterSetMap);
+}
+
+TEST_F(TrafficControllerTest, TestSetCounterSetWithoutPermission) {
+ ASSERT_EQ(-EPERM, mTc.setCounterSet(TEST_COUNTERSET, TEST_UID, TEST_UID2));
+ uid_t uid = TEST_UID;
+ ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid).ok());
+ expectMapEmpty(mFakeUidCounterSetMap);
+}
+
+TEST_F(TrafficControllerTest, TestSetInvalidCounterSet) {
+ uid_t callingUid = TEST_UID2;
+ addPrivilegedUid(callingUid);
+ ASSERT_GT(0, mTc.setCounterSet(OVERFLOW_COUNTERSET, TEST_UID, callingUid));
+ uid_t uid = TEST_UID;
+ ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid).ok());
+ expectMapEmpty(mFakeUidCounterSetMap);
+}
+
+TEST_F(TrafficControllerTest, TestDeleteTagDataWithoutPermission) {
+ uint64_t cookie = 1;
+ uid_t uid = TEST_UID;
+ uint32_t tag = TEST_TAG;
+ StatsKey tagStatsMapKey;
+ populateFakeStats(cookie, uid, tag, &tagStatsMapKey);
+ ASSERT_EQ(-EPERM, mTc.deleteTagData(0, TEST_UID, TEST_UID2));
+
+ expectFakeStatsUnchanged(cookie, tag, uid, tagStatsMapKey);
+}
+
+TEST_F(TrafficControllerTest, TestDeleteTagData) {
+ uid_t callingUid = TEST_UID2;
+ addPrivilegedUid(callingUid);
+ uint64_t cookie = 1;
+ uid_t uid = TEST_UID;
+ uint32_t tag = TEST_TAG;
+ StatsKey tagStatsMapKey;
+ populateFakeStats(cookie, uid, tag, &tagStatsMapKey);
+ ASSERT_EQ(0, mTc.deleteTagData(TEST_TAG, TEST_UID, callingUid));
+ ASSERT_FALSE(mFakeCookieTagMap.readValue(cookie).ok());
+ Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid);
+ ASSERT_RESULT_OK(counterSetResult);
+ ASSERT_EQ(TEST_COUNTERSET, counterSetResult.value());
+ ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey).ok());
+ tagStatsMapKey.tag = 0;
+ Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
+ ASSERT_RESULT_OK(statsMapResult);
+ ASSERT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
+ ASSERT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
+ auto appStatsResult = mFakeAppUidStatsMap.readValue(TEST_UID);
+ ASSERT_RESULT_OK(appStatsResult);
+ ASSERT_EQ((uint64_t)1, appStatsResult.value().rxPackets);
+ ASSERT_EQ((uint64_t)100, appStatsResult.value().rxBytes);
+}
+
+TEST_F(TrafficControllerTest, TestDeleteAllUidData) {
+ uid_t callingUid = TEST_UID2;
+ addPrivilegedUid(callingUid);
+ uint64_t cookie = 1;
+ uid_t uid = TEST_UID;
+ uint32_t tag = TEST_TAG;
+ StatsKey tagStatsMapKey;
+ populateFakeStats(cookie, uid, tag, &tagStatsMapKey);
+ ASSERT_EQ(0, mTc.deleteTagData(0, TEST_UID, callingUid));
+ ASSERT_FALSE(mFakeCookieTagMap.readValue(cookie).ok());
+ ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid).ok());
+ ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey).ok());
+ tagStatsMapKey.tag = 0;
+ ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey).ok());
+ ASSERT_FALSE(mFakeAppUidStatsMap.readValue(TEST_UID).ok());
+}
+
+TEST_F(TrafficControllerTest, TestDeleteDataWithTwoTags) {
+ uid_t callingUid = TEST_UID2;
+ addPrivilegedUid(callingUid);
+ uint64_t cookie1 = 1;
+ uint64_t cookie2 = 2;
+ uid_t uid = TEST_UID;
+ uint32_t tag1 = TEST_TAG;
+ uint32_t tag2 = TEST_TAG + 1;
+ StatsKey tagStatsMapKey1;
+ StatsKey tagStatsMapKey2;
+ populateFakeStats(cookie1, uid, tag1, &tagStatsMapKey1);
+ populateFakeStats(cookie2, uid, tag2, &tagStatsMapKey2);
+ ASSERT_EQ(0, mTc.deleteTagData(TEST_TAG, TEST_UID, callingUid));
+ ASSERT_FALSE(mFakeCookieTagMap.readValue(cookie1).ok());
+ Result<UidTagValue> cookieMapResult = mFakeCookieTagMap.readValue(cookie2);
+ ASSERT_RESULT_OK(cookieMapResult);
+ ASSERT_EQ(TEST_UID, cookieMapResult.value().uid);
+ ASSERT_EQ(TEST_TAG + 1, cookieMapResult.value().tag);
+ Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid);
+ ASSERT_RESULT_OK(counterSetResult);
+ ASSERT_EQ(TEST_COUNTERSET, counterSetResult.value());
+ ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey1).ok());
+ Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey2);
+ ASSERT_RESULT_OK(statsMapResult);
+ ASSERT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
+ ASSERT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
+}
+
+TEST_F(TrafficControllerTest, TestDeleteDataWithTwoUids) {
+ uid_t callingUid = TEST_UID2;
+ addPrivilegedUid(callingUid);
+ uint64_t cookie1 = 1;
+ uint64_t cookie2 = 2;
+ uid_t uid1 = TEST_UID;
+ uid_t uid2 = TEST_UID + 1;
+ uint32_t tag = TEST_TAG;
+ StatsKey tagStatsMapKey1;
+ StatsKey tagStatsMapKey2;
+ populateFakeStats(cookie1, uid1, tag, &tagStatsMapKey1);
+ populateFakeStats(cookie2, uid2, tag, &tagStatsMapKey2);
+
+ // Delete the stats of one of the uid. Check if it is properly collected by
+ // removedStats.
+ ASSERT_EQ(0, mTc.deleteTagData(0, uid2, callingUid));
+ ASSERT_FALSE(mFakeCookieTagMap.readValue(cookie2).ok());
+ Result<uint8_t> counterSetResult = mFakeUidCounterSetMap.readValue(uid1);
+ ASSERT_RESULT_OK(counterSetResult);
+ ASSERT_EQ(TEST_COUNTERSET, counterSetResult.value());
+ ASSERT_FALSE(mFakeUidCounterSetMap.readValue(uid2).ok());
+ ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey2).ok());
+ tagStatsMapKey2.tag = 0;
+ ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey2).ok());
+ ASSERT_FALSE(mFakeAppUidStatsMap.readValue(uid2).ok());
+ tagStatsMapKey1.tag = 0;
+ Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey1);
+ ASSERT_RESULT_OK(statsMapResult);
+ ASSERT_EQ((uint64_t)1, statsMapResult.value().rxPackets);
+ ASSERT_EQ((uint64_t)100, statsMapResult.value().rxBytes);
+ auto appStatsResult = mFakeAppUidStatsMap.readValue(uid1);
+ ASSERT_RESULT_OK(appStatsResult);
+ ASSERT_EQ((uint64_t)1, appStatsResult.value().rxPackets);
+ ASSERT_EQ((uint64_t)100, appStatsResult.value().rxBytes);
+
+ // Delete the stats of the other uid.
+ ASSERT_EQ(0, mTc.deleteTagData(0, uid1, callingUid));
+ ASSERT_FALSE(mFakeStatsMapA.readValue(tagStatsMapKey1).ok());
+ ASSERT_FALSE(mFakeAppUidStatsMap.readValue(uid1).ok());
+}
+
+TEST_F(TrafficControllerTest, TestUpdateOwnerMapEntry) {
+ uint32_t uid = TEST_UID;
+ ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, DENY, DENYLIST)));
+ Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
+ ASSERT_RESULT_OK(value);
+ ASSERT_TRUE(value.value().rule & STANDBY_MATCH);
+
+ ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(DOZABLE_MATCH, uid, ALLOW, ALLOWLIST)));
+ value = mFakeUidOwnerMap.readValue(uid);
+ ASSERT_RESULT_OK(value);
+ ASSERT_TRUE(value.value().rule & DOZABLE_MATCH);
+
+ ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(DOZABLE_MATCH, uid, DENY, ALLOWLIST)));
+ value = mFakeUidOwnerMap.readValue(uid);
+ ASSERT_RESULT_OK(value);
+ ASSERT_FALSE(value.value().rule & DOZABLE_MATCH);
+
+ ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, ALLOW, DENYLIST)));
+ ASSERT_FALSE(mFakeUidOwnerMap.readValue(uid).ok());
+
+ uid = TEST_UID2;
+ ASSERT_FALSE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, ALLOW, DENYLIST)));
+ ASSERT_FALSE(mFakeUidOwnerMap.readValue(uid).ok());
+}
+
+TEST_F(TrafficControllerTest, TestChangeUidOwnerRule) {
+ checkUidOwnerRuleForChain(DOZABLE, DOZABLE_MATCH);
+ checkUidOwnerRuleForChain(STANDBY, STANDBY_MATCH);
+ checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
+ checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
+ ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(NONE, TEST_UID, ALLOW, ALLOWLIST));
+ ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(INVALID_CHAIN, TEST_UID, ALLOW, ALLOWLIST));
+}
+
+TEST_F(TrafficControllerTest, TestReplaceUidOwnerMap) {
+ std::vector<int32_t> uids = {TEST_UID, TEST_UID2, TEST_UID3};
+ checkUidMapReplace("fw_dozable", uids, DOZABLE_MATCH);
+ checkUidMapReplace("fw_standby", uids, STANDBY_MATCH);
+ checkUidMapReplace("fw_powersave", uids, POWERSAVE_MATCH);
+ checkUidMapReplace("fw_restricted", uids, RESTRICTED_MATCH);
+ ASSERT_EQ(-EINVAL, mTc.replaceUidOwnerMap("unknow", true, uids));
+}
+
+TEST_F(TrafficControllerTest, TestReplaceSameChain) {
+ std::vector<int32_t> uids = {TEST_UID, TEST_UID2, TEST_UID3};
+ checkUidMapReplace("fw_dozable", uids, DOZABLE_MATCH);
+ std::vector<int32_t> newUids = {TEST_UID2, TEST_UID3};
+ checkUidMapReplace("fw_dozable", newUids, DOZABLE_MATCH);
+}
+
+TEST_F(TrafficControllerTest, TestDenylistUidMatch) {
+ std::vector<uint32_t> appUids = {1000, 1001, 10012};
+ ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
+ TrafficController::IptOpInsert)));
+ expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0);
+ ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
+ TrafficController::IptOpDelete)));
+ expectMapEmpty(mFakeUidOwnerMap);
+}
+
+TEST_F(TrafficControllerTest, TestAllowlistUidMatch) {
+ std::vector<uint32_t> appUids = {1000, 1001, 10012};
+ ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpInsert)));
+ expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0);
+ ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpDelete)));
+ expectMapEmpty(mFakeUidOwnerMap);
+}
+
+TEST_F(TrafficControllerTest, TestReplaceMatchUid) {
+ std::vector<uint32_t> appUids = {1000, 1001, 10012};
+ // Add appUids to the denylist and expect that their values are all PENALTY_BOX_MATCH.
+ ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
+ TrafficController::IptOpInsert)));
+ expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0);
+
+ // Add the same UIDs to the allowlist and expect that we get PENALTY_BOX_MATCH |
+ // HAPPY_BOX_MATCH.
+ ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpInsert)));
+ expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH | PENALTY_BOX_MATCH, 0);
+
+ // Remove the same UIDs from the allowlist and check the PENALTY_BOX_MATCH is still there.
+ ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpDelete)));
+ expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0);
+
+ // Remove the same UIDs from the denylist and check the map is empty.
+ ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
+ TrafficController::IptOpDelete)));
+ ASSERT_FALSE(mFakeUidOwnerMap.getFirstKey().ok());
+}
+
+TEST_F(TrafficControllerTest, TestDeleteWrongMatchSilentlyFails) {
+ std::vector<uint32_t> appUids = {1000, 1001, 10012};
+ // If the uid does not exist in the map, trying to delete a rule about it will fail.
+ ASSERT_FALSE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH,
+ TrafficController::IptOpDelete)));
+ expectMapEmpty(mFakeUidOwnerMap);
+
+ // Add denylist rules for appUids.
+ ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH,
+ TrafficController::IptOpInsert)));
+ expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0);
+
+ // Delete (non-existent) denylist rules for appUids, and check that this silently does
+ // nothing if the uid is in the map but does not have denylist match. This is required because
+ // NetworkManagementService will try to remove a uid from denylist after adding it to the
+ // allowlist and if the remove fails it will not update the uid status.
+ ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
+ TrafficController::IptOpDelete)));
+ expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0);
+}
+
+TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRules) {
+ int iif0 = 15;
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif0, {1000, 1001})));
+ expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0);
+
+ // Add some non-overlapping new uids. They should coexist with existing rules
+ int iif1 = 16;
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {2000, 2001})));
+ expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0);
+ expectUidOwnerMapValues({2000, 2001}, IIF_MATCH, iif1);
+
+ // Overwrite some existing uids
+ int iif2 = 17;
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif2, {1000, 2000})));
+ expectUidOwnerMapValues({1001}, IIF_MATCH, iif0);
+ expectUidOwnerMapValues({2001}, IIF_MATCH, iif1);
+ expectUidOwnerMapValues({1000, 2000}, IIF_MATCH, iif2);
+}
+
+TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRules) {
+ int iif0 = 15;
+ int iif1 = 16;
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif0, {1000, 1001})));
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {2000, 2001})));
+ expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0);
+ expectUidOwnerMapValues({2000, 2001}, IIF_MATCH, iif1);
+
+ // Rmove some uids
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001, 2001})));
+ expectUidOwnerMapValues({1000}, IIF_MATCH, iif0);
+ expectUidOwnerMapValues({2000}, IIF_MATCH, iif1);
+ checkEachUidValue({1000, 2000}, IIF_MATCH); // Make sure there are only two uids remaining
+
+ // Remove non-existent uids shouldn't fail
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({2000, 3000})));
+ expectUidOwnerMapValues({1000}, IIF_MATCH, iif0);
+ checkEachUidValue({1000}, IIF_MATCH); // Make sure there are only one uid remaining
+
+ // Remove everything
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+ expectMapEmpty(mFakeUidOwnerMap);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithExistingMatches) {
+ // Set up existing PENALTY_BOX_MATCH rules
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000, 1001, 10012}, PENALTY_BOX_MATCH,
+ TrafficController::IptOpInsert)));
+ expectUidOwnerMapValues({1000, 1001, 10012}, PENALTY_BOX_MATCH, 0);
+
+ // Add some partially-overlapping uid owner rules and check result
+ int iif1 = 32;
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {10012, 10013, 10014})));
+ expectUidOwnerMapValues({1000, 1001}, PENALTY_BOX_MATCH, 0);
+ expectUidOwnerMapValues({10012}, PENALTY_BOX_MATCH | IIF_MATCH, iif1);
+ expectUidOwnerMapValues({10013, 10014}, IIF_MATCH, iif1);
+
+ // Removing some PENALTY_BOX_MATCH rules should not change uid interface rule
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1001, 10012}, PENALTY_BOX_MATCH,
+ TrafficController::IptOpDelete)));
+ expectUidOwnerMapValues({1000}, PENALTY_BOX_MATCH, 0);
+ expectUidOwnerMapValues({10012, 10013, 10014}, IIF_MATCH, iif1);
+
+ // Remove all uid interface rules
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({10012, 10013, 10014})));
+ expectUidOwnerMapValues({1000}, PENALTY_BOX_MATCH, 0);
+ // Make sure these are the only uids left
+ checkEachUidValue({1000}, PENALTY_BOX_MATCH);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithNewMatches) {
+ int iif1 = 56;
+ // Set up existing uid interface rules
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {10001, 10002})));
+ expectUidOwnerMapValues({10001, 10002}, IIF_MATCH, iif1);
+
+ // Add some partially-overlapping doze rules
+ EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_dozable", true, {10002, 10003}));
+ expectUidOwnerMapValues({10001}, IIF_MATCH, iif1);
+ expectUidOwnerMapValues({10002}, DOZABLE_MATCH | IIF_MATCH, iif1);
+ expectUidOwnerMapValues({10003}, DOZABLE_MATCH, 0);
+
+ // Introduce a third rule type (powersave) on various existing UIDs
+ EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_powersave", true, {10000, 10001, 10002, 10003}));
+ expectUidOwnerMapValues({10000}, POWERSAVE_MATCH, 0);
+ expectUidOwnerMapValues({10001}, POWERSAVE_MATCH | IIF_MATCH, iif1);
+ expectUidOwnerMapValues({10002}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif1);
+ expectUidOwnerMapValues({10003}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
+
+ // Remove all doze rules
+ EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_dozable", true, {}));
+ expectUidOwnerMapValues({10000}, POWERSAVE_MATCH, 0);
+ expectUidOwnerMapValues({10001}, POWERSAVE_MATCH | IIF_MATCH, iif1);
+ expectUidOwnerMapValues({10002}, POWERSAVE_MATCH | IIF_MATCH, iif1);
+ expectUidOwnerMapValues({10003}, POWERSAVE_MATCH, 0);
+
+ // Remove all powersave rules, expect ownerMap to only have uid interface rules left
+ EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_powersave", true, {}));
+ expectUidOwnerMapValues({10001, 10002}, IIF_MATCH, iif1);
+ // Make sure these are the only uids left
+ checkEachUidValue({10001, 10002}, IIF_MATCH);
+}
+
+TEST_F(TrafficControllerTest, TestGrantInternetPermission) {
+ std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+ mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, appUids);
+ expectMapEmpty(mFakeUidPermissionMap);
+ expectPrivilegedUserSetEmpty();
+}
+
+TEST_F(TrafficControllerTest, TestRevokeInternetPermission) {
+ std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+ mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
+ expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
+}
+
+TEST_F(TrafficControllerTest, TestPermissionUninstalled) {
+ std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+ mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
+ expectUidPermissionMapValues(appUids, INetd::PERMISSION_UPDATE_DEVICE_STATS);
+ expectPrivilegedUserSet(appUids);
+
+ std::vector<uid_t> uidToRemove = {TEST_UID};
+ mTc.setPermissionForUids(INetd::PERMISSION_UNINSTALLED, uidToRemove);
+
+ std::vector<uid_t> uidRemain = {TEST_UID3, TEST_UID2};
+ expectUidPermissionMapValues(uidRemain, INetd::PERMISSION_UPDATE_DEVICE_STATS);
+ expectPrivilegedUserSet(uidRemain);
+
+ mTc.setPermissionForUids(INetd::PERMISSION_UNINSTALLED, uidRemain);
+ expectMapEmpty(mFakeUidPermissionMap);
+ expectPrivilegedUserSetEmpty();
+}
+
+TEST_F(TrafficControllerTest, TestGrantUpdateStatsPermission) {
+ std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+ mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
+ expectUidPermissionMapValues(appUids, INetd::PERMISSION_UPDATE_DEVICE_STATS);
+ expectPrivilegedUserSet(appUids);
+
+ mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
+ expectPrivilegedUserSetEmpty();
+ expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
+}
+
+TEST_F(TrafficControllerTest, TestRevokeUpdateStatsPermission) {
+ std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+ mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
+ expectPrivilegedUserSet(appUids);
+
+ std::vector<uid_t> uidToRemove = {TEST_UID};
+ mTc.setPermissionForUids(INetd::PERMISSION_NONE, uidToRemove);
+
+ std::vector<uid_t> uidRemain = {TEST_UID3, TEST_UID2};
+ expectPrivilegedUserSet(uidRemain);
+
+ mTc.setPermissionForUids(INetd::PERMISSION_NONE, uidRemain);
+ expectPrivilegedUserSetEmpty();
+}
+
+TEST_F(TrafficControllerTest, TestGrantWrongPermission) {
+ std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+ mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
+ expectPrivilegedUserSetEmpty();
+ expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
+}
+
+TEST_F(TrafficControllerTest, TestGrantDuplicatePermissionSlientlyFail) {
+ std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
+
+ mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, appUids);
+ expectMapEmpty(mFakeUidPermissionMap);
+
+ std::vector<uid_t> uidToAdd = {TEST_UID};
+ mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, uidToAdd);
+
+ expectPrivilegedUserSetEmpty();
+
+ mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
+ expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
+
+ mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
+ expectPrivilegedUserSet(appUids);
+
+ mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, uidToAdd);
+ expectPrivilegedUserSet(appUids);
+
+ mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
+ expectPrivilegedUserSetEmpty();
+}
+
+} // namespace net
+} // namespace android
diff --git a/service/native/include/Common.h b/service/native/include/Common.h
new file mode 100644
index 0000000..7c0b797
--- /dev/null
+++ b/service/native/include/Common.h
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+#pragma once
+// TODO: deduplicate with the constants in NetdConstants.h.
+#include <aidl/android/net/INetd.h>
+
+using aidl::android::net::INetd;
+
+enum FirewallRule { ALLOW = INetd::FIREWALL_RULE_ALLOW, DENY = INetd::FIREWALL_RULE_DENY };
+
+// ALLOWLIST means the firewall denies all by default, uids must be explicitly ALLOWed
+// DENYLIST means the firewall allows all by default, uids must be explicitly DENYed
+
+enum FirewallType { ALLOWLIST = INetd::FIREWALL_ALLOWLIST, DENYLIST = INetd::FIREWALL_DENYLIST };
+
+enum ChildChain {
+ NONE = INetd::FIREWALL_CHAIN_NONE,
+ DOZABLE = INetd::FIREWALL_CHAIN_DOZABLE,
+ STANDBY = INetd::FIREWALL_CHAIN_STANDBY,
+ POWERSAVE = INetd::FIREWALL_CHAIN_POWERSAVE,
+ RESTRICTED = INetd::FIREWALL_CHAIN_RESTRICTED,
+ INVALID_CHAIN
+};
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
new file mode 100644
index 0000000..c050871
--- /dev/null
+++ b/service/native/include/TrafficController.h
@@ -0,0 +1,203 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <set>
+#include <Common.h>
+
+#include "android-base/thread_annotations.h"
+#include "bpf/BpfMap.h"
+#include "bpf_shared.h"
+#include "netdutils/DumpWriter.h"
+#include "netdutils/NetlinkListener.h"
+#include "netdutils/StatusOr.h"
+#include "utils/String16.h"
+
+namespace android {
+namespace net {
+
+using netdutils::StatusOr;
+
+class TrafficController {
+ public:
+ /*
+ * Initialize the whole controller
+ */
+ netdutils::Status start();
+
+ int setCounterSet(int counterSetNum, uid_t uid, uid_t callingUid) EXCLUDES(mMutex);
+
+ /*
+ * When deleting a tag data, the qtaguid module will grab the spinlock of each
+ * related rb_tree one by one and delete the tag information, counterSet
+ * information, iface stats information and uid stats information one by one.
+ * The new eBPF implementation is done similiarly by removing the entry on
+ * each map one by one. And deleting processes are also protected by the
+ * spinlock of the map. So no additional lock is required.
+ */
+ int deleteTagData(uint32_t tag, uid_t uid, uid_t callingUid) EXCLUDES(mMutex);
+
+ /*
+ * Swap the stats map config from current active stats map to the idle one.
+ */
+ netdutils::Status swapActiveStatsMap() EXCLUDES(mMutex);
+
+ /*
+ * Add the interface name and index pair into the eBPF map.
+ */
+ int addInterface(const char* name, uint32_t ifaceIndex);
+
+ int changeUidOwnerRule(ChildChain chain, const uid_t uid, FirewallRule rule, FirewallType type);
+
+ int removeUidOwnerRule(const uid_t uid);
+
+ int replaceUidOwnerMap(const std::string& name, bool isAllowlist,
+ const std::vector<int32_t>& uids);
+
+ enum IptOp { IptOpInsert, IptOpDelete };
+
+ netdutils::Status updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
+ FirewallType type) EXCLUDES(mMutex);
+
+ void dump(netdutils::DumpWriter& dw, bool verbose) EXCLUDES(mMutex);
+
+ netdutils::Status replaceRulesInMap(UidOwnerMatchType match, const std::vector<int32_t>& uids)
+ EXCLUDES(mMutex);
+
+ netdutils::Status addUidInterfaceRules(const int ifIndex, const std::vector<int32_t>& uids)
+ EXCLUDES(mMutex);
+ netdutils::Status removeUidInterfaceRules(const std::vector<int32_t>& uids) EXCLUDES(mMutex);
+
+ netdutils::Status updateUidOwnerMap(const uint32_t uid,
+ UidOwnerMatchType matchType, IptOp op) EXCLUDES(mMutex);
+ static const String16 DUMP_KEYWORD;
+
+ int toggleUidOwnerMap(ChildChain chain, bool enable) EXCLUDES(mMutex);
+
+ static netdutils::StatusOr<std::unique_ptr<netdutils::NetlinkListenerInterface>>
+ makeSkDestroyListener();
+
+ void setPermissionForUids(int permission, const std::vector<uid_t>& uids) EXCLUDES(mMutex);
+
+ FirewallType getFirewallType(ChildChain);
+
+ static const char* LOCAL_DOZABLE;
+ static const char* LOCAL_STANDBY;
+ static const char* LOCAL_POWERSAVE;
+ static const char* LOCAL_RESTRICTED;
+
+ private:
+ /*
+ * mCookieTagMap: Store the corresponding tag and uid for a specific socket.
+ * DO NOT hold any locks when modifying this map, otherwise when the untag
+ * operation is waiting for a lock hold by other process and there are more
+ * sockets being closed than can fit in the socket buffer of the netlink socket
+ * that receives them, then the kernel will drop some of these sockets and we
+ * won't delete their tags.
+ * Map Key: uint64_t socket cookie
+ * Map Value: UidTagValue, contains a uint32 uid and a uint32 tag.
+ */
+ bpf::BpfMap<uint64_t, UidTagValue> mCookieTagMap GUARDED_BY(mMutex);
+
+ /*
+ * mUidCounterSetMap: Store the counterSet of a specific uid.
+ * Map Key: uint32 uid.
+ * Map Value: uint32 counterSet specifies if the traffic is a background
+ * or foreground traffic.
+ */
+ bpf::BpfMap<uint32_t, uint8_t> mUidCounterSetMap GUARDED_BY(mMutex);
+
+ /*
+ * mAppUidStatsMap: Store the total traffic stats for a uid regardless of
+ * tag, counterSet and iface. The stats is used by TrafficStats.getUidStats
+ * API to return persistent stats for a specific uid since device boot.
+ */
+ bpf::BpfMap<uint32_t, StatsValue> mAppUidStatsMap;
+
+ /*
+ * mStatsMapA/mStatsMapB: Store the traffic statistics for a specific
+ * combination of uid, tag, iface and counterSet. These two maps contain
+ * both tagged and untagged traffic.
+ * Map Key: StatsKey contains the uid, tag, counterSet and ifaceIndex
+ * information.
+ * Map Value: Stats, contains packet count and byte count of each
+ * transport protocol on egress and ingress direction.
+ */
+ bpf::BpfMap<StatsKey, StatsValue> mStatsMapA GUARDED_BY(mMutex);
+
+ bpf::BpfMap<StatsKey, StatsValue> mStatsMapB GUARDED_BY(mMutex);
+
+ /*
+ * mIfaceIndexNameMap: Store the index name pair of each interface show up
+ * on the device since boot. The interface index is used by the eBPF program
+ * to correctly match the iface name when receiving a packet.
+ */
+ bpf::BpfMap<uint32_t, IfaceValue> mIfaceIndexNameMap;
+
+ /*
+ * mIfaceStataMap: Store per iface traffic stats gathered from xt_bpf
+ * filter.
+ */
+ bpf::BpfMap<uint32_t, StatsValue> mIfaceStatsMap;
+
+ /*
+ * mConfigurationMap: Store the current network policy about uid filtering
+ * and the current stats map in use. There are two configuration entries in
+ * the map right now:
+ * - Entry with UID_RULES_CONFIGURATION_KEY:
+ * Store the configuration for the current uid rules. It indicates the device
+ * is in doze/powersave/standby/restricted mode.
+ * - Entry with CURRENT_STATS_MAP_CONFIGURATION_KEY:
+ * Stores the current live stats map that kernel program is writing to.
+ * Userspace can do scraping and cleaning job on the other one depending on the
+ * current configs.
+ */
+ bpf::BpfMap<uint32_t, uint8_t> mConfigurationMap GUARDED_BY(mMutex);
+
+ /*
+ * mUidOwnerMap: Store uids that are used for bandwidth control uid match.
+ */
+ bpf::BpfMap<uint32_t, UidOwnerValue> mUidOwnerMap GUARDED_BY(mMutex);
+
+ /*
+ * mUidOwnerMap: Store uids that are used for INTERNET permission check.
+ */
+ bpf::BpfMap<uint32_t, uint8_t> mUidPermissionMap GUARDED_BY(mMutex);
+
+ std::unique_ptr<netdutils::NetlinkListenerInterface> mSkDestroyListener;
+
+ netdutils::Status removeRule(uint32_t uid, UidOwnerMatchType match) REQUIRES(mMutex);
+
+ netdutils::Status addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif = 0)
+ REQUIRES(mMutex);
+
+ std::mutex mMutex;
+
+ netdutils::Status initMaps() EXCLUDES(mMutex);
+
+ // Keep track of uids that have permission UPDATE_DEVICE_STATS so we don't
+ // need to call back to system server for permission check.
+ std::set<uid_t> mPrivilegedUser GUARDED_BY(mMutex);
+
+ bool hasUpdateDeviceStatsPermission(uid_t uid) REQUIRES(mMutex);
+
+ // For testing
+ friend class TrafficControllerTest;
+};
+
+} // namespace net
+} // namespace android
diff --git a/service/native/jni/com_android_server_BpfNetMaps.cpp b/service/native/jni/com_android_server_BpfNetMaps.cpp
new file mode 100644
index 0000000..7ab4d46
--- /dev/null
+++ b/service/native/jni/com_android_server_BpfNetMaps.cpp
@@ -0,0 +1,266 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "TrafficControllerJni"
+
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedUtfChars.h>
+#include <nativehelper/ScopedPrimitiveArray.h>
+#include <net/if.h>
+#include <vector>
+
+#include "TrafficController.h"
+#include "android-base/logging.h"
+#include "bpf_shared.h"
+#include "utils/Log.h"
+
+using android::net::TrafficController;
+using android::netdutils::Status;
+
+using UidOwnerMatchType::PENALTY_BOX_MATCH;
+using UidOwnerMatchType::HAPPY_BOX_MATCH;
+
+static android::net::TrafficController mTc;
+
+namespace android {
+
+static void native_init(JNIEnv* env, jobject clazz) {
+ Status status = mTc.start();
+ if (!isOk(status)) {
+ ALOGE("%s failed", __func__);
+ }
+}
+
+static jint native_addNaughtyApp(JNIEnv* env, jobject clazz, jint uid) {
+ const uint32_t appUids = static_cast<uint32_t>(abs(uid));
+ Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
+ TrafficController::IptOp::IptOpInsert);
+ if (!isOk(status)) {
+ ALOGE("%s failed, errer code = %d", __func__, status.code());
+ }
+ return (jint)status.code();
+}
+
+static jint native_removeNaughtyApp(JNIEnv* env, jobject clazz, jint uid) {
+ const uint32_t appUids = static_cast<uint32_t>(abs(uid));
+ Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
+ TrafficController::IptOp::IptOpDelete);
+ if (!isOk(status)) {
+ ALOGE("%s failed, errer code = %d", __func__, status.code());
+ }
+ return (jint)status.code();
+}
+
+static jint native_addNiceApp(JNIEnv* env, jobject clazz, jint uid) {
+ const uint32_t appUids = static_cast<uint32_t>(abs(uid));
+ Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
+ TrafficController::IptOp::IptOpInsert);
+ if (!isOk(status)) {
+ ALOGE("%s failed, errer code = %d", __func__, status.code());
+ }
+ return (jint)status.code();
+}
+
+static jint native_removeNiceApp(JNIEnv* env, jobject clazz, jint uid) {
+ const uint32_t appUids = static_cast<uint32_t>(abs(uid));
+ Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
+ TrafficController::IptOp::IptOpDelete);
+ if (!isOk(status)) {
+ ALOGD("%s failed, errer code = %d", __func__, status.code());
+ }
+ return (jint)status.code();
+}
+
+static jint native_setChildChain(JNIEnv* env, jobject clazz, jint childChain, jboolean enable) {
+ auto chain = static_cast<ChildChain>(childChain);
+ int res = mTc.toggleUidOwnerMap(chain, enable);
+ if (res) {
+ ALOGE("%s failed, error code = %d", __func__, res);
+ }
+ return (jint)res;
+}
+
+static jint native_replaceUidChain(JNIEnv* env, jobject clazz, jstring name, jboolean isAllowlist,
+ jintArray jUids) {
+ const ScopedUtfChars chainNameUtf8(env, name);
+ if (chainNameUtf8.c_str() == nullptr) {
+ return -EINVAL;
+ }
+ const std::string chainName(chainNameUtf8.c_str());
+
+ ScopedIntArrayRO uids(env, jUids);
+ if (uids.get() == nullptr) {
+ return -EINVAL;
+ }
+
+ size_t size = uids.size();
+ std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
+ int res = mTc.replaceUidOwnerMap(chainName, isAllowlist, data);
+ if (res) {
+ ALOGE("%s failed, error code = %d", __func__, res);
+ }
+ return (jint)res;
+}
+
+static FirewallType getFirewallType(ChildChain chain) {
+ switch (chain) {
+ case DOZABLE:
+ return ALLOWLIST;
+ case STANDBY:
+ return DENYLIST;
+ case POWERSAVE:
+ return ALLOWLIST;
+ case RESTRICTED:
+ return ALLOWLIST;
+ case NONE:
+ default:
+ return DENYLIST;
+ }
+}
+
+static jint native_setUidRule(JNIEnv* env, jobject clazz, jint childChain, jint uid,
+ jint firewallRule) {
+ auto chain = static_cast<ChildChain>(childChain);
+ auto rule = static_cast<FirewallRule>(firewallRule);
+ FirewallType fType = getFirewallType(chain);
+
+ int res = mTc.changeUidOwnerRule(chain, uid, rule, fType);
+ if (res) {
+ ALOGE("%s failed, error code = %d", __func__, res);
+ }
+ return (jint)res;
+}
+
+static jint native_addUidInterfaceRules(JNIEnv* env, jobject clazz, jstring ifName,
+ jintArray jUids) {
+ const ScopedUtfChars ifNameUtf8(env, ifName);
+ if (ifNameUtf8.c_str() == nullptr) {
+ return -EINVAL;
+ }
+ const std::string interfaceName(ifNameUtf8.c_str());
+ const int ifIndex = if_nametoindex(interfaceName.c_str());
+
+ ScopedIntArrayRO uids(env, jUids);
+ if (uids.get() == nullptr) {
+ return -EINVAL;
+ }
+
+ size_t size = uids.size();
+ std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
+ Status status = mTc.addUidInterfaceRules(ifIndex, data);
+ if (!isOk(status)) {
+ ALOGE("%s failed, error code = %d", __func__, status.code());
+ }
+ return (jint)status.code();
+}
+
+static jint native_removeUidInterfaceRules(JNIEnv* env, jobject clazz, jintArray jUids) {
+ ScopedIntArrayRO uids(env, jUids);
+ if (uids.get() == nullptr) {
+ return -EINVAL;
+ }
+
+ size_t size = uids.size();
+ std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
+ Status status = mTc.removeUidInterfaceRules(data);
+ if (!isOk(status)) {
+ ALOGE("%s failed, error code = %d", __func__, status.code());
+ }
+ return (jint)status.code();
+}
+
+static jint native_swapActiveStatsMap(JNIEnv* env, jobject clazz) {
+ Status status = mTc.swapActiveStatsMap();
+ if (!isOk(status)) {
+ ALOGD("%s failed, error code = %d", __func__, status.code());
+ }
+ return (jint)status.code();
+}
+
+static void native_setPermissionForUids(JNIEnv* env, jobject clazz, jint permission,
+ jintArray jUids) {
+ ScopedIntArrayRO uids(env, jUids);
+ if (uids.get() == nullptr) return;
+
+ size_t size = uids.size();
+ static_assert(sizeof(*(uids.get())) == sizeof(uid_t));
+ std::vector<uid_t> data ((uid_t *)&uids[0], (uid_t*)&uids[size]);
+ mTc.setPermissionForUids(permission, data);
+}
+
+static jint native_setCounterSet(JNIEnv* env, jobject clazz, jint setNum, jint uid) {
+ uid_t callingUid = getuid();
+ int res = mTc.setCounterSet(setNum, (uid_t)uid, callingUid);
+ if (res) {
+ ALOGE("%s failed, error code = %d", __func__, res);
+ }
+ return (jint)res;
+}
+
+static jint native_deleteTagData(JNIEnv* env, jobject clazz, jint tagNum, jint uid) {
+ uid_t callingUid = getuid();
+ int res = mTc.deleteTagData(tagNum, (uid_t)uid, callingUid);
+ if (res) {
+ ALOGE("%s failed, error code = %d", __func__, res);
+ }
+ return (jint)res;
+}
+
+/*
+ * JNI registration.
+ */
+// clang-format off
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"native_init", "()V",
+ (void*)native_init},
+ {"native_addNaughtyApp", "(I)I",
+ (void*)native_addNaughtyApp},
+ {"native_removeNaughtyApp", "(I)I",
+ (void*)native_removeNaughtyApp},
+ {"native_addNiceApp", "(I)I",
+ (void*)native_addNiceApp},
+ {"native_removeNiceApp", "(I)I",
+ (void*)native_removeNiceApp},
+ {"native_setChildChain", "(IZ)I",
+ (void*)native_setChildChain},
+ {"native_replaceUidChain", "(Ljava/lang/String;Z[I)I",
+ (void*)native_replaceUidChain},
+ {"native_setUidRule", "(III)I",
+ (void*)native_setUidRule},
+ {"native_addUidInterfaceRules", "(Ljava/lang/String;[I)I",
+ (void*)native_addUidInterfaceRules},
+ {"native_removeUidInterfaceRules", "([I)I",
+ (void*)native_removeUidInterfaceRules},
+ {"native_swapActiveStatsMap", "()I",
+ (void*)native_swapActiveStatsMap},
+ {"native_setPermissionForUids", "(I[I)V",
+ (void*)native_setPermissionForUids},
+ {"native_setCounterSet", "(II)I",
+ (void*)native_setCounterSet},
+ {"native_deleteTagData", "(II)I",
+ (void*)native_deleteTagData},
+};
+// clang-format on
+
+int register_com_android_server_BpfNetMaps(JNIEnv* env) {
+ return jniRegisterNativeMethods(env,
+ "com/android/server/BpfNetMaps",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/service/native/jni/onload.cpp b/service/native/jni/onload.cpp
new file mode 100644
index 0000000..df7c77b
--- /dev/null
+++ b/service/native/jni/onload.cpp
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "TrafficControllerJni"
+
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+
+#include "utils/Log.h"
+
+namespace android {
+
+int register_com_android_server_BpfNetMaps(JNIEnv* env);
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
+ JNIEnv *env;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ ALOGE("%s: ERROR: GetEnv failed", __func__);
+ return JNI_ERR;
+ }
+
+ if (register_com_android_server_BpfNetMaps(env) < 0)
+ return JNI_ERR;
+
+ return JNI_VERSION_1_6;
+}
+
+}; // namespace android
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
new file mode 100644
index 0000000..5e208d8
--- /dev/null
+++ b/service/native/libs/libclat/Android.bp
@@ -0,0 +1,70 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+ name: "libclat",
+ defaults: ["netd_defaults"],
+ header_libs: [
+ "bpf_connectivity_headers",
+ "bpf_headers",
+ "bpf_syscall_wrappers",
+ ],
+ srcs: [
+ "TcUtils.cpp", // TODO: move to frameworks/libs/net
+ "bpfhelper.cpp",
+ "clatutils.cpp",
+ ],
+ stl: "libc++_static",
+ static_libs: [
+ "libbase",
+ "libip_checksum",
+ "libnetdutils", // for netdutils/UidConstants.h in bpf_shared.h
+ ],
+ shared_libs: ["liblog"],
+ export_include_dirs: ["include"],
+ min_sdk_version: "30",
+ apex_available: ["com.android.tethering"],
+}
+
+cc_test {
+ name: "libclat_test",
+ defaults: ["netd_defaults"],
+ test_suites: ["device-tests"],
+ header_libs: [
+ "bpf_connectivity_headers",
+ "bpf_headers",
+ "bpf_syscall_wrappers",
+ ],
+ srcs: [
+ "TcUtilsTest.cpp",
+ "clatutils_test.cpp",
+ ],
+ static_libs: [
+ "libbase",
+ "libclat",
+ "libip_checksum",
+ "libnetd_test_tun_interface",
+ "libnetdutils", // for netdutils/UidConstants.h in bpf_shared.h
+ "libtcutils",
+ ],
+ shared_libs: [
+ "liblog",
+ "libnetutils",
+ ],
+ require_root: true,
+}
diff --git a/service/native/libs/libclat/TcUtils.cpp b/service/native/libs/libclat/TcUtils.cpp
new file mode 100644
index 0000000..cdfb763
--- /dev/null
+++ b/service/native/libs/libclat/TcUtils.cpp
@@ -0,0 +1,390 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#define LOG_TAG "TcUtils"
+
+#include "libclat/TcUtils.h"
+
+#include <arpa/inet.h>
+#include <linux/if.h>
+#include <linux/if_arp.h>
+#include <linux/netlink.h>
+#include <linux/pkt_cls.h>
+#include <linux/pkt_sched.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <log/log.h>
+
+#include "android-base/unique_fd.h"
+
+namespace android {
+namespace net {
+
+using std::max;
+
+// Sync from system/netd/server/NetlinkCommands.h
+const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
+const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
+
+static int doSIOCGIF(const std::string& interface, int opt) {
+ base::unique_fd ufd(socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0));
+
+ if (ufd < 0) {
+ const int err = errno;
+ ALOGE("socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0)");
+ return -err;
+ };
+
+ struct ifreq ifr = {};
+ // We use strncpy() instead of strlcpy() since kernel has to be able
+ // to handle non-zero terminated junk passed in by userspace anyway,
+ // and this way too long interface names (more than IFNAMSIZ-1 = 15
+ // characters plus terminating NULL) will not get truncated to 15
+ // characters and zero-terminated and thus potentially erroneously
+ // match a truncated interface if one were to exist.
+ strncpy(ifr.ifr_name, interface.c_str(), sizeof(ifr.ifr_name));
+
+ if (ioctl(ufd, opt, &ifr, sizeof(ifr))) return -errno;
+
+ if (opt == SIOCGIFHWADDR) return ifr.ifr_hwaddr.sa_family;
+ if (opt == SIOCGIFMTU) return ifr.ifr_mtu;
+ return -EINVAL;
+}
+
+int hardwareAddressType(const std::string& interface) {
+ return doSIOCGIF(interface, SIOCGIFHWADDR);
+}
+
+int deviceMTU(const std::string& interface) {
+ return doSIOCGIF(interface, SIOCGIFMTU);
+}
+
+base::Result<bool> isEthernet(const std::string& interface) {
+ int rv = hardwareAddressType(interface);
+ if (rv < 0) {
+ errno = -rv;
+ return ErrnoErrorf("Get hardware address type of interface {} failed", interface);
+ }
+
+ switch (rv) {
+ case ARPHRD_ETHER:
+ return true;
+ case ARPHRD_NONE:
+ case ARPHRD_RAWIP: // in Linux 4.14+ rmnet support was upstreamed and this is 519
+ case 530: // this is ARPHRD_RAWIP on some Android 4.9 kernels with rmnet
+ return false;
+ default:
+ errno = EAFNOSUPPORT; // Address family not supported
+ return ErrnoErrorf("Unknown hardware address type {} on interface {}", rv, interface);
+ }
+}
+
+// TODO: use //system/netd/server/NetlinkCommands.cpp:openNetlinkSocket(protocol)
+// and //system/netd/server/SockDiag.cpp:checkError(fd)
+static int sendAndProcessNetlinkResponse(const void* req, int len) {
+ base::unique_fd fd(socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE));
+ if (fd == -1) {
+ const int err = errno;
+ ALOGE("socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE)");
+ return -err;
+ }
+
+ static constexpr int on = 1;
+ int rv = setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on));
+ if (rv) ALOGE("setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, %d)", on);
+
+ // this is needed to get sane strace netlink parsing, it allocates the pid
+ rv = bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR));
+ if (rv) {
+ const int err = errno;
+ ALOGE("bind(fd, {AF_NETLINK, 0, 0})");
+ return -err;
+ }
+
+ // we do not want to receive messages from anyone besides the kernel
+ rv = connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR));
+ if (rv) {
+ const int err = errno;
+ ALOGE("connect(fd, {AF_NETLINK, 0, 0})");
+ return -err;
+ }
+
+ rv = send(fd, req, len, 0);
+ if (rv == -1) return -errno;
+ if (rv != len) return -EMSGSIZE;
+
+ struct {
+ nlmsghdr h;
+ nlmsgerr e;
+ char buf[256];
+ } resp = {};
+
+ rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC);
+
+ if (rv == -1) {
+ const int err = errno;
+ ALOGE("recv() failed");
+ return -err;
+ }
+
+ if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) {
+ ALOGE("recv() returned short packet: %d", rv);
+ return -EMSGSIZE;
+ }
+
+ if (resp.h.nlmsg_len != (unsigned)rv) {
+ ALOGE("recv() returned invalid header length: %d != %d", resp.h.nlmsg_len, rv);
+ return -EBADMSG;
+ }
+
+ if (resp.h.nlmsg_type != NLMSG_ERROR) {
+ ALOGE("recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type);
+ return -EBADMSG;
+ }
+
+ return resp.e.error; // returns 0 on success
+}
+
+// ADD: nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_EXCL|NLM_F_CREATE
+// REPLACE: nlMsgType=RTM_NEWQDISC nlMsgFlags=NLM_F_CREATE|NLM_F_REPLACE
+// DEL: nlMsgType=RTM_DELQDISC nlMsgFlags=0
+int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags) {
+ // This is the name of the qdisc we are attaching.
+ // Some hoop jumping to make this compile time constant with known size,
+ // so that the structure declaration is well defined at compile time.
+#define CLSACT "clsact"
+ // sizeof() includes the terminating NULL
+ static constexpr size_t ASCIIZ_LEN_CLSACT = sizeof(CLSACT);
+
+ const struct {
+ nlmsghdr n;
+ tcmsg t;
+ struct {
+ nlattr attr;
+ char str[NLMSG_ALIGN(ASCIIZ_LEN_CLSACT)];
+ } kind;
+ } req = {
+ .n =
+ {
+ .nlmsg_len = sizeof(req),
+ .nlmsg_type = nlMsgType,
+ .nlmsg_flags = static_cast<__u16>(NETLINK_REQUEST_FLAGS | nlMsgFlags),
+ },
+ .t =
+ {
+ .tcm_family = AF_UNSPEC,
+ .tcm_ifindex = ifIndex,
+ .tcm_handle = TC_H_MAKE(TC_H_CLSACT, 0),
+ .tcm_parent = TC_H_CLSACT,
+ },
+ .kind =
+ {
+ .attr =
+ {
+ .nla_len = NLA_HDRLEN + ASCIIZ_LEN_CLSACT,
+ .nla_type = TCA_KIND,
+ },
+ .str = CLSACT,
+ },
+ };
+#undef CLSACT
+
+ return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+// tc filter add dev .. in/egress prio 4 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
+// direct-action
+int tcFilterAddDevBpf(int ifIndex, bool ingress, uint16_t proto, int bpfFd, bool ethernet) {
+ // This is the name of the filter we're attaching (ie. this is the 'bpf'
+ // packet classifier enabled by kernel config option CONFIG_NET_CLS_BPF.
+ //
+ // We go through some hoops in order to make this compile time constants
+ // so that we can define the struct further down the function with the
+ // field for this sized correctly already during the build.
+#define BPF "bpf"
+ // sizeof() includes the terminating NULL
+ static constexpr size_t ASCIIZ_LEN_BPF = sizeof(BPF);
+
+ // This is to replicate program name suffix used by 'tc' Linux cli
+ // when it attaches programs.
+#define FSOBJ_SUFFIX ":[*fsobj]"
+
+ // This macro expands (from header files) to:
+ // prog_clatd_schedcls_ingress6_clat_rawip:[*fsobj]
+ // and is the name of the pinned ingress ebpf program for ARPHRD_RAWIP interfaces.
+ // (also compatible with anything that has 0 size L2 header)
+ static constexpr char name_clat_rx_rawip[] = CLAT_INGRESS6_PROG_RAWIP_NAME FSOBJ_SUFFIX;
+
+ // This macro expands (from header files) to:
+ // prog_clatd_schedcls_ingress6_clat_ether:[*fsobj]
+ // and is the name of the pinned ingress ebpf program for ARPHRD_ETHER interfaces.
+ // (also compatible with anything that has standard ethernet header)
+ static constexpr char name_clat_rx_ether[] = CLAT_INGRESS6_PROG_ETHER_NAME FSOBJ_SUFFIX;
+
+ // This macro expands (from header files) to:
+ // prog_clatd_schedcls_egress4_clat_rawip:[*fsobj]
+ // and is the name of the pinned egress ebpf program for ARPHRD_RAWIP interfaces.
+ // (also compatible with anything that has 0 size L2 header)
+ static constexpr char name_clat_tx_rawip[] = CLAT_EGRESS4_PROG_RAWIP_NAME FSOBJ_SUFFIX;
+
+ // This macro expands (from header files) to:
+ // prog_clatd_schedcls_egress4_clat_ether:[*fsobj]
+ // and is the name of the pinned egress ebpf program for ARPHRD_ETHER interfaces.
+ // (also compatible with anything that has standard ethernet header)
+ static constexpr char name_clat_tx_ether[] = CLAT_EGRESS4_PROG_ETHER_NAME FSOBJ_SUFFIX;
+
+#undef FSOBJ_SUFFIX
+
+ // The actual name we'll use is determined at run time via 'ethernet' and 'ingress'
+ // booleans. We need to compile time allocate enough space in the struct
+ // hence this macro magic to make sure we have enough space for either
+ // possibility. In practice some of these are actually the same size.
+ static constexpr size_t ASCIIZ_MAXLEN_NAME = max({
+ sizeof(name_clat_rx_rawip),
+ sizeof(name_clat_rx_ether),
+ sizeof(name_clat_tx_rawip),
+ sizeof(name_clat_tx_ether),
+ });
+
+ // These are not compile time constants: 'name' is used in strncpy below
+ const char* const name_clat_rx = ethernet ? name_clat_rx_ether : name_clat_rx_rawip;
+ const char* const name_clat_tx = ethernet ? name_clat_tx_ether : name_clat_tx_rawip;
+ const char* const name = ingress ? name_clat_rx : name_clat_tx;
+
+ struct {
+ nlmsghdr n;
+ tcmsg t;
+ struct {
+ nlattr attr;
+ char str[NLMSG_ALIGN(ASCIIZ_LEN_BPF)];
+ } kind;
+ struct {
+ nlattr attr;
+ struct {
+ nlattr attr;
+ __u32 u32;
+ } fd;
+ struct {
+ nlattr attr;
+ char str[NLMSG_ALIGN(ASCIIZ_MAXLEN_NAME)];
+ } name;
+ struct {
+ nlattr attr;
+ __u32 u32;
+ } flags;
+ } options;
+ } req = {
+ .n =
+ {
+ .nlmsg_len = sizeof(req),
+ .nlmsg_type = RTM_NEWTFILTER,
+ .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE,
+ },
+ .t =
+ {
+ .tcm_family = AF_UNSPEC,
+ .tcm_ifindex = ifIndex,
+ .tcm_handle = TC_H_UNSPEC,
+ .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
+ ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
+ .tcm_info = static_cast<__u32>((PRIO_CLAT << 16) | htons(proto)),
+ },
+ .kind =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.kind),
+ .nla_type = TCA_KIND,
+ },
+ .str = BPF,
+ },
+ .options =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.options),
+ .nla_type = NLA_F_NESTED | TCA_OPTIONS,
+ },
+ .fd =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.options.fd),
+ .nla_type = TCA_BPF_FD,
+ },
+ .u32 = static_cast<__u32>(bpfFd),
+ },
+ .name =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.options.name),
+ .nla_type = TCA_BPF_NAME,
+ },
+ // Visible via 'tc filter show', but
+ // is overwritten by strncpy below
+ .str = "placeholder",
+ },
+ .flags =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.options.flags),
+ .nla_type = TCA_BPF_FLAGS,
+ },
+ .u32 = TCA_BPF_FLAG_ACT_DIRECT,
+ },
+ },
+ };
+#undef BPF
+
+ strncpy(req.options.name.str, name, sizeof(req.options.name.str));
+
+ return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+// tc filter del dev .. in/egress prio 4 protocol ..
+int tcFilterDelDev(int ifIndex, bool ingress, uint16_t prio, uint16_t proto) {
+ const struct {
+ nlmsghdr n;
+ tcmsg t;
+ } req = {
+ .n =
+ {
+ .nlmsg_len = sizeof(req),
+ .nlmsg_type = RTM_DELTFILTER,
+ .nlmsg_flags = NETLINK_REQUEST_FLAGS,
+ },
+ .t =
+ {
+ .tcm_family = AF_UNSPEC,
+ .tcm_ifindex = ifIndex,
+ .tcm_handle = TC_H_UNSPEC,
+ .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
+ ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
+ .tcm_info = (static_cast<uint32_t>(prio) << 16) |
+ static_cast<uint32_t>(htons(proto)),
+ },
+ };
+
+ return sendAndProcessNetlinkResponse(&req, sizeof(req));
+}
+
+} // namespace net
+} // namespace android
diff --git a/service/native/libs/libclat/TcUtilsTest.cpp b/service/native/libs/libclat/TcUtilsTest.cpp
new file mode 100644
index 0000000..08f3042
--- /dev/null
+++ b/service/native/libs/libclat/TcUtilsTest.cpp
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2019 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.
+ *
+ * TcUtilsTest.cpp - unit tests for TcUtils.cpp
+ */
+
+#include <gtest/gtest.h>
+
+#include "libclat/TcUtils.h"
+
+#include <linux/if_arp.h>
+#include <stdlib.h>
+#include <sys/wait.h>
+
+#include "bpf/BpfUtils.h"
+#include "bpf_shared.h"
+
+namespace android {
+namespace net {
+
+class TcUtilsTest : public ::testing::Test {
+ public:
+ void SetUp() {}
+};
+
+TEST_F(TcUtilsTest, HardwareAddressTypeOfNonExistingIf) {
+ ASSERT_EQ(-ENODEV, hardwareAddressType("not_existing_if"));
+}
+
+TEST_F(TcUtilsTest, HardwareAddressTypeOfLoopback) {
+ ASSERT_EQ(ARPHRD_LOOPBACK, hardwareAddressType("lo"));
+}
+
+// If wireless 'wlan0' interface exists it should be Ethernet.
+TEST_F(TcUtilsTest, HardwareAddressTypeOfWireless) {
+ int type = hardwareAddressType("wlan0");
+ if (type == -ENODEV) return;
+
+ ASSERT_EQ(ARPHRD_ETHER, type);
+}
+
+// If cellular 'rmnet_data0' interface exists it should
+// *probably* not be Ethernet and instead be RawIp.
+TEST_F(TcUtilsTest, HardwareAddressTypeOfCellular) {
+ int type = hardwareAddressType("rmnet_data0");
+ if (type == -ENODEV) return;
+
+ ASSERT_NE(ARPHRD_ETHER, type);
+
+ // ARPHRD_RAWIP is 530 on some pre-4.14 Qualcomm devices.
+ if (type == 530) return;
+
+ ASSERT_EQ(ARPHRD_RAWIP, type);
+}
+
+TEST_F(TcUtilsTest, IsEthernetOfNonExistingIf) {
+ auto res = isEthernet("not_existing_if");
+ ASSERT_FALSE(res.ok());
+ ASSERT_EQ(ENODEV, res.error().code());
+}
+
+TEST_F(TcUtilsTest, IsEthernetOfLoopback) {
+ auto res = isEthernet("lo");
+ ASSERT_FALSE(res.ok());
+ ASSERT_EQ(EAFNOSUPPORT, res.error().code());
+}
+
+// If wireless 'wlan0' interface exists it should be Ethernet.
+// See also HardwareAddressTypeOfWireless.
+TEST_F(TcUtilsTest, IsEthernetOfWireless) {
+ auto res = isEthernet("wlan0");
+ if (!res.ok() && res.error().code() == ENODEV) return;
+
+ ASSERT_RESULT_OK(res);
+ ASSERT_TRUE(res.value());
+}
+
+// If cellular 'rmnet_data0' interface exists it should
+// *probably* not be Ethernet and instead be RawIp.
+// See also HardwareAddressTypeOfCellular.
+TEST_F(TcUtilsTest, IsEthernetOfCellular) {
+ auto res = isEthernet("rmnet_data0");
+ if (!res.ok() && res.error().code() == ENODEV) return;
+
+ ASSERT_RESULT_OK(res);
+ ASSERT_FALSE(res.value());
+}
+
+TEST_F(TcUtilsTest, DeviceMTUOfNonExistingIf) {
+ ASSERT_EQ(-ENODEV, deviceMTU("not_existing_if"));
+}
+
+TEST_F(TcUtilsTest, DeviceMTUofLoopback) {
+ ASSERT_EQ(65536, deviceMTU("lo"));
+}
+
+TEST_F(TcUtilsTest, GetClatEgress4MapFd) {
+ int fd = getClatEgress4MapFd();
+ ASSERT_GE(fd, 3); // 0,1,2 - stdin/out/err, thus fd >= 3
+ EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+ close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatEgress4RawIpProgFd) {
+ int fd = getClatEgress4ProgFd(RAWIP);
+ ASSERT_GE(fd, 3);
+ EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+ close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatEgress4EtherProgFd) {
+ int fd = getClatEgress4ProgFd(ETHER);
+ ASSERT_GE(fd, 3);
+ EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+ close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatIngress6MapFd) {
+ int fd = getClatIngress6MapFd();
+ ASSERT_GE(fd, 3); // 0,1,2 - stdin/out/err, thus fd >= 3
+ EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+ close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatIngress6RawIpProgFd) {
+ int fd = getClatIngress6ProgFd(RAWIP);
+ ASSERT_GE(fd, 3);
+ EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+ close(fd);
+}
+
+TEST_F(TcUtilsTest, GetClatIngress6EtherProgFd) {
+ int fd = getClatIngress6ProgFd(ETHER);
+ ASSERT_GE(fd, 3);
+ EXPECT_EQ(FD_CLOEXEC, fcntl(fd, F_GETFD));
+ close(fd);
+}
+
+// See Linux kernel source in include/net/flow.h
+#define LOOPBACK_IFINDEX 1
+
+TEST_F(TcUtilsTest, AttachReplaceDetachClsactLo) {
+ // This attaches and detaches a configuration-less and thus no-op clsact
+ // qdisc to loopback interface (and it takes fractions of a second)
+ EXPECT_EQ(0, tcQdiscAddDevClsact(LOOPBACK_IFINDEX));
+ EXPECT_EQ(0, tcQdiscReplaceDevClsact(LOOPBACK_IFINDEX));
+ EXPECT_EQ(0, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
+ EXPECT_EQ(-EINVAL, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
+}
+
+static void checkAttachDetachBpfFilterClsactLo(const bool ingress, const bool ethernet) {
+ // Older kernels return EINVAL instead of ENOENT due to lacking proper error propagation...
+ const int errNOENT = android::bpf::isAtLeastKernelVersion(4, 19, 0) ? ENOENT : EINVAL;
+
+ int clatBpfFd = ingress ? getClatIngress6ProgFd(ethernet) : getClatEgress4ProgFd(ethernet);
+ ASSERT_GE(clatBpfFd, 3);
+
+ // This attaches and detaches a clsact plus ebpf program to loopback
+ // interface, but it should not affect traffic by virtue of us not
+ // actually populating the ebpf control map.
+ // Furthermore: it only takes fractions of a second.
+ EXPECT_EQ(-EINVAL, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+ EXPECT_EQ(-EINVAL, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+ EXPECT_EQ(0, tcQdiscAddDevClsact(LOOPBACK_IFINDEX));
+ EXPECT_EQ(-errNOENT, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+ EXPECT_EQ(-errNOENT, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+ if (ingress) {
+ EXPECT_EQ(0, tcFilterAddDevIngressClatIpv6(LOOPBACK_IFINDEX, clatBpfFd, ethernet));
+ EXPECT_EQ(0, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+ } else {
+ EXPECT_EQ(0, tcFilterAddDevEgressClatIpv4(LOOPBACK_IFINDEX, clatBpfFd, ethernet));
+ EXPECT_EQ(0, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+ }
+ EXPECT_EQ(-errNOENT, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+ EXPECT_EQ(-errNOENT, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+ EXPECT_EQ(0, tcQdiscDelDevClsact(LOOPBACK_IFINDEX));
+ EXPECT_EQ(-EINVAL, tcFilterDelDevIngressClatIpv6(LOOPBACK_IFINDEX));
+ EXPECT_EQ(-EINVAL, tcFilterDelDevEgressClatIpv4(LOOPBACK_IFINDEX));
+
+ close(clatBpfFd);
+}
+
+TEST_F(TcUtilsTest, CheckAttachBpfFilterRawIpClsactEgressLo) {
+ checkAttachDetachBpfFilterClsactLo(EGRESS, RAWIP);
+}
+
+TEST_F(TcUtilsTest, CheckAttachBpfFilterEthernetClsactEgressLo) {
+ checkAttachDetachBpfFilterClsactLo(EGRESS, ETHER);
+}
+
+TEST_F(TcUtilsTest, CheckAttachBpfFilterRawIpClsactIngressLo) {
+ checkAttachDetachBpfFilterClsactLo(INGRESS, RAWIP);
+}
+
+TEST_F(TcUtilsTest, CheckAttachBpfFilterEthernetClsactIngressLo) {
+ checkAttachDetachBpfFilterClsactLo(INGRESS, ETHER);
+}
+
+} // namespace net
+} // namespace android
diff --git a/service/native/libs/libclat/bpfhelper.cpp b/service/native/libs/libclat/bpfhelper.cpp
new file mode 100644
index 0000000..6e230d0
--- /dev/null
+++ b/service/native/libs/libclat/bpfhelper.cpp
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2021 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.
+ *
+ * main.c - main function
+ */
+#define LOG_TAG "bpfhelper"
+
+#include "libclat/bpfhelper.h"
+
+#include <android-base/unique_fd.h>
+#include <log/log.h>
+
+#include "bpf/BpfMap.h"
+#include "libclat/TcUtils.h"
+
+#define DEVICEPREFIX "v4-"
+
+using android::base::unique_fd;
+using android::net::RAWIP;
+using android::net::getClatEgress4MapFd;
+using android::net::getClatIngress6MapFd;
+using android::net::getClatEgress4ProgFd;
+using android::net::getClatIngress6ProgFd;
+using android::net::tcQdiscAddDevClsact;
+using android::net::tcFilterAddDevEgressClatIpv4;
+using android::net::tcFilterAddDevIngressClatIpv6;
+using android::net::tcFilterDelDevEgressClatIpv4;
+using android::net::tcFilterDelDevIngressClatIpv6;
+using android::bpf::BpfMap;
+
+BpfMap<ClatEgress4Key, ClatEgress4Value> mClatEgress4Map;
+BpfMap<ClatIngress6Key, ClatIngress6Value> mClatIngress6Map;
+
+namespace android {
+namespace net {
+namespace clat {
+
+// TODO: have a clearMap function to remove all stubs while system server crash.
+// For long term, move bpf access into java and map initialization should live
+// ClatCoordinator constructor.
+int initMaps(void) {
+ int rv = getClatEgress4MapFd();
+ if (rv < 0) {
+ ALOGE("getClatEgress4MapFd() failure: %s", strerror(-rv));
+ return -rv;
+ }
+ mClatEgress4Map.reset(rv);
+
+ rv = getClatIngress6MapFd();
+ if (rv < 0) {
+ ALOGE("getClatIngress6MapFd() failure: %s", strerror(-rv));
+ mClatEgress4Map.reset(-1);
+ return -rv;
+ }
+ mClatIngress6Map.reset(rv);
+
+ return 0;
+}
+
+void maybeStartBpf(const ClatdTracker& tracker) {
+ auto isEthernet = android::net::isEthernet(tracker.iface);
+ if (!isEthernet.ok()) {
+ ALOGE("isEthernet(%s[%d]) failure: %s", tracker.iface, tracker.ifIndex,
+ isEthernet.error().message().c_str());
+ return;
+ }
+
+ // This program will be attached to the v4-* interface which is a TUN and thus always rawip.
+ int rv = getClatEgress4ProgFd(RAWIP);
+ if (rv < 0) {
+ ALOGE("getClatEgress4ProgFd(RAWIP) failure: %s", strerror(-rv));
+ return;
+ }
+ unique_fd txRawIpProgFd(rv);
+
+ rv = getClatIngress6ProgFd(isEthernet.value());
+ if (rv < 0) {
+ ALOGE("getClatIngress6ProgFd(%d) failure: %s", isEthernet.value(), strerror(-rv));
+ return;
+ }
+ unique_fd rxProgFd(rv);
+
+ ClatEgress4Key txKey = {
+ .iif = tracker.v4ifIndex,
+ .local4 = tracker.v4,
+ };
+ ClatEgress4Value txValue = {
+ .oif = tracker.ifIndex,
+ .local6 = tracker.v6,
+ .pfx96 = tracker.pfx96,
+ .oifIsEthernet = isEthernet.value(),
+ };
+
+ auto ret = mClatEgress4Map.writeValue(txKey, txValue, BPF_ANY);
+ if (!ret.ok()) {
+ ALOGE("mClatEgress4Map.writeValue failure: %s", strerror(ret.error().code()));
+ return;
+ }
+
+ ClatIngress6Key rxKey = {
+ .iif = tracker.ifIndex,
+ .pfx96 = tracker.pfx96,
+ .local6 = tracker.v6,
+ };
+ ClatIngress6Value rxValue = {
+ // TODO: move all the clat code to eBPF and remove the tun interface entirely.
+ .oif = tracker.v4ifIndex,
+ .local4 = tracker.v4,
+ };
+
+ ret = mClatIngress6Map.writeValue(rxKey, rxValue, BPF_ANY);
+ if (!ret.ok()) {
+ ALOGE("mClatIngress6Map.writeValue failure: %s", strerror(ret.error().code()));
+ ret = mClatEgress4Map.deleteValue(txKey);
+ if (!ret.ok())
+ ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+ return;
+ }
+
+ // We do tc setup *after* populating the maps, so scanning through them
+ // can always be used to tell us what needs cleanup.
+
+ // Usually the clsact will be added in RouteController::addInterfaceToPhysicalNetwork.
+ // But clat is started before the v4- interface is added to the network. The clat startup have
+ // to add clsact of v4- tun interface first for adding bpf filter in maybeStartBpf.
+ // TODO: move "qdisc add clsact" of v4- tun interface out from ClatdController.
+ rv = tcQdiscAddDevClsact(tracker.v4ifIndex);
+ if (rv) {
+ ALOGE("tcQdiscAddDevClsact(%d[%s]) failure: %s", tracker.v4ifIndex, tracker.v4iface,
+ strerror(-rv));
+ ret = mClatEgress4Map.deleteValue(txKey);
+ if (!ret.ok())
+ ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+ ret = mClatIngress6Map.deleteValue(rxKey);
+ if (!ret.ok())
+ ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
+ return;
+ }
+
+ rv = tcFilterAddDevEgressClatIpv4(tracker.v4ifIndex, txRawIpProgFd, RAWIP);
+ if (rv) {
+ ALOGE("tcFilterAddDevEgressClatIpv4(%d[%s], RAWIP) failure: %s", tracker.v4ifIndex,
+ tracker.v4iface, strerror(-rv));
+
+ // The v4- interface clsact is not deleted for unwinding error because once it is created
+ // with interface addition, the lifetime is till interface deletion. Moreover, the clsact
+ // has no clat filter now. It should not break anything.
+
+ ret = mClatEgress4Map.deleteValue(txKey);
+ if (!ret.ok())
+ ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+ ret = mClatIngress6Map.deleteValue(rxKey);
+ if (!ret.ok())
+ ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
+ return;
+ }
+
+ rv = tcFilterAddDevIngressClatIpv6(tracker.ifIndex, rxProgFd, isEthernet.value());
+ if (rv) {
+ ALOGE("tcFilterAddDevIngressClatIpv6(%d[%s], %d) failure: %s", tracker.ifIndex,
+ tracker.iface, isEthernet.value(), strerror(-rv));
+ rv = tcFilterDelDevEgressClatIpv4(tracker.v4ifIndex);
+ if (rv) {
+ ALOGE("tcFilterDelDevEgressClatIpv4(%d[%s]) failure: %s", tracker.v4ifIndex,
+ tracker.v4iface, strerror(-rv));
+ }
+
+ // The v4- interface clsact is not deleted. See the reason in the error unwinding code of
+ // the egress filter attaching of v4- tun interface.
+
+ ret = mClatEgress4Map.deleteValue(txKey);
+ if (!ret.ok())
+ ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+ ret = mClatIngress6Map.deleteValue(rxKey);
+ if (!ret.ok())
+ ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
+ return;
+ }
+
+ // success
+}
+
+void maybeStopBpf(const ClatdTracker& tracker) {
+ int rv = tcFilterDelDevIngressClatIpv6(tracker.ifIndex);
+ if (rv < 0) {
+ ALOGE("tcFilterDelDevIngressClatIpv6(%d[%s]) failure: %s", tracker.ifIndex, tracker.iface,
+ strerror(-rv));
+ }
+
+ rv = tcFilterDelDevEgressClatIpv4(tracker.v4ifIndex);
+ if (rv < 0) {
+ ALOGE("tcFilterDelDevEgressClatIpv4(%d[%s]) failure: %s", tracker.v4ifIndex,
+ tracker.v4iface, strerror(-rv));
+ }
+
+ // We cleanup the maps last, so scanning through them can be used to
+ // determine what still needs cleanup.
+
+ ClatEgress4Key txKey = {
+ .iif = tracker.v4ifIndex,
+ .local4 = tracker.v4,
+ };
+
+ auto ret = mClatEgress4Map.deleteValue(txKey);
+ if (!ret.ok()) ALOGE("mClatEgress4Map.deleteValue failure: %s", strerror(ret.error().code()));
+
+ ClatIngress6Key rxKey = {
+ .iif = tracker.ifIndex,
+ .pfx96 = tracker.pfx96,
+ .local6 = tracker.v6,
+ };
+
+ ret = mClatIngress6Map.deleteValue(rxKey);
+ if (!ret.ok()) ALOGE("mClatIngress6Map.deleteValue failure: %s", strerror(ret.error().code()));
+}
+
+} // namespace clat
+} // namespace net
+} // namespace android
diff --git a/service/native/libs/libclat/clatutils.cpp b/service/native/libs/libclat/clatutils.cpp
new file mode 100644
index 0000000..4a125ba
--- /dev/null
+++ b/service/native/libs/libclat/clatutils.cpp
@@ -0,0 +1,268 @@
+// 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.
+
+#define LOG_TAG "clatutils"
+
+#include "libclat/clatutils.h"
+
+#include <errno.h>
+#include <linux/filter.h>
+#include <linux/if_packet.h>
+#include <linux/if_tun.h>
+#include <log/log.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+extern "C" {
+#include "checksum.h"
+}
+
+// Sync from external/android-clat/clatd.h
+#define MAXMTU 65536
+#define PACKETLEN (MAXMTU + sizeof(struct tun_pi))
+
+// Sync from system/netd/include/netid_client.h.
+#define MARK_UNSET 0u
+
+namespace android {
+namespace net {
+namespace clat {
+
+bool isIpv4AddressFree(in_addr_t addr) {
+ int s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (s == -1) {
+ return 0;
+ }
+
+ // Attempt to connect to the address. If the connection succeeds and getsockname returns the
+ // same then the address is already assigned to the system and we can't use it.
+ struct sockaddr_in sin = {
+ .sin_family = AF_INET,
+ .sin_port = htons(53),
+ .sin_addr = {addr},
+ };
+ socklen_t len = sizeof(sin);
+ bool inuse = connect(s, (struct sockaddr*)&sin, sizeof(sin)) == 0 &&
+ getsockname(s, (struct sockaddr*)&sin, &len) == 0 && (size_t)len >= sizeof(sin) &&
+ sin.sin_addr.s_addr == addr;
+
+ close(s);
+ return !inuse;
+}
+
+// Picks a free IPv4 address, starting from ip and trying all addresses in the prefix in order.
+// ip - the IP address from the configuration file
+// prefixlen - the length of the prefix from which addresses may be selected.
+// returns: the IPv4 address, or INADDR_NONE if no addresses were available
+in_addr_t selectIpv4Address(const in_addr ip, int16_t prefixlen) {
+ return selectIpv4AddressInternal(ip, prefixlen, isIpv4AddressFree);
+}
+
+// Only allow testing to use this function directly. Otherwise call selectIpv4Address(ip, pfxlen)
+// which has applied valid isIpv4AddressFree function pointer.
+in_addr_t selectIpv4AddressInternal(const in_addr ip, int16_t prefixlen,
+ isIpv4AddrFreeFn isIpv4AddressFreeFunc) {
+ // Impossible! Only test allows to apply fn.
+ if (isIpv4AddressFreeFunc == nullptr) {
+ return INADDR_NONE;
+ }
+
+ // Don't accept prefixes that are too large because we scan addresses one by one.
+ if (prefixlen < 16 || prefixlen > 32) {
+ return INADDR_NONE;
+ }
+
+ // All these are in host byte order.
+ in_addr_t mask = 0xffffffff >> (32 - prefixlen) << (32 - prefixlen);
+ in_addr_t ipv4 = ntohl(ip.s_addr);
+ in_addr_t first_ipv4 = ipv4;
+ in_addr_t prefix = ipv4 & mask;
+
+ // Pick the first IPv4 address in the pool, wrapping around if necessary.
+ // So, for example, 192.0.0.4 -> 192.0.0.5 -> 192.0.0.6 -> 192.0.0.7 -> 192.0.0.0.
+ do {
+ if (isIpv4AddressFreeFunc(htonl(ipv4))) {
+ return htonl(ipv4);
+ }
+ ipv4 = prefix | ((ipv4 + 1) & ~mask);
+ } while (ipv4 != first_ipv4);
+
+ return INADDR_NONE;
+}
+
+// Alters the bits in the IPv6 address to make them checksum neutral with v4 and nat64Prefix.
+void makeChecksumNeutral(in6_addr* v6, const in_addr v4, const in6_addr& nat64Prefix) {
+ // Fill last 8 bytes of IPv6 address with random bits.
+ arc4random_buf(&v6->s6_addr[8], 8);
+
+ // Make the IID checksum-neutral. That is, make it so that:
+ // checksum(Local IPv4 | Remote IPv4) = checksum(Local IPv6 | Remote IPv6)
+ // in other words (because remote IPv6 = NAT64 prefix | Remote IPv4):
+ // checksum(Local IPv4) = checksum(Local IPv6 | NAT64 prefix)
+ // Do this by adjusting the two bytes in the middle of the IID.
+
+ uint16_t middlebytes = (v6->s6_addr[11] << 8) + v6->s6_addr[12];
+
+ uint32_t c1 = ip_checksum_add(0, &v4, sizeof(v4));
+ uint32_t c2 = ip_checksum_add(0, &nat64Prefix, sizeof(nat64Prefix)) +
+ ip_checksum_add(0, v6, sizeof(*v6));
+
+ uint16_t delta = ip_checksum_adjust(middlebytes, c1, c2);
+ v6->s6_addr[11] = delta >> 8;
+ v6->s6_addr[12] = delta & 0xff;
+}
+
+// Picks a random interface ID that is checksum neutral with the IPv4 address and the NAT64 prefix.
+int generateIpv6Address(const char* iface, const in_addr v4, const in6_addr& nat64Prefix,
+ in6_addr* v6) {
+ int s = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (s == -1) return -errno;
+
+ if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, iface, strlen(iface) + 1) == -1) {
+ close(s);
+ return -errno;
+ }
+
+ sockaddr_in6 sin6 = {.sin6_family = AF_INET6, .sin6_addr = nat64Prefix};
+ if (connect(s, reinterpret_cast<struct sockaddr*>(&sin6), sizeof(sin6)) == -1) {
+ close(s);
+ return -errno;
+ }
+
+ socklen_t len = sizeof(sin6);
+ if (getsockname(s, reinterpret_cast<struct sockaddr*>(&sin6), &len) == -1) {
+ close(s);
+ return -errno;
+ }
+
+ *v6 = sin6.sin6_addr;
+
+ if (IN6_IS_ADDR_UNSPECIFIED(v6) || IN6_IS_ADDR_LOOPBACK(v6) || IN6_IS_ADDR_LINKLOCAL(v6) ||
+ IN6_IS_ADDR_SITELOCAL(v6) || IN6_IS_ADDR_ULA(v6)) {
+ close(s);
+ return -ENETUNREACH;
+ }
+
+ makeChecksumNeutral(v6, v4, nat64Prefix);
+ close(s);
+
+ return 0;
+}
+
+int detect_mtu(const struct in6_addr* plat_subnet, uint32_t plat_suffix, uint32_t mark) {
+ // Create an IPv6 UDP socket.
+ int s = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (s < 0) {
+ int ret = errno;
+ ALOGE("socket(AF_INET6, SOCK_DGRAM, 0) failed: %s", strerror(errno));
+ return -ret;
+ }
+
+ // Socket's mark affects routing decisions (network selection)
+ if ((mark != MARK_UNSET) && setsockopt(s, SOL_SOCKET, SO_MARK, &mark, sizeof(mark))) {
+ int ret = errno;
+ ALOGE("setsockopt(SOL_SOCKET, SO_MARK) failed: %s", strerror(errno));
+ close(s);
+ return -ret;
+ }
+
+ // Try to connect udp socket to plat_subnet(96 bits):plat_suffix(32 bits)
+ struct sockaddr_in6 dst = {
+ .sin6_family = AF_INET6,
+ .sin6_addr = *plat_subnet,
+ };
+ dst.sin6_addr.s6_addr32[3] = plat_suffix;
+ if (connect(s, (struct sockaddr*)&dst, sizeof(dst))) {
+ int ret = errno;
+ ALOGE("connect() failed: %s", strerror(errno));
+ close(s);
+ return -ret;
+ }
+
+ // Fetch the socket's IPv6 mtu - this is effectively fetching mtu from routing table
+ int mtu;
+ socklen_t sz_mtu = sizeof(mtu);
+ if (getsockopt(s, SOL_IPV6, IPV6_MTU, &mtu, &sz_mtu)) {
+ int ret = errno;
+ ALOGE("getsockopt(SOL_IPV6, IPV6_MTU) failed: %s", strerror(errno));
+ close(s);
+ return -ret;
+ }
+ if (sz_mtu != sizeof(mtu)) {
+ ALOGE("getsockopt(SOL_IPV6, IPV6_MTU) returned unexpected size: %d", sz_mtu);
+ close(s);
+ return -EFAULT;
+ }
+ close(s);
+
+ return mtu;
+}
+
+/* function: configure_packet_socket
+ * Binds the packet socket and attaches the receive filter to it.
+ * sock - the socket to configure
+ * addr - the IP address to filter
+ * ifindex - index of interface to add the filter to
+ * returns: 0 on success, -errno on failure
+ */
+int configure_packet_socket(int sock, in6_addr* addr, int ifindex) {
+ uint32_t* ipv6 = addr->s6_addr32;
+
+ // clang-format off
+ struct sock_filter filter_code[] = {
+ // Load the first four bytes of the IPv6 destination address (starts 24 bytes in).
+ // Compare it against the first four bytes of our IPv6 address, in host byte order (BPF loads
+ // are always in host byte order). If it matches, continue with next instruction (JMP 0). If it
+ // doesn't match, jump ahead to statement that returns 0 (ignore packet). Repeat for the other
+ // three words of the IPv6 address, and if they all match, return PACKETLEN (accept packet).
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 24),
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, htonl(ipv6[0]), 0, 7),
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 28),
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, htonl(ipv6[1]), 0, 5),
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 32),
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, htonl(ipv6[2]), 0, 3),
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 36),
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, htonl(ipv6[3]), 0, 1),
+ BPF_STMT(BPF_RET | BPF_K, PACKETLEN),
+ BPF_STMT(BPF_RET | BPF_K, 0),
+ };
+ // clang-format on
+ struct sock_fprog filter = {sizeof(filter_code) / sizeof(filter_code[0]), filter_code};
+
+ if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter))) {
+ int res = errno;
+ ALOGE("attach packet filter failed: %s", strerror(errno));
+ return -res;
+ }
+
+ struct sockaddr_ll sll = {
+ .sll_family = AF_PACKET,
+ .sll_protocol = htons(ETH_P_IPV6),
+ .sll_ifindex = ifindex,
+ .sll_pkttype =
+ PACKET_OTHERHOST, // The 464xlat IPv6 address is not assigned to the kernel.
+ };
+ if (bind(sock, (struct sockaddr*)&sll, sizeof(sll))) {
+ int res = errno;
+ ALOGE("binding packet socket: %s", strerror(errno));
+ return -res;
+ }
+
+ return 0;
+}
+
+} // namespace clat
+} // namespace net
+} // namespace android
diff --git a/service/native/libs/libclat/clatutils_test.cpp b/service/native/libs/libclat/clatutils_test.cpp
new file mode 100644
index 0000000..4153e19
--- /dev/null
+++ b/service/native/libs/libclat/clatutils_test.cpp
@@ -0,0 +1,187 @@
+// 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.
+
+#include "libclat/clatutils.h"
+
+#include <android-base/stringprintf.h>
+#include <arpa/inet.h>
+#include <gtest/gtest.h>
+#include <linux/if_packet.h>
+#include <linux/if_tun.h>
+#include "tun_interface.h"
+
+extern "C" {
+#include "checksum.h"
+}
+
+// Default translation parameters.
+static const char kIPv4LocalAddr[] = "192.0.0.4";
+
+namespace android {
+namespace net {
+namespace clat {
+
+using android::net::TunInterface;
+using base::StringPrintf;
+
+class ClatUtils : public ::testing::Test {};
+
+// Mock functions for isIpv4AddressFree.
+bool neverFree(in_addr_t /* addr */) {
+ return 0;
+}
+bool alwaysFree(in_addr_t /* addr */) {
+ return 1;
+}
+bool only2Free(in_addr_t addr) {
+ return (ntohl(addr) & 0xff) == 2;
+}
+bool over6Free(in_addr_t addr) {
+ return (ntohl(addr) & 0xff) >= 6;
+}
+bool only10Free(in_addr_t addr) {
+ return (ntohl(addr) & 0xff) == 10;
+}
+
+// Apply mocked isIpv4AddressFree function for selectIpv4Address test.
+in_addr_t selectIpv4Address(const in_addr ip, int16_t prefixlen,
+ isIpv4AddrFreeFn fn /* mocked function */) {
+ // Call internal function to replace isIpv4AddressFreeFn for testing.
+ return selectIpv4AddressInternal(ip, prefixlen, fn);
+}
+
+TEST_F(ClatUtils, SelectIpv4Address) {
+ struct in_addr addr;
+
+ inet_pton(AF_INET, kIPv4LocalAddr, &addr);
+
+ // If no addresses are free, return INADDR_NONE.
+ EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 29, neverFree));
+ EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 16, neverFree));
+
+ // If the configured address is free, pick that. But a prefix that's too big is invalid.
+ EXPECT_EQ(inet_addr(kIPv4LocalAddr), selectIpv4Address(addr, 29, alwaysFree));
+ EXPECT_EQ(inet_addr(kIPv4LocalAddr), selectIpv4Address(addr, 20, alwaysFree));
+ EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 15, alwaysFree));
+
+ // A prefix length of 32 works, but anything above it is invalid.
+ EXPECT_EQ(inet_addr(kIPv4LocalAddr), selectIpv4Address(addr, 32, alwaysFree));
+ EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 33, alwaysFree));
+
+ // If another address is free, pick it.
+ EXPECT_EQ(inet_addr("192.0.0.6"), selectIpv4Address(addr, 29, over6Free));
+
+ // Check that we wrap around to addresses that are lower than the first address.
+ EXPECT_EQ(inet_addr("192.0.0.2"), selectIpv4Address(addr, 29, only2Free));
+ EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 30, only2Free));
+
+ // If a free address exists outside the prefix, we don't pick it.
+ EXPECT_EQ(INADDR_NONE, selectIpv4Address(addr, 29, only10Free));
+ EXPECT_EQ(inet_addr("192.0.0.10"), selectIpv4Address(addr, 24, only10Free));
+
+ // Now try using the real function which sees if IP addresses are free using bind().
+ // Assume that the machine running the test has the address 127.0.0.1, but not 8.8.8.8.
+ addr.s_addr = inet_addr("8.8.8.8");
+ EXPECT_EQ(inet_addr("8.8.8.8"), selectIpv4Address(addr, 29));
+
+ addr.s_addr = inet_addr("127.0.0.1");
+ EXPECT_EQ(inet_addr("127.0.0.2"), selectIpv4Address(addr, 29));
+}
+
+TEST_F(ClatUtils, MakeChecksumNeutral) {
+ // We can't test generateIPv6Address here since it requires manipulating routing, which we can't
+ // do without talking to the real netd on the system.
+ uint32_t rand = arc4random_uniform(0xffffffff);
+ uint16_t rand1 = rand & 0xffff;
+ uint16_t rand2 = (rand >> 16) & 0xffff;
+ std::string v6PrefixStr = StringPrintf("2001:db8:%x:%x", rand1, rand2);
+ std::string v6InterfaceAddrStr = StringPrintf("%s::%x:%x", v6PrefixStr.c_str(), rand2, rand1);
+ std::string nat64PrefixStr = StringPrintf("2001:db8:%x:%x::", rand2, rand1);
+
+ in_addr v4 = {inet_addr(kIPv4LocalAddr)};
+ in6_addr v6InterfaceAddr;
+ ASSERT_TRUE(inet_pton(AF_INET6, v6InterfaceAddrStr.c_str(), &v6InterfaceAddr));
+ in6_addr nat64Prefix;
+ ASSERT_TRUE(inet_pton(AF_INET6, nat64PrefixStr.c_str(), &nat64Prefix));
+
+ // Generate a boatload of random IIDs.
+ int onebits = 0;
+ uint64_t prev_iid = 0;
+ for (int i = 0; i < 100000; i++) {
+ in6_addr v6 = v6InterfaceAddr;
+ makeChecksumNeutral(&v6, v4, nat64Prefix);
+
+ // Check the generated IP address is in the same prefix as the interface IPv6 address.
+ EXPECT_EQ(0, memcmp(&v6, &v6InterfaceAddr, 8));
+
+ // Check that consecutive IIDs are not the same.
+ uint64_t iid = *(uint64_t*)(&v6.s6_addr[8]);
+ ASSERT_TRUE(iid != prev_iid)
+ << "Two consecutive random IIDs are the same: " << std::showbase << std::hex << iid
+ << "\n";
+ prev_iid = iid;
+
+ // Check that the IID is checksum-neutral with the NAT64 prefix and the
+ // local prefix.
+ uint16_t c1 = ip_checksum_finish(ip_checksum_add(0, &v4, sizeof(v4)));
+ uint16_t c2 = ip_checksum_finish(ip_checksum_add(0, &nat64Prefix, sizeof(nat64Prefix)) +
+ ip_checksum_add(0, &v6, sizeof(v6)));
+
+ if (c1 != c2) {
+ char v6Str[INET6_ADDRSTRLEN];
+ inet_ntop(AF_INET6, &v6, v6Str, sizeof(v6Str));
+ FAIL() << "Bad IID: " << v6Str << " not checksum-neutral with " << kIPv4LocalAddr
+ << " and " << nat64PrefixStr.c_str() << std::showbase << std::hex
+ << "\n IPv4 checksum: " << c1 << "\n IPv6 checksum: " << c2 << "\n";
+ }
+
+ // Check that IIDs are roughly random and use all the bits by counting the
+ // total number of bits set to 1 in a random sample of 100000 generated IIDs.
+ onebits += __builtin_popcountll(*(uint64_t*)&iid);
+ }
+ EXPECT_LE(3190000, onebits);
+ EXPECT_GE(3210000, onebits);
+}
+
+TEST_F(ClatUtils, DetectMtu) {
+ // ::1 with bottom 32 bits set to 1 is still ::1 which routes via lo with mtu of 64KiB
+ ASSERT_EQ(detect_mtu(&in6addr_loopback, htonl(1), 0 /*MARK_UNSET*/), 65536);
+}
+
+TEST_F(ClatUtils, ConfigurePacketSocket) {
+ // Create an interface for configure_packet_socket to attach socket filter to.
+ TunInterface v6Iface;
+ ASSERT_EQ(0, v6Iface.init());
+
+ int s = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC, htons(ETH_P_IPV6));
+ EXPECT_LE(0, s);
+ struct in6_addr addr6;
+ EXPECT_EQ(1, inet_pton(AF_INET6, "2001:db8::f00", &addr6));
+ EXPECT_EQ(0, configure_packet_socket(s, &addr6, v6Iface.ifindex()));
+
+ // Check that the packet socket is bound to the interface. We can't check the socket filter
+ // because there is no way to fetch it from the kernel.
+ sockaddr_ll sll;
+ socklen_t len = sizeof(sll);
+ ASSERT_EQ(0, getsockname(s, reinterpret_cast<sockaddr*>(&sll), &len));
+ EXPECT_EQ(htons(ETH_P_IPV6), sll.sll_protocol);
+ EXPECT_EQ(sll.sll_ifindex, v6Iface.ifindex());
+
+ close(s);
+ v6Iface.destroy();
+}
+
+} // namespace clat
+} // namespace net
+} // namespace android
diff --git a/service/native/libs/libclat/include/libclat/TcUtils.h b/service/native/libs/libclat/include/libclat/TcUtils.h
new file mode 100644
index 0000000..212838e
--- /dev/null
+++ b/service/native/libs/libclat/include/libclat/TcUtils.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#pragma once
+
+#include <android-base/result.h>
+#include <errno.h>
+#include <linux/if_ether.h>
+#include <linux/if_link.h>
+#include <linux/rtnetlink.h>
+
+#include <string>
+
+#include "bpf/BpfUtils.h"
+#include "bpf_shared.h"
+
+namespace android {
+namespace net {
+
+// For better code clarity - do not change values - used for booleans like
+// with_ethernet_header or isEthernet.
+constexpr bool RAWIP = false;
+constexpr bool ETHER = true;
+
+// For better code clarity when used for 'bool ingress' parameter.
+constexpr bool EGRESS = false;
+constexpr bool INGRESS = true;
+
+// The priority of clat hook - must be after tethering.
+constexpr uint16_t PRIO_CLAT = 4;
+
+// this returns an ARPHRD_* constant or a -errno
+int hardwareAddressType(const std::string& interface);
+
+// return MTU or -errno
+int deviceMTU(const std::string& interface);
+
+base::Result<bool> isEthernet(const std::string& interface);
+
+inline int getClatEgress4MapFd(void) {
+ const int fd = bpf::mapRetrieveRW(CLAT_EGRESS4_MAP_PATH);
+ return (fd == -1) ? -errno : fd;
+}
+
+inline int getClatEgress4ProgFd(bool with_ethernet_header) {
+ const int fd = bpf::retrieveProgram(with_ethernet_header ? CLAT_EGRESS4_PROG_ETHER_PATH
+ : CLAT_EGRESS4_PROG_RAWIP_PATH);
+ return (fd == -1) ? -errno : fd;
+}
+
+inline int getClatIngress6MapFd(void) {
+ const int fd = bpf::mapRetrieveRW(CLAT_INGRESS6_MAP_PATH);
+ return (fd == -1) ? -errno : fd;
+}
+
+inline int getClatIngress6ProgFd(bool with_ethernet_header) {
+ const int fd = bpf::retrieveProgram(with_ethernet_header ? CLAT_INGRESS6_PROG_ETHER_PATH
+ : CLAT_INGRESS6_PROG_RAWIP_PATH);
+ return (fd == -1) ? -errno : fd;
+}
+
+int doTcQdiscClsact(int ifIndex, uint16_t nlMsgType, uint16_t nlMsgFlags);
+
+inline int tcQdiscAddDevClsact(int ifIndex) {
+ return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_EXCL | NLM_F_CREATE);
+}
+
+inline int tcQdiscReplaceDevClsact(int ifIndex) {
+ return doTcQdiscClsact(ifIndex, RTM_NEWQDISC, NLM_F_CREATE | NLM_F_REPLACE);
+}
+
+inline int tcQdiscDelDevClsact(int ifIndex) {
+ return doTcQdiscClsact(ifIndex, RTM_DELQDISC, 0);
+}
+
+// tc filter add dev .. in/egress prio 4 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
+// direct-action
+int tcFilterAddDevBpf(int ifIndex, bool ingress, uint16_t proto, int bpfFd, bool ethernet);
+
+// tc filter add dev .. ingress prio 4 protocol ipv6 bpf object-pinned /sys/fs/bpf/... direct-action
+inline int tcFilterAddDevIngressClatIpv6(int ifIndex, int bpfFd, bool ethernet) {
+ return tcFilterAddDevBpf(ifIndex, INGRESS, ETH_P_IPV6, bpfFd, ethernet);
+}
+
+// tc filter add dev .. egress prio 4 protocol ip bpf object-pinned /sys/fs/bpf/... direct-action
+inline int tcFilterAddDevEgressClatIpv4(int ifIndex, int bpfFd, bool ethernet) {
+ return tcFilterAddDevBpf(ifIndex, EGRESS, ETH_P_IP, bpfFd, ethernet);
+}
+
+// tc filter del dev .. in/egress prio .. protocol ..
+int tcFilterDelDev(int ifIndex, bool ingress, uint16_t prio, uint16_t proto);
+
+// tc filter del dev .. ingress prio 4 protocol ipv6
+inline int tcFilterDelDevIngressClatIpv6(int ifIndex) {
+ return tcFilterDelDev(ifIndex, INGRESS, PRIO_CLAT, ETH_P_IPV6);
+}
+
+// tc filter del dev .. egress prio 4 protocol ip
+inline int tcFilterDelDevEgressClatIpv4(int ifIndex) {
+ return tcFilterDelDev(ifIndex, EGRESS, PRIO_CLAT, ETH_P_IP);
+}
+
+} // namespace net
+} // namespace android
diff --git a/service/native/libs/libclat/include/libclat/bpfhelper.h b/service/native/libs/libclat/include/libclat/bpfhelper.h
new file mode 100644
index 0000000..c0328c0
--- /dev/null
+++ b/service/native/libs/libclat/include/libclat/bpfhelper.h
@@ -0,0 +1,40 @@
+// Copyright (C) 2021 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.
+
+#pragma once
+
+#include <arpa/inet.h>
+#include <linux/if.h>
+
+namespace android {
+namespace net {
+namespace clat {
+
+struct ClatdTracker {
+ unsigned ifIndex;
+ char iface[IFNAMSIZ];
+ unsigned v4ifIndex;
+ char v4iface[IFNAMSIZ];
+ in_addr v4;
+ in6_addr v6;
+ in6_addr pfx96;
+};
+
+int initMaps(void);
+void maybeStartBpf(const ClatdTracker& tracker);
+void maybeStopBpf(const ClatdTracker& tracker);
+
+} // namespace clat
+} // namespace net
+} // namespace android
diff --git a/service/native/libs/libclat/include/libclat/clatutils.h b/service/native/libs/libclat/include/libclat/clatutils.h
new file mode 100644
index 0000000..812c86e
--- /dev/null
+++ b/service/native/libs/libclat/include/libclat/clatutils.h
@@ -0,0 +1,37 @@
+// 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.
+
+#pragma once
+#include <netinet/in.h>
+#include <netinet/in6.h>
+
+namespace android {
+namespace net {
+namespace clat {
+
+bool isIpv4AddressFree(in_addr_t addr);
+in_addr_t selectIpv4Address(const in_addr ip, int16_t prefixlen);
+void makeChecksumNeutral(in6_addr* v6, const in_addr v4, const in6_addr& nat64Prefix);
+int generateIpv6Address(const char* iface, const in_addr v4, const in6_addr& nat64Prefix,
+ in6_addr* v6);
+int detect_mtu(const struct in6_addr* plat_subnet, uint32_t plat_suffix, uint32_t mark);
+int configure_packet_socket(int sock, in6_addr* addr, int ifindex);
+
+// For testing
+typedef bool (*isIpv4AddrFreeFn)(in_addr_t);
+in_addr_t selectIpv4AddressInternal(const in_addr ip, int16_t prefixlen, isIpv4AddrFreeFn fn);
+
+} // namespace clat
+} // namespace net
+} // namespace android
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
new file mode 100644
index 0000000..9d89788
--- /dev/null
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -0,0 +1,261 @@
+/*
+ * 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 com.android.server;
+
+import android.os.ServiceSpecificException;
+import android.system.Os;
+import android.util.Log;
+
+/**
+ * BpfNetMaps is responsible for providing traffic controller relevant functionality.
+ *
+ * {@hide}
+ */
+public class BpfNetMaps {
+ private static final String TAG = "BpfNetMaps";
+
+ static {
+ System.loadLibrary("traffic_controller_jni");
+ native_init();
+ }
+
+ /**
+ * Add naughty app bandwidth rule for specific app
+ *
+ * @param uid uid of target app
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void addNaughtyApp(final int uid) {
+ final int err = native_addNaughtyApp(uid);
+ if (err != 0) {
+ throw new ServiceSpecificException(err, "Unable to add naughty app: "
+ + Os.strerror(err));
+ }
+ }
+
+ /**
+ * Remove naughty app bandwidth rule for specific app
+ *
+ * @param uid uid of target app
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void removeNaughtyApp(final int uid) {
+ final int err = native_removeNaughtyApp(uid);
+ if (err != 0) {
+ throw new ServiceSpecificException(err, "Unable to remove naughty app: "
+ + Os.strerror(err));
+ }
+ }
+
+ /**
+ * Add nice app bandwidth rule for specific app
+ *
+ * @param uid uid of target app
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void addNiceApp(final int uid) {
+ final int err = native_addNiceApp(uid);
+ if (err != 0) {
+ throw new ServiceSpecificException(err, "Unable to add nice app: "
+ + Os.strerror(err));
+ }
+ }
+
+ /**
+ * Remove nice app bandwidth rule for specific app
+ *
+ * @param uid uid of target app
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void removeNiceApp(final int uid) {
+ final int err = native_removeNiceApp(uid);
+ if (err != 0) {
+ throw new ServiceSpecificException(err, "Unable to remove nice app: "
+ + Os.strerror(err));
+ }
+ }
+
+ /**
+ * Set target firewall child chain
+ *
+ * @param childChain target chain to enable
+ * @param enable whether to enable or disable child chain.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void setChildChain(final int childChain, final boolean enable) {
+ final int err = native_setChildChain(childChain, enable);
+ if (err != 0) {
+ throw new ServiceSpecificException(-err, "Unable to set child chain: "
+ + Os.strerror(-err));
+ }
+ }
+
+ /**
+ * Replaces the contents of the specified UID-based firewall chain.
+ *
+ * The chain may be an allowlist chain or a denylist chain. A denylist chain contains DROP
+ * rules for the specified UIDs and a RETURN rule at the end. An allowlist chain contains RETURN
+ * rules for the system UID range (0 to {@code UID_APP} - 1), RETURN rules for for the specified
+ * UIDs, and a DROP rule at the end. The chain will be created if it does not exist.
+ *
+ * @param chainName The name of the chain to replace.
+ * @param isAllowlist Whether this is an allowlist or denylist chain.
+ * @param uids The list of UIDs to allow/deny.
+ * @return true if the chain was successfully replaced, false otherwise.
+ */
+ public int replaceUidChain(final String chainName, final boolean isAllowlist,
+ final int[] uids) {
+ final int err = native_replaceUidChain(chainName, isAllowlist, uids);
+ if (err != 0) {
+ Log.e(TAG, "replaceUidChain failed: " + Os.strerror(-err));
+ }
+ return -err;
+ }
+
+ /**
+ * Set firewall rule for uid
+ *
+ * @param childChain target chain
+ * @param uid uid to allow/deny
+ * @param firewallRule either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void setUidRule(final int childChain, final int uid,
+ final int firewallRule) {
+ final int err = native_setUidRule(childChain, uid, firewallRule);
+ if (err != 0) {
+ throw new ServiceSpecificException(-err, "Unable to set uid rule: "
+ + Os.strerror(-err));
+ }
+ }
+
+ /**
+ * Add ingress interface filtering rules to a list of UIDs
+ *
+ * For a given uid, once a filtering rule is added, the kernel will only allow packets from the
+ * allowed interface and loopback to be sent to the list of UIDs.
+ *
+ * Calling this method on one or more UIDs with an existing filtering rule but a different
+ * interface name will result in the filtering rule being updated to allow the new interface
+ * instead. Otherwise calling this method will not affect existing rules set on other UIDs.
+ *
+ * @param ifName the name of the interface on which the filtering rules will allow packets to
+ be received.
+ * @param uids an array of UIDs which the filtering rules will be set
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void addUidInterfaceRules(final String ifName, final int[] uids) {
+ final int err = native_addUidInterfaceRules(ifName, uids);
+ if (err != 0) {
+ throw new ServiceSpecificException(err, "Unable to add uid interface rules: "
+ + Os.strerror(err));
+ }
+ }
+
+ /**
+ * Remove ingress interface filtering rules from a list of UIDs
+ *
+ * Clear the ingress interface filtering rules from the list of UIDs which were previously set
+ * by addUidInterfaceRules(). Ignore any uid which does not have filtering rule.
+ *
+ * @param uids an array of UIDs from which the filtering rules will be removed
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void removeUidInterfaceRules(final int[] uids) {
+ final int err = native_removeUidInterfaceRules(uids);
+ if (err != 0) {
+ throw new ServiceSpecificException(err, "Unable to remove uid interface rules: "
+ + Os.strerror(err));
+ }
+ }
+
+ /**
+ * Request netd to change the current active network stats map.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void swapActiveStatsMap() {
+ final int err = native_swapActiveStatsMap();
+ if (err != 0) {
+ throw new ServiceSpecificException(err, "Unable to swap active stats map: "
+ + Os.strerror(err));
+ }
+ }
+
+ /**
+ * Assigns android.permission.INTERNET and/or android.permission.UPDATE_DEVICE_STATS to the uids
+ * specified. Or remove all permissions from the uids.
+ *
+ * @param permission The permission to grant, it could be either PERMISSION_INTERNET and/or
+ * PERMISSION_UPDATE_DEVICE_STATS. If the permission is NO_PERMISSIONS, then
+ * revoke all permissions for the uids.
+ * @param uids uid of users to grant permission
+ */
+ public void setNetPermForUids(final int permission, final int[] uids) {
+ native_setPermissionForUids(permission, uids);
+ }
+
+ /**
+ * Set counter set for uid
+ *
+ * @param counterSet either SET_DEFAULT or SET_FOREGROUND
+ * @param uid uid to foreground/background
+ */
+ public int setCounterSet(final int counterSet, final int uid) {
+ final int err = native_setCounterSet(counterSet, uid);
+ if (err != 0) {
+ Log.e(TAG, "setCounterSet failed: " + Os.strerror(-err));
+ }
+ return -err;
+ }
+
+ /**
+ * Reset Uid stats
+ * @param tag default 0
+ * @param uid given uid to be clear
+ */
+ public int deleteTagData(final int tag, final int uid) {
+ final int err = native_deleteTagData(tag, uid);
+ if (err != 0) {
+ Log.e(TAG, "deleteTagData failed: " + Os.strerror(-err));
+ }
+ return -err;
+ }
+
+ private static native void native_init();
+ private native int native_addNaughtyApp(int uid);
+ private native int native_removeNaughtyApp(int uid);
+ private native int native_addNiceApp(int uid);
+ private native int native_removeNiceApp(int uid);
+ private native int native_setChildChain(int childChain, boolean enable);
+ private native int native_replaceUidChain(String name, boolean isAllowlist, int[] uids);
+ private native int native_setUidRule(int childChain, int uid, int firewallRule);
+ private native int native_addUidInterfaceRules(String ifName, int[] uids);
+ private native int native_removeUidInterfaceRules(int[] uids);
+ private native int native_swapActiveStatsMap();
+ private native void native_setPermissionForUids(int permission, int[] uids);
+ private native int native_setCounterSet(int counterSet, int uid);
+ private native int native_deleteTagData(int tag, int uid);
+}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index d625d1b..fa7dfa9 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -73,6 +73,8 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
@@ -124,6 +126,7 @@
import android.net.ConnectivitySettingsManager;
import android.net.DataStallReportParcelable;
import android.net.DnsResolverServiceManager;
+import android.net.DscpPolicy;
import android.net.ICaptivePortal;
import android.net.IConnectivityDiagnosticsCallback;
import android.net.IConnectivityManager;
@@ -218,6 +221,7 @@
import android.os.UserManager;
import android.provider.Settings;
import android.sysprop.NetworkProperties;
+import android.system.ErrnoException;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -248,6 +252,7 @@
import com.android.server.connectivity.ConnectivityFlags;
import com.android.server.connectivity.DnsManager;
import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
+import com.android.server.connectivity.DscpPolicyTracker;
import com.android.server.connectivity.FullScore;
import com.android.server.connectivity.KeepaliveTracker;
import com.android.server.connectivity.LingerMonitor;
@@ -262,6 +267,7 @@
import com.android.server.connectivity.ProfileNetworkPreferenceList;
import com.android.server.connectivity.ProxyTracker;
import com.android.server.connectivity.QosCallbackTracker;
+import com.android.server.connectivity.UidRangeUtils;
import libcore.io.IoUtils;
@@ -386,6 +392,7 @@
protected IDnsResolver mDnsResolver;
@VisibleForTesting
protected INetd mNetd;
+ private DscpPolicyTracker mDscpPolicyTracker = null;
private NetworkStatsManager mStatsManager;
private NetworkPolicyManager mPolicyManager;
private final NetdCallback mNetdCallback;
@@ -1486,19 +1493,33 @@
new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null,
new NetworkAgentConfig(), this, null, null, 0, INVALID_UID,
mLingerDelayMs, mQosCallbackTracker, mDeps);
+
+ try {
+ // DscpPolicyTracker cannot run on S because on S the tethering module can only load
+ // BPF programs/maps into /sys/fs/tethering/bpf, which the system server cannot access.
+ // Even if it could, running on S would at least require mocking out the BPF map,
+ // otherwise the unit tests will fail on pre-T devices where the seccomp filter blocks
+ // the bpf syscall. http://aosp/1907693
+ if (SdkLevel.isAtLeastT()) {
+ mDscpPolicyTracker = new DscpPolicyTracker();
+ }
+ } catch (ErrnoException e) {
+ loge("Unable to create DscpPolicyTracker");
+ }
}
private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) {
- return createDefaultNetworkCapabilitiesForUidRange(new UidRange(uid, uid));
+ return createDefaultNetworkCapabilitiesForUidRangeSet(Collections.singleton(
+ new UidRange(uid, uid)));
}
- private static NetworkCapabilities createDefaultNetworkCapabilitiesForUidRange(
- @NonNull final UidRange uids) {
+ private static NetworkCapabilities createDefaultNetworkCapabilitiesForUidRangeSet(
+ @NonNull final Set<UidRange> uidRangeSet) {
final NetworkCapabilities netCap = new NetworkCapabilities();
netCap.addCapability(NET_CAPABILITY_INTERNET);
netCap.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
netCap.removeCapability(NET_CAPABILITY_NOT_VPN);
- netCap.setUids(UidRange.toIntRanges(Collections.singleton(uids)));
+ netCap.setUids(UidRange.toIntRanges(uidRangeSet));
return netCap;
}
@@ -3402,6 +3423,25 @@
nai.setLingerDuration((int) arg.second);
break;
}
+ case NetworkAgent.EVENT_ADD_DSCP_POLICY: {
+ DscpPolicy policy = (DscpPolicy) arg.second;
+ if (mDscpPolicyTracker != null) {
+ mDscpPolicyTracker.addDscpPolicy(nai, policy);
+ }
+ break;
+ }
+ case NetworkAgent.EVENT_REMOVE_DSCP_POLICY: {
+ if (mDscpPolicyTracker != null) {
+ mDscpPolicyTracker.removeDscpPolicy(nai, (int) arg.second);
+ }
+ break;
+ }
+ case NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES: {
+ if (mDscpPolicyTracker != null) {
+ mDscpPolicyTracker.removeAllDscpPolicies(nai);
+ }
+ break;
+ }
}
}
@@ -4042,7 +4082,7 @@
config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.VIRTUAL,
INetd.PERMISSION_NONE,
(nai.networkAgentConfig == null || !nai.networkAgentConfig.allowBypass),
- getVpnType(nai), /*excludeLocalRoutes=*/ false);
+ getVpnType(nai), nai.networkAgentConfig.excludeLocalRouteVpn);
} else {
config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL,
getNetworkPermission(nai.networkCapabilities), /*secure=*/ false,
@@ -10145,14 +10185,30 @@
switch (preference.getPreference()) {
case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT:
nc = null;
+ if (preference.getPreferenceEnterpriseId() != 0) {
+ throw new IllegalArgumentException(
+ "Invalid enterprise identifier in setProfileNetworkPreferences");
+ }
break;
case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK:
allowFallback = false;
// continue to process the enterprise preference.
case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE:
- final UidRange uids = UidRange.createForUser(profile);
- nc = createDefaultNetworkCapabilitiesForUidRange(uids);
+ if (!isEnterpriseIdentifierValid(preference.getPreferenceEnterpriseId())) {
+ throw new IllegalArgumentException(
+ "Invalid enterprise identifier in setProfileNetworkPreferences");
+ }
+ final Set<UidRange> uidRangeSet =
+ getUidListToBeAppliedForNetworkPreference(profile, preference);
+ if (!isRangeAlreadyInPreferenceList(preferenceList, uidRangeSet)) {
+ nc = createDefaultNetworkCapabilitiesForUidRangeSet(uidRangeSet);
+ } else {
+ throw new IllegalArgumentException(
+ "Overlapping uid range in setProfileNetworkPreferences");
+ }
nc.addCapability(NET_CAPABILITY_ENTERPRISE);
+ nc.addEnterpriseId(
+ preference.getPreferenceEnterpriseId());
nc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
break;
default:
@@ -10166,6 +10222,44 @@
new Pair<>(preferenceList, listener)));
}
+ private Set<UidRange> getUidListToBeAppliedForNetworkPreference(
+ @NonNull final UserHandle profile,
+ @NonNull final ProfileNetworkPreference profileNetworkPreference) {
+ final UidRange profileUids = UidRange.createForUser(profile);
+ Set<UidRange> uidRangeSet = UidRangeUtils.convertListToUidRange(
+ profileNetworkPreference.getIncludedUids());
+ if (uidRangeSet.size() > 0) {
+ if (!UidRangeUtils.isRangeSetInUidRange(profileUids, uidRangeSet)) {
+ throw new IllegalArgumentException(
+ "Allow uid range is outside the uid range of profile.");
+ }
+ } else {
+ ArraySet<UidRange> disallowUidRangeSet = UidRangeUtils.convertListToUidRange(
+ profileNetworkPreference.getExcludedUids());
+ if (disallowUidRangeSet.size() > 0) {
+ if (!UidRangeUtils.isRangeSetInUidRange(profileUids, disallowUidRangeSet)) {
+ throw new IllegalArgumentException(
+ "disallow uid range is outside the uid range of profile.");
+ }
+ uidRangeSet = UidRangeUtils.removeRangeSetFromUidRange(profileUids,
+ disallowUidRangeSet);
+ } else {
+ uidRangeSet = new ArraySet<UidRange>();
+ uidRangeSet.add(profileUids);
+ }
+ }
+ return uidRangeSet;
+ }
+
+ private boolean isEnterpriseIdentifierValid(
+ @NetworkCapabilities.EnterpriseId int identifier) {
+ if ((identifier >= NET_ENTERPRISE_ID_1)
+ && (identifier <= NET_ENTERPRISE_ID_5)) {
+ return true;
+ }
+ return false;
+ }
+
private void validateNetworkCapabilitiesOfProfileNetworkPreference(
@Nullable final NetworkCapabilities nc) {
if (null == nc) return; // Null caps are always allowed. It means to remove the setting.
@@ -10187,6 +10281,11 @@
nrs.add(createDefaultInternetRequestForTransport(
TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
}
+ if (VDBG) {
+ loge("pref.capabilities.getUids():" + UidRange.fromIntRanges(
+ pref.capabilities.getUids()));
+ }
+
setNetworkRequestUids(nrs, UidRange.fromIntRanges(pref.capabilities.getUids()));
final NetworkRequestInfo nri = new NetworkRequestInfo(Process.myUid(), nrs,
PREFERENCE_ORDER_PROFILE);
@@ -10195,6 +10294,25 @@
return result;
}
+ /**
+ * Compare if the given UID range sets have the same UIDs.
+ *
+ */
+ private boolean isRangeAlreadyInPreferenceList(
+ @NonNull List<ProfileNetworkPreferenceList.Preference> preferenceList,
+ @NonNull Set<UidRange> uidRangeSet) {
+ if (uidRangeSet.size() == 0 || preferenceList.size() == 0) {
+ return false;
+ }
+ for (ProfileNetworkPreferenceList.Preference pref : preferenceList) {
+ if (UidRangeUtils.doesRangeSetOverlap(
+ UidRange.fromIntRanges(pref.capabilities.getUids()), uidRangeSet)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void handleSetProfileNetworkPreference(
@NonNull final List<ProfileNetworkPreferenceList.Preference> preferenceList,
@Nullable final IOnCompleteListener listener) {
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
new file mode 100644
index 0000000..c57983b
--- /dev/null
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -0,0 +1,406 @@
+/*
+ * 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 com.android.server.connectivity;
+
+import static android.net.INetd.IF_STATE_UP;
+import static android.net.INetd.PERMISSION_SYSTEM;
+
+import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.INetd;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.InterfaceParams;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+/**
+ * This coordinator is responsible for providing clat relevant functionality.
+ *
+ * {@hide}
+ */
+public class ClatCoordinator {
+ private static final String TAG = ClatCoordinator.class.getSimpleName();
+
+ // Sync from external/android-clat/clatd.c
+ // 40 bytes IPv6 header - 20 bytes IPv4 header + 8 bytes fragment header.
+ @VisibleForTesting
+ static final int MTU_DELTA = 28;
+ @VisibleForTesting
+ static final int CLAT_MAX_MTU = 65536;
+
+ // This must match the interface prefix in clatd.c.
+ private static final String CLAT_PREFIX = "v4-";
+
+ // For historical reasons, start with 192.0.0.4, and after that, use all subsequent addresses
+ // in 192.0.0.0/29 (RFC 7335).
+ @VisibleForTesting
+ static final String INIT_V4ADDR_STRING = "192.0.0.4";
+ @VisibleForTesting
+ static final int INIT_V4ADDR_PREFIX_LEN = 29;
+ private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8");
+
+ private static final int INVALID_IFINDEX = 0;
+ private static final int INVALID_PID = 0;
+
+ @NonNull
+ private final INetd mNetd;
+ @NonNull
+ private final Dependencies mDeps;
+ @Nullable
+ private String mIface = null;
+ @Nullable
+ private String mNat64Prefix = null;
+ @Nullable
+ private String mXlatLocalAddress4 = null;
+ @Nullable
+ private String mXlatLocalAddress6 = null;
+ private int mPid = INVALID_PID;
+
+ @VisibleForTesting
+ abstract static class Dependencies {
+ /**
+ * Get netd.
+ */
+ @NonNull
+ public abstract INetd getNetd();
+
+ /**
+ * @see ParcelFileDescriptor#adoptFd(int).
+ */
+ @NonNull
+ public ParcelFileDescriptor adoptFd(int fd) {
+ return ParcelFileDescriptor.adoptFd(fd);
+ }
+
+ /**
+ * Get interface index for a given interface.
+ */
+ public int getInterfaceIndex(String ifName) {
+ final InterfaceParams params = InterfaceParams.getByName(ifName);
+ return params != null ? params.index : INVALID_IFINDEX;
+ }
+
+ /**
+ * Create tun interface for a given interface name.
+ */
+ public int createTunInterface(@NonNull String tuniface) throws IOException {
+ return native_createTunInterface(tuniface);
+ }
+
+ /**
+ * Pick an IPv4 address for clat.
+ */
+ @NonNull
+ public String selectIpv4Address(@NonNull String v4addr, int prefixlen)
+ throws IOException {
+ return native_selectIpv4Address(v4addr, prefixlen);
+ }
+
+ /**
+ * Generate a checksum-neutral IID.
+ */
+ @NonNull
+ public String generateIpv6Address(@NonNull String iface, @NonNull String v4,
+ @NonNull String prefix64) throws IOException {
+ return native_generateIpv6Address(iface, v4, prefix64);
+ }
+
+ /**
+ * Detect MTU.
+ */
+ public int detectMtu(@NonNull String platSubnet, int platSuffix, int mark)
+ throws IOException {
+ return native_detectMtu(platSubnet, platSuffix, mark);
+ }
+
+ /**
+ * Open packet socket.
+ */
+ public int openPacketSocket() throws IOException {
+ return native_openPacketSocket();
+ }
+
+ /**
+ * Open IPv6 raw socket and set SO_MARK.
+ */
+ public int openRawSocket6(int mark) throws IOException {
+ return native_openRawSocket6(mark);
+ }
+
+ /**
+ * Add anycast setsockopt.
+ */
+ public void addAnycastSetsockopt(@NonNull FileDescriptor sock, String v6, int ifindex)
+ throws IOException {
+ native_addAnycastSetsockopt(sock, v6, ifindex);
+ }
+
+ /**
+ * Configure packet socket.
+ */
+ public void configurePacketSocket(@NonNull FileDescriptor sock, String v6, int ifindex)
+ throws IOException {
+ native_configurePacketSocket(sock, v6, ifindex);
+ }
+
+ /**
+ * Start clatd.
+ */
+ public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+ @NonNull FileDescriptor writesock6, @NonNull String iface, @NonNull String pfx96,
+ @NonNull String v4, @NonNull String v6) throws IOException {
+ return native_startClatd(tunfd, readsock6, writesock6, iface, pfx96, v4, v6);
+ }
+
+ /**
+ * Stop clatd.
+ */
+ public void stopClatd(String iface, String pfx96, String v4, String v6, int pid)
+ throws IOException {
+ native_stopClatd(iface, pfx96, v4, v6, pid);
+ }
+ }
+
+ @VisibleForTesting
+ static int getFwmark(int netId) {
+ // See union Fwmark in system/netd/include/Fwmark.h
+ return (netId & 0xffff)
+ | 0x1 << 16 // protectedFromVpn: true
+ | 0x1 << 17 // explicitlySelected: true
+ | (PERMISSION_SYSTEM & 0x3) << 18;
+ }
+
+ @VisibleForTesting
+ static int adjustMtu(int mtu) {
+ // clamp to minimum ipv6 mtu - this probably cannot ever trigger
+ if (mtu < IPV6_MIN_MTU) mtu = IPV6_MIN_MTU;
+ // clamp to buffer size
+ if (mtu > CLAT_MAX_MTU) mtu = CLAT_MAX_MTU;
+ // decrease by ipv6(40) + ipv6 fragmentation header(8) vs ipv4(20) overhead of 28 bytes
+ mtu -= MTU_DELTA;
+
+ return mtu;
+ }
+
+ public ClatCoordinator(@NonNull Dependencies deps) {
+ mDeps = deps;
+ mNetd = mDeps.getNetd();
+ }
+
+ /**
+ * Start clatd for a given interface and NAT64 prefix.
+ */
+ public String clatStart(final String iface, final int netId,
+ @NonNull final IpPrefix nat64Prefix)
+ throws IOException {
+ if (mIface != null || mPid != INVALID_PID) {
+ throw new IOException("Clatd is already running on " + mIface + " (pid " + mPid + ")");
+ }
+ if (nat64Prefix.getPrefixLength() != 96) {
+ throw new IOException("Prefix must be 96 bits long: " + nat64Prefix);
+ }
+
+ // [1] Pick an IPv4 address from 192.0.0.4, 192.0.0.5, 192.0.0.6 ..
+ final String v4;
+ try {
+ v4 = mDeps.selectIpv4Address(INIT_V4ADDR_STRING, INIT_V4ADDR_PREFIX_LEN);
+ } catch (IOException e) {
+ throw new IOException("no IPv4 addresses were available for clat: " + e);
+ }
+
+ // [2] Generate a checksum-neutral IID.
+ final String pfx96 = nat64Prefix.getAddress().getHostAddress();
+ final String v6;
+ try {
+ v6 = mDeps.generateIpv6Address(iface, v4, pfx96);
+ } catch (IOException e) {
+ throw new IOException("no IPv6 addresses were available for clat: " + e);
+ }
+
+ // [3] Open, configure and bring up the tun interface.
+ // Create the v4-... tun interface.
+ final String tunIface = CLAT_PREFIX + iface;
+ final ParcelFileDescriptor tunFd;
+ try {
+ tunFd = mDeps.adoptFd(mDeps.createTunInterface(tunIface));
+ } catch (IOException e) {
+ throw new IOException("Create tun interface " + tunIface + " failed: " + e);
+ }
+
+ // disable IPv6 on it - failing to do so is not a critical error
+ try {
+ mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */);
+ } catch (RemoteException | ServiceSpecificException e) {
+ tunFd.close();
+ Log.e(TAG, "Disable IPv6 on " + tunIface + " failed: " + e);
+ }
+
+ // Detect ipv4 mtu.
+ final Integer fwmark = getFwmark(netId);
+ final int detectedMtu = mDeps.detectMtu(pfx96,
+ ByteBuffer.wrap(GOOGLE_DNS_4.getAddress()).getInt(), fwmark);
+ final int mtu = adjustMtu(detectedMtu);
+ Log.i(TAG, "ipv4 mtu is " + mtu);
+
+ // TODO: add setIptablesDropRule
+
+ // Config tun interface mtu, address and bring up.
+ try {
+ mNetd.interfaceSetMtu(tunIface, mtu);
+ } catch (RemoteException | ServiceSpecificException e) {
+ tunFd.close();
+ throw new IOException("Set MTU " + mtu + " on " + tunIface + " failed: " + e);
+ }
+ final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
+ ifConfig.ifName = tunIface;
+ ifConfig.ipv4Addr = v4;
+ ifConfig.prefixLength = 32;
+ ifConfig.hwAddr = "";
+ ifConfig.flags = new String[] {IF_STATE_UP};
+ try {
+ mNetd.interfaceSetCfg(ifConfig);
+ } catch (RemoteException | ServiceSpecificException e) {
+ tunFd.close();
+ throw new IOException("Setting IPv4 address to " + ifConfig.ipv4Addr + "/"
+ + ifConfig.prefixLength + " failed on " + ifConfig.ifName + ": " + e);
+ }
+
+ // [4] Open and configure local 464xlat read/write sockets.
+ // Opens a packet socket to receive IPv6 packets in clatd.
+ final ParcelFileDescriptor readSock6;
+ try {
+ // Use a JNI call to get native file descriptor instead of Os.socket() because we would
+ // like to use ParcelFileDescriptor to manage file descriptor. But ctor
+ // ParcelFileDescriptor(FileDescriptor fd) is a @hide function. Need to use native file
+ // descriptor to initialize ParcelFileDescriptor object instead.
+ readSock6 = mDeps.adoptFd(mDeps.openPacketSocket());
+ } catch (IOException e) {
+ tunFd.close();
+ throw new IOException("Open packet socket failed: " + e);
+ }
+
+ // Opens a raw socket with a given fwmark to send IPv6 packets in clatd.
+ final ParcelFileDescriptor writeSock6;
+ try {
+ // Use a JNI call to get native file descriptor instead of Os.socket(). See above
+ // reason why we use jniOpenPacketSocket6().
+ writeSock6 = mDeps.adoptFd(mDeps.openRawSocket6(fwmark));
+ } catch (IOException e) {
+ tunFd.close();
+ readSock6.close();
+ throw new IOException("Open raw socket failed: " + e);
+ }
+
+ final int ifaceIndex = mDeps.getInterfaceIndex(iface);
+ if (ifaceIndex == INVALID_IFINDEX) {
+ tunFd.close();
+ readSock6.close();
+ writeSock6.close();
+ throw new IOException("Fail to get interface index for interface " + iface);
+ }
+
+ // Start translating packets to the new prefix.
+ try {
+ mDeps.addAnycastSetsockopt(writeSock6.getFileDescriptor(), v6, ifaceIndex);
+ } catch (IOException e) {
+ tunFd.close();
+ readSock6.close();
+ writeSock6.close();
+ throw new IOException("add anycast sockopt failed: " + e);
+ }
+
+ // Update our packet socket filter to reflect the new 464xlat IP address.
+ try {
+ mDeps.configurePacketSocket(readSock6.getFileDescriptor(), v6, ifaceIndex);
+ } catch (IOException e) {
+ tunFd.close();
+ readSock6.close();
+ writeSock6.close();
+ throw new IOException("configure packet socket failed: " + e);
+ }
+
+ // [5] Start clatd.
+ try {
+ mPid = mDeps.startClatd(tunFd.getFileDescriptor(), readSock6.getFileDescriptor(),
+ writeSock6.getFileDescriptor(), iface, pfx96, v4, v6);
+ mIface = iface;
+ mNat64Prefix = pfx96;
+ mXlatLocalAddress4 = v4;
+ mXlatLocalAddress6 = v6;
+ } catch (IOException e) {
+ throw new IOException("Error start clatd on " + iface + ": " + e);
+ } finally {
+ tunFd.close();
+ readSock6.close();
+ writeSock6.close();
+ }
+
+ return v6;
+ }
+
+ /**
+ * Stop clatd
+ */
+ public void clatStop() throws IOException {
+ if (mPid == INVALID_PID) {
+ throw new IOException("Clatd has not started");
+ }
+ Log.i(TAG, "Stopping clatd pid=" + mPid + " on " + mIface);
+
+ mDeps.stopClatd(mIface, mNat64Prefix, mXlatLocalAddress4, mXlatLocalAddress6, mPid);
+ // TODO: remove setIptablesDropRule
+
+ Log.i(TAG, "clatd on " + mIface + " stopped");
+
+ mIface = null;
+ mNat64Prefix = null;
+ mXlatLocalAddress4 = null;
+ mXlatLocalAddress6 = null;
+ mPid = INVALID_PID;
+ }
+
+ private static native String native_selectIpv4Address(String v4addr, int prefixlen)
+ throws IOException;
+ private static native String native_generateIpv6Address(String iface, String v4,
+ String prefix64) throws IOException;
+ private static native int native_createTunInterface(String tuniface) throws IOException;
+ private static native int native_detectMtu(String platSubnet, int platSuffix, int mark)
+ throws IOException;
+ private static native int native_openPacketSocket() throws IOException;
+ private static native int native_openRawSocket6(int mark) throws IOException;
+ private static native void native_addAnycastSetsockopt(FileDescriptor sock, String v6,
+ int ifindex) throws IOException;
+ private static native void native_configurePacketSocket(FileDescriptor sock, String v6,
+ int ifindex) throws IOException;
+ private static native int native_startClatd(FileDescriptor tunfd, FileDescriptor readsock6,
+ FileDescriptor writesock6, String iface, String pfx96, String v4, String v6)
+ throws IOException;
+ private static native void native_stopClatd(String iface, String pfx96, String v4, String v6,
+ int pid) throws IOException;
+}
diff --git a/service/src/com/android/server/connectivity/DscpPolicyTracker.java b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
new file mode 100644
index 0000000..43cfc8f
--- /dev/null
+++ b/service/src/com/android/server/connectivity/DscpPolicyTracker.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static android.net.DscpPolicy.STATUS_DELETED;
+import static android.net.DscpPolicy.STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+import static android.net.DscpPolicy.STATUS_POLICY_NOT_FOUND;
+import static android.net.DscpPolicy.STATUS_SUCCESS;
+import static android.system.OsConstants.ETH_P_ALL;
+
+import android.annotation.NonNull;
+import android.net.DscpPolicy;
+import android.os.RemoteException;
+import android.system.ErrnoException;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.TcUtils;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.NetworkInterface;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * DscpPolicyTracker has a single entry point from ConnectivityService handler.
+ * This guarantees that all code runs on the same thread and no locking is needed.
+ */
+public class DscpPolicyTracker {
+ // After tethering and clat priorities.
+ static final short PRIO_DSCP = 5;
+
+ private static final String TAG = DscpPolicyTracker.class.getSimpleName();
+ private static final String PROG_PATH =
+ "/sys/fs/bpf/prog_dscp_policy_schedcls_set_dscp";
+ // Name is "map + *.o + map_name + map". Can probably shorten this
+ private static final String IPV4_POLICY_MAP_PATH = makeMapPath(
+ "dscp_policy_ipv4_dscp_policies");
+ private static final String IPV6_POLICY_MAP_PATH = makeMapPath(
+ "dscp_policy_ipv6_dscp_policies");
+ private static final int MAX_POLICIES = 16;
+
+ private static String makeMapPath(String which) {
+ return "/sys/fs/bpf/map_" + which + "_map";
+ }
+
+ private Set<String> mAttachedIfaces;
+
+ private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv4Policies;
+ private final BpfMap<Struct.U32, DscpPolicyValue> mBpfDscpIpv6Policies;
+ private final SparseIntArray mPolicyIdToBpfMapIndex;
+
+ public DscpPolicyTracker() throws ErrnoException {
+ mAttachedIfaces = new HashSet<String>();
+
+ mPolicyIdToBpfMapIndex = new SparseIntArray(MAX_POLICIES);
+ mBpfDscpIpv4Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV4_POLICY_MAP_PATH,
+ BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
+ mBpfDscpIpv6Policies = new BpfMap<Struct.U32, DscpPolicyValue>(IPV6_POLICY_MAP_PATH,
+ BpfMap.BPF_F_RDWR, Struct.U32.class, DscpPolicyValue.class);
+ }
+
+ private int getFirstFreeIndex() {
+ for (int i = 0; i < MAX_POLICIES; i++) {
+ if (mPolicyIdToBpfMapIndex.indexOfValue(i) < 0) return i;
+ }
+ return MAX_POLICIES;
+ }
+
+ private void sendStatus(NetworkAgentInfo nai, int policyId, int status) {
+ try {
+ nai.networkAgent.onDscpPolicyStatusUpdated(policyId, status);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Failed update policy status: ", e);
+ }
+ }
+
+ private boolean matchesIpv4(DscpPolicy policy) {
+ return ((policy.getDestinationAddress() == null
+ || policy.getDestinationAddress() instanceof Inet4Address)
+ && (policy.getSourceAddress() == null
+ || policy.getSourceAddress() instanceof Inet4Address));
+ }
+
+ private boolean matchesIpv6(DscpPolicy policy) {
+ return ((policy.getDestinationAddress() == null
+ || policy.getDestinationAddress() instanceof Inet6Address)
+ && (policy.getSourceAddress() == null
+ || policy.getSourceAddress() instanceof Inet6Address));
+ }
+
+ private int addDscpPolicyInternal(DscpPolicy policy) {
+ // If there is no existing policy with a matching ID, and we are already at
+ // the maximum number of policies then return INSUFFICIENT_PROCESSING_RESOURCES.
+ final int existingIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId(), -1);
+ if (existingIndex == -1 && mPolicyIdToBpfMapIndex.size() >= MAX_POLICIES) {
+ return STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+ }
+
+ // Currently all classifiers are supported, if any are removed return
+ // STATUS_REQUESTED_CLASSIFIER_NOT_SUPPORTED,
+ // and for any other generic error STATUS_REQUEST_DECLINED
+
+ int addIndex = 0;
+ // If a policy with a matching ID exists, replace it, otherwise use the next free
+ // index for the policy.
+ if (existingIndex != -1) {
+ addIndex = mPolicyIdToBpfMapIndex.get(policy.getPolicyId());
+ } else {
+ addIndex = getFirstFreeIndex();
+ }
+
+ try {
+ mPolicyIdToBpfMapIndex.put(policy.getPolicyId(), addIndex);
+
+ // Add v4 policy to mBpfDscpIpv4Policies if source and destination address
+ // are both null or if they are both instances of Inet6Address.
+ if (matchesIpv4(policy)) {
+ mBpfDscpIpv4Policies.insertOrReplaceEntry(
+ new Struct.U32(addIndex),
+ new DscpPolicyValue(policy.getSourceAddress(),
+ policy.getDestinationAddress(),
+ policy.getSourcePort(), policy.getDestinationPortRange(),
+ (short) policy.getProtocol(), (short) policy.getDscpValue()));
+ }
+
+ // Add v6 policy to mBpfDscpIpv6Policies if source and destination address
+ // are both null or if they are both instances of Inet6Address.
+ if (matchesIpv6(policy)) {
+ mBpfDscpIpv6Policies.insertOrReplaceEntry(
+ new Struct.U32(addIndex),
+ new DscpPolicyValue(policy.getSourceAddress(),
+ policy.getDestinationAddress(),
+ policy.getSourcePort(), policy.getDestinationPortRange(),
+ (short) policy.getProtocol(), (short) policy.getDscpValue()));
+ }
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to insert policy into map: ", e);
+ return STATUS_INSUFFICIENT_PROCESSING_RESOURCES;
+ }
+
+ return STATUS_SUCCESS;
+ }
+
+ /**
+ * Add the provided DSCP policy to the bpf map. Attach bpf program dscp_policy to iface
+ * if not already attached. Response will be sent back to nai with status.
+ *
+ * STATUS_SUCCESS - if policy was added successfully
+ * STATUS_INSUFFICIENT_PROCESSING_RESOURCES - if max policies were already set
+ */
+ public void addDscpPolicy(NetworkAgentInfo nai, DscpPolicy policy) {
+ if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
+ if (!attachProgram(nai.linkProperties.getInterfaceName())) {
+ Log.e(TAG, "Unable to attach program");
+ sendStatus(nai, policy.getPolicyId(), STATUS_INSUFFICIENT_PROCESSING_RESOURCES);
+ return;
+ }
+ }
+
+ int status = addDscpPolicyInternal(policy);
+ sendStatus(nai, policy.getPolicyId(), status);
+ }
+
+ private void removePolicyFromMap(NetworkAgentInfo nai, int policyId, int index) {
+ int status = STATUS_POLICY_NOT_FOUND;
+ try {
+ mBpfDscpIpv4Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE);
+ mBpfDscpIpv6Policies.replaceEntry(new Struct.U32(index), DscpPolicyValue.NONE);
+ status = STATUS_DELETED;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to delete policy from map: ", e);
+ }
+
+ sendStatus(nai, policyId, status);
+ }
+
+ /**
+ * Remove specified DSCP policy and detach program if no other policies are active.
+ */
+ public void removeDscpPolicy(NetworkAgentInfo nai, int policyId) {
+ if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
+ // Nothing to remove since program is not attached. Send update back for policy id.
+ sendStatus(nai, policyId, STATUS_POLICY_NOT_FOUND);
+ return;
+ }
+
+ if (mPolicyIdToBpfMapIndex.get(policyId, -1) != -1) {
+ removePolicyFromMap(nai, policyId, mPolicyIdToBpfMapIndex.get(policyId));
+ mPolicyIdToBpfMapIndex.delete(policyId);
+ }
+
+ // TODO: detach should only occur if no more policies are present on the nai's iface.
+ if (mPolicyIdToBpfMapIndex.size() == 0) {
+ detachProgram(nai.linkProperties.getInterfaceName());
+ }
+ }
+
+ /**
+ * Remove all DSCP policies and detach program.
+ */
+ // TODO: Remove all should only remove policies from corresponding nai iface.
+ public void removeAllDscpPolicies(NetworkAgentInfo nai) {
+ if (!mAttachedIfaces.contains(nai.linkProperties.getInterfaceName())) {
+ // Nothing to remove since program is not attached. Send update for policy
+ // id 0. The status update must contain a policy ID, and 0 is an invalid id.
+ sendStatus(nai, 0, STATUS_SUCCESS);
+ return;
+ }
+
+ for (int i = 0; i < mPolicyIdToBpfMapIndex.size(); i++) {
+ removePolicyFromMap(nai, mPolicyIdToBpfMapIndex.keyAt(i),
+ mPolicyIdToBpfMapIndex.valueAt(i));
+ }
+ mPolicyIdToBpfMapIndex.clear();
+
+ // Can detach program since no policies are active.
+ detachProgram(nai.linkProperties.getInterfaceName());
+ }
+
+ /**
+ * Attach BPF program
+ */
+ private boolean attachProgram(@NonNull String iface) {
+ // TODO: attach needs to be per iface not program.
+
+ try {
+ NetworkInterface netIface = NetworkInterface.getByName(iface);
+ TcUtils.tcFilterAddDevBpf(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL,
+ PROG_PATH);
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to attach to TC on " + iface + ": " + e);
+ return false;
+ }
+ mAttachedIfaces.add(iface);
+ return true;
+ }
+
+ /**
+ * Detach BPF program
+ */
+ public void detachProgram(@NonNull String iface) {
+ try {
+ NetworkInterface netIface = NetworkInterface.getByName(iface);
+ if (netIface != null) {
+ TcUtils.tcFilterDelDev(netIface.getIndex(), false, PRIO_DSCP, (short) ETH_P_ALL);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to detach to TC on " + iface + ": " + e);
+ }
+ mAttachedIfaces.remove(iface);
+ }
+}
diff --git a/service/src/com/android/server/connectivity/DscpPolicyValue.java b/service/src/com/android/server/connectivity/DscpPolicyValue.java
new file mode 100644
index 0000000..cb40306
--- /dev/null
+++ b/service/src/com/android/server/connectivity/DscpPolicyValue.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.util.Log;
+import android.util.Range;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/** Value type for DSCP setting and rewriting to DSCP policy BPF maps. */
+public class DscpPolicyValue extends Struct {
+ private static final String TAG = DscpPolicyValue.class.getSimpleName();
+
+ // TODO: add the interface index.
+ @Field(order = 0, type = Type.ByteArray, arraysize = 16)
+ public final byte[] src46;
+
+ @Field(order = 1, type = Type.ByteArray, arraysize = 16)
+ public final byte[] dst46;
+
+ @Field(order = 2, type = Type.UBE16)
+ public final int srcPort;
+
+ @Field(order = 3, type = Type.UBE16)
+ public final int dstPortStart;
+
+ @Field(order = 4, type = Type.UBE16)
+ public final int dstPortEnd;
+
+ @Field(order = 5, type = Type.U8)
+ public final short proto;
+
+ @Field(order = 6, type = Type.U8)
+ public final short dscp;
+
+ @Field(order = 7, type = Type.U8, padding = 3)
+ public final short mask;
+
+ private static final int SRC_IP_MASK = 0x1;
+ private static final int DST_IP_MASK = 0x02;
+ private static final int SRC_PORT_MASK = 0x4;
+ private static final int DST_PORT_MASK = 0x8;
+ private static final int PROTO_MASK = 0x10;
+
+ private boolean ipEmpty(final byte[] ip) {
+ for (int i = 0; i < ip.length; i++) {
+ if (ip[i] != 0) return false;
+ }
+ return true;
+ }
+
+ private byte[] toIpv4MappedAddressBytes(InetAddress ia) {
+ final byte[] addr6 = new byte[16];
+ if (ia != null) {
+ final byte[] addr4 = ia.getAddress();
+ addr6[10] = (byte) 0xff;
+ addr6[11] = (byte) 0xff;
+ addr6[12] = addr4[0];
+ addr6[13] = addr4[1];
+ addr6[14] = addr4[2];
+ addr6[15] = addr4[3];
+ }
+ return addr6;
+ }
+
+ private byte[] toAddressField(InetAddress addr) {
+ if (addr == null) {
+ return EMPTY_ADDRESS_FIELD;
+ } else if (addr instanceof Inet4Address) {
+ return toIpv4MappedAddressBytes(addr);
+ } else {
+ return addr.getAddress();
+ }
+ }
+
+ private static final byte[] EMPTY_ADDRESS_FIELD =
+ InetAddress.parseNumericAddress("::").getAddress();
+
+ private short makeMask(final byte[] src46, final byte[] dst46, final int srcPort,
+ final int dstPortStart, final short proto, final short dscp) {
+ short mask = 0;
+ if (src46 != EMPTY_ADDRESS_FIELD) {
+ mask |= SRC_IP_MASK;
+ }
+ if (dst46 != EMPTY_ADDRESS_FIELD) {
+ mask |= DST_IP_MASK;
+ }
+ if (srcPort != -1) {
+ mask |= SRC_PORT_MASK;
+ }
+ if (dstPortStart != -1 && dstPortEnd != -1) {
+ mask |= DST_PORT_MASK;
+ }
+ if (proto != -1) {
+ mask |= PROTO_MASK;
+ }
+ return mask;
+ }
+
+ // This constructor is necessary for BpfMap#getValue since all values must be
+ // in the constructor.
+ public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
+ final int dstPortStart, final int dstPortEnd, final short proto,
+ final short dscp) {
+ this.src46 = toAddressField(src46);
+ this.dst46 = toAddressField(dst46);
+
+ // These params need to be stored as 0 because uints are used in BpfMap.
+ // If they are -1 BpfMap write will throw errors.
+ this.srcPort = srcPort != -1 ? srcPort : 0;
+ this.dstPortStart = dstPortStart != -1 ? dstPortStart : 0;
+ this.dstPortEnd = dstPortEnd != -1 ? dstPortEnd : 0;
+ this.proto = proto != -1 ? proto : 0;
+
+ this.dscp = dscp;
+ // Use member variables for IP since byte[] is needed and api variables for everything else
+ // so -1 is passed into mask if parameter is not present.
+ this.mask = makeMask(this.src46, this.dst46, srcPort, dstPortStart, proto, dscp);
+ }
+
+ public DscpPolicyValue(final InetAddress src46, final InetAddress dst46, final int srcPort,
+ final Range<Integer> dstPort, final short proto,
+ final short dscp) {
+ this(src46, dst46, srcPort, dstPort != null ? dstPort.getLower() : -1,
+ dstPort != null ? dstPort.getUpper() : -1, proto, dscp);
+ }
+
+ public static final DscpPolicyValue NONE = new DscpPolicyValue(
+ null /* src46 */, null /* dst46 */, -1 /* srcPort */,
+ -1 /* dstPortStart */, -1 /* dstPortEnd */, (short) -1 /* proto */,
+ (short) 0 /* dscp */);
+
+ @Override
+ public String toString() {
+ String srcIpString = "empty";
+ String dstIpString = "empty";
+
+ // Separate try/catch for IP's so it's easier to debug.
+ try {
+ srcIpString = InetAddress.getByAddress(src46).getHostAddress();
+ } catch (UnknownHostException e) {
+ Log.e(TAG, "Invalid SRC IP address", e);
+ }
+
+ try {
+ dstIpString = InetAddress.getByAddress(src46).getHostAddress();
+ } catch (UnknownHostException e) {
+ Log.e(TAG, "Invalid DST IP address", e);
+ }
+
+ try {
+ return String.format(
+ "src46: %s, dst46: %s, srcPort: %d, dstPortStart: %d, dstPortEnd: %d,"
+ + " protocol: %d, dscp %s", srcIpString, dstIpString, srcPort, dstPortStart,
+ dstPortEnd, proto, dscp);
+ } catch (IllegalArgumentException e) {
+ return String.format("String format error: " + e);
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index b7f3ed9..1a7248a 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -24,6 +24,7 @@
import android.annotation.Nullable;
import android.content.Context;
import android.net.CaptivePortalData;
+import android.net.DscpPolicy;
import android.net.IDnsResolver;
import android.net.INetd;
import android.net.INetworkAgent;
@@ -700,6 +701,24 @@
mHandler.obtainMessage(NetworkAgent.EVENT_LINGER_DURATION_CHANGED,
new Pair<>(NetworkAgentInfo.this, durationMs)).sendToTarget();
}
+
+ @Override
+ public void sendAddDscpPolicy(final DscpPolicy policy) {
+ mHandler.obtainMessage(NetworkAgent.EVENT_ADD_DSCP_POLICY,
+ new Pair<>(NetworkAgentInfo.this, policy)).sendToTarget();
+ }
+
+ @Override
+ public void sendRemoveDscpPolicy(final int policyId) {
+ mHandler.obtainMessage(NetworkAgent.EVENT_REMOVE_DSCP_POLICY,
+ new Pair<>(NetworkAgentInfo.this, policyId)).sendToTarget();
+ }
+
+ @Override
+ public void sendRemoveAllDscpPolicies() {
+ mHandler.obtainMessage(NetworkAgent.EVENT_REMOVE_ALL_DSCP_POLICIES,
+ new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
+ }
}
/**
diff --git a/service/src/com/android/server/connectivity/UidRangeUtils.java b/service/src/com/android/server/connectivity/UidRangeUtils.java
new file mode 100644
index 0000000..7318296
--- /dev/null
+++ b/service/src/com/android/server/connectivity/UidRangeUtils.java
@@ -0,0 +1,156 @@
+/*
+ * 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 com.android.server.connectivity;
+
+import android.annotation.NonNull;
+import android.net.UidRange;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Utility class for UidRange
+ *
+ * @hide
+ */
+public final class UidRangeUtils {
+ /**
+ * Check if given uid range set is within the uid range
+ * @param uids uid range in which uidRangeSet is checked to be in range.
+ * @param uidRangeSet uid range set to be be checked if it is in range of uids
+ * @return true uidRangeSet is in the range of uids
+ * @hide
+ */
+ public static boolean isRangeSetInUidRange(@NonNull UidRange uids,
+ @NonNull Set<UidRange> uidRangeSet) {
+ Objects.requireNonNull(uids);
+ Objects.requireNonNull(uidRangeSet);
+ if (uidRangeSet.size() == 0) {
+ return true;
+ }
+ for (UidRange range : uidRangeSet) {
+ if (!uids.contains(range.start) || !uids.contains(range.stop)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Remove given uid ranges set from a uid range
+ * @param uids uid range from which uidRangeSet will be removed
+ * @param uidRangeSet uid range set to be removed from uids.
+ * WARNING : This function requires the UidRanges in uidRangeSet to be disjoint
+ * WARNING : This function requires the arrayset to be iterated in increasing order of the
+ * ranges. Today this is provided by the iteration order stability of
+ * ArraySet, and the fact that the code creating this ArraySet always
+ * creates it in increasing order.
+ * Note : if any of the above is not satisfied this function throws IllegalArgumentException
+ * TODO : remove these limitations
+ * @hide
+ */
+ public static ArraySet<UidRange> removeRangeSetFromUidRange(@NonNull UidRange uids,
+ @NonNull ArraySet<UidRange> uidRangeSet) {
+ Objects.requireNonNull(uids);
+ Objects.requireNonNull(uidRangeSet);
+ final ArraySet<UidRange> filteredRangeSet = new ArraySet<UidRange>();
+ if (uidRangeSet.size() == 0) {
+ filteredRangeSet.add(uids);
+ return filteredRangeSet;
+ }
+
+ int start = uids.start;
+ UidRange previousRange = null;
+ for (UidRange uidRange : uidRangeSet) {
+ if (previousRange != null) {
+ if (previousRange.stop > uidRange.start) {
+ throw new IllegalArgumentException("UID ranges are not increasing order");
+ }
+ }
+ if (uidRange.start > start) {
+ filteredRangeSet.add(new UidRange(start, uidRange.start - 1));
+ start = uidRange.stop + 1;
+ } else if (uidRange.start == start) {
+ start = uidRange.stop + 1;
+ }
+ previousRange = uidRange;
+ }
+ if (start < uids.stop) {
+ filteredRangeSet.add(new UidRange(start, uids.stop));
+ }
+ return filteredRangeSet;
+ }
+
+ /**
+ * Compare if the given UID range sets have overlapping uids
+ * @param uidRangeSet1 first uid range set to check for overlap
+ * @param uidRangeSet2 second uid range set to check for overlap
+ * @hide
+ */
+ public static boolean doesRangeSetOverlap(@NonNull Set<UidRange> uidRangeSet1,
+ @NonNull Set<UidRange> uidRangeSet2) {
+ Objects.requireNonNull(uidRangeSet1);
+ Objects.requireNonNull(uidRangeSet2);
+
+ if (uidRangeSet1.size() == 0 || uidRangeSet2.size() == 0) {
+ return false;
+ }
+ for (UidRange range1 : uidRangeSet1) {
+ for (UidRange range2 : uidRangeSet2) {
+ if (range1.contains(range2.start) || range1.contains(range2.stop)
+ || range2.contains(range1.start) || range2.contains(range1.stop)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Convert a list of uid to set of UidRanges.
+ * @param uids list of uids
+ * @return set of UidRanges
+ * @hide
+ */
+ public static ArraySet<UidRange> convertListToUidRange(@NonNull List<Integer> uids) {
+ Objects.requireNonNull(uids);
+ final ArraySet<UidRange> uidRangeSet = new ArraySet<UidRange>();
+ if (uids.size() == 0) {
+ return uidRangeSet;
+ }
+ List<Integer> uidsNew = new ArrayList<>(uids);
+ Collections.sort(uidsNew);
+ int start = uidsNew.get(0);
+ int stop = start;
+
+ for (Integer i : uidsNew) {
+ if (i <= stop + 1) {
+ stop = i;
+ } else {
+ uidRangeSet.add(new UidRange(start, stop));
+ start = i;
+ stop = i;
+ }
+ }
+ uidRangeSet.add(new UidRange(start, stop));
+ return uidRangeSet;
+ }
+}
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index f1897f5..acf04bf 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -122,3 +122,25 @@
"framework-res",
],
}
+
+// 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.
+
+// test_mainline_modules is an array of strings. Each element in the array is a list of modules
+// separated by "+". The modules in this list must be in alphabetical order.
+// See SuiteModuleLoader.java.
+// TODO: why are the modules separated by + instead of being separate entries in the array?
+mainline_presubmit_modules = [
+ "CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex",
+]
+
+cc_defaults {
+ name: "connectivity-mainline-presubmit-cc-defaults",
+ test_mainline_modules: mainline_presubmit_modules,
+}
+
+java_defaults {
+ name: "connectivity-mainline-presubmit-java-defaults",
+ test_mainline_modules: mainline_presubmit_modules,
+}
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index 9cc001d..bea00a9 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -23,11 +23,6 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4;
-import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
@@ -39,9 +34,16 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY;
import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_2;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_3;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_4;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
@@ -57,6 +59,7 @@
import static com.android.modules.utils.build.SdkLevel.isAtLeastR;
import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.MiscAsserts.assertEmpty;
import static com.android.testutils.MiscAsserts.assertThrows;
import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
@@ -414,6 +417,31 @@
assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities()));
}
+ @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+ public void testPrioritizeLatencyAndBandwidth() {
+ NetworkCapabilities netCap = new NetworkCapabilities();
+ netCap.addCapability(NET_CAPABILITY_PRIORITIZE_LATENCY);
+ netCap.addCapability(NET_CAPABILITY_NOT_METERED);
+ netCap.maybeMarkCapabilitiesRestricted();
+ assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+ netCap = new NetworkCapabilities();
+ netCap.addCapability(NET_CAPABILITY_PRIORITIZE_LATENCY);
+ netCap.removeCapability(NET_CAPABILITY_NOT_METERED);
+ netCap.maybeMarkCapabilitiesRestricted();
+ assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+
+ netCap = new NetworkCapabilities();
+ netCap.addCapability(NET_CAPABILITY_PRIORITIZE_BANDWIDTH);
+ netCap.addCapability(NET_CAPABILITY_NOT_METERED);
+ netCap.maybeMarkCapabilitiesRestricted();
+ assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+ netCap = new NetworkCapabilities();
+ netCap.addCapability(NET_CAPABILITY_PRIORITIZE_BANDWIDTH);
+ netCap.removeCapability(NET_CAPABILITY_NOT_METERED);
+ netCap.maybeMarkCapabilitiesRestricted();
+ assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+ }
+
@Test @IgnoreUpTo(Build.VERSION_CODES.R)
public void testOemPrivate() {
NetworkCapabilities nc = new NetworkCapabilities();
@@ -788,79 +816,80 @@
} catch (IllegalStateException expected) { }
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.S)
- public void testEnterpriseCapabilitySubLevel() {
+ @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+ public void testEnterpriseId() {
final NetworkCapabilities nc1 = new NetworkCapabilities.Builder()
.addCapability(NET_CAPABILITY_ENTERPRISE)
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
+ .addEnterpriseId(NET_ENTERPRISE_ID_1)
.build();
- assertEquals(1, nc1.getEnterpriseCapabilitySubLevels().length);
- assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1,
- nc1.getEnterpriseCapabilitySubLevels()[0]);
+ assertEquals(1, nc1.getEnterpriseIds().length);
+ assertEquals(NET_ENTERPRISE_ID_1,
+ nc1.getEnterpriseIds()[0]);
final NetworkCapabilities nc2 = new NetworkCapabilities.Builder()
.addCapability(NET_CAPABILITY_ENTERPRISE)
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
+ .addEnterpriseId(NET_ENTERPRISE_ID_1)
+ .addEnterpriseId(NET_ENTERPRISE_ID_2)
.build();
- assertEquals(2, nc2.getEnterpriseCapabilitySubLevels().length);
- assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1,
- nc2.getEnterpriseCapabilitySubLevels()[0]);
- assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2,
- nc2.getEnterpriseCapabilitySubLevels()[1]);
+ assertEquals(2, nc2.getEnterpriseIds().length);
+ assertEquals(NET_ENTERPRISE_ID_1,
+ nc2.getEnterpriseIds()[0]);
+ assertEquals(NET_ENTERPRISE_ID_2,
+ nc2.getEnterpriseIds()[1]);
final NetworkCapabilities nc3 = new NetworkCapabilities.Builder()
.addCapability(NET_CAPABILITY_ENTERPRISE)
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3)
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4)
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5)
+ .addEnterpriseId(NET_ENTERPRISE_ID_1)
+ .addEnterpriseId(NET_ENTERPRISE_ID_2)
+ .addEnterpriseId(NET_ENTERPRISE_ID_3)
+ .addEnterpriseId(NET_ENTERPRISE_ID_4)
+ .addEnterpriseId(NET_ENTERPRISE_ID_5)
.build();
- assertEquals(5, nc3.getEnterpriseCapabilitySubLevels().length);
- assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1,
- nc3.getEnterpriseCapabilitySubLevels()[0]);
- assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2,
- nc3.getEnterpriseCapabilitySubLevels()[1]);
- assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_3,
- nc3.getEnterpriseCapabilitySubLevels()[2]);
- assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_4,
- nc3.getEnterpriseCapabilitySubLevels()[3]);
- assertEquals(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_5,
- nc3.getEnterpriseCapabilitySubLevels()[4]);
+ assertEquals(5, nc3.getEnterpriseIds().length);
+ assertEquals(NET_ENTERPRISE_ID_1,
+ nc3.getEnterpriseIds()[0]);
+ assertEquals(NET_ENTERPRISE_ID_2,
+ nc3.getEnterpriseIds()[1]);
+ assertEquals(NET_ENTERPRISE_ID_3,
+ nc3.getEnterpriseIds()[2]);
+ assertEquals(NET_ENTERPRISE_ID_4,
+ nc3.getEnterpriseIds()[3]);
+ assertEquals(NET_ENTERPRISE_ID_5,
+ nc3.getEnterpriseIds()[4]);
final Class<IllegalArgumentException> illegalArgumentExceptionClass =
IllegalArgumentException.class;
assertThrows(illegalArgumentExceptionClass, () -> new NetworkCapabilities.Builder()
- .addEnterpriseCapabilitySubLevel(6)
+ .addEnterpriseId(6)
.build());
assertThrows(illegalArgumentExceptionClass, () -> new NetworkCapabilities.Builder()
- .removeEnterpriseCapabilitySubLevel(6)
+ .removeEnterpriseId(6)
.build());
final Class<IllegalStateException> illegalStateException =
IllegalStateException.class;
assertThrows(illegalStateException, () -> new NetworkCapabilities.Builder()
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
+ .addEnterpriseId(NET_ENTERPRISE_ID_1)
.build());
final NetworkCapabilities nc4 = new NetworkCapabilities.Builder()
.addCapability(NET_CAPABILITY_ENTERPRISE)
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
- .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
- .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
+ .addEnterpriseId(NET_ENTERPRISE_ID_1)
+ .addEnterpriseId(NET_ENTERPRISE_ID_2)
+ .removeEnterpriseId(NET_ENTERPRISE_ID_1)
+ .removeEnterpriseId(NET_ENTERPRISE_ID_2)
.build();
- assertEquals(0, nc4.getEnterpriseCapabilitySubLevels().length);
+ assertEquals(1, nc4.getEnterpriseIds().length);
+ assertTrue(nc4.hasEnterpriseId(NET_ENTERPRISE_ID_1));
final NetworkCapabilities nc5 = new NetworkCapabilities.Builder()
.addCapability(NET_CAPABILITY_CBS)
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
- .addEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
- .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_1)
- .removeEnterpriseCapabilitySubLevel(NET_CAPABILITY_ENTERPRISE_SUB_LEVEL_2)
+ .addEnterpriseId(NET_ENTERPRISE_ID_1)
+ .addEnterpriseId(NET_ENTERPRISE_ID_2)
+ .removeEnterpriseId(NET_ENTERPRISE_ID_1)
+ .removeEnterpriseId(NET_ENTERPRISE_ID_2)
.build();
assertTrue(nc4.satisfiedByNetworkCapabilities(nc1));
- assertFalse(nc1.satisfiedByNetworkCapabilities(nc4));
+ assertTrue(nc1.satisfiedByNetworkCapabilities(nc4));
assertFalse(nc3.satisfiedByNetworkCapabilities(nc2));
assertTrue(nc2.satisfiedByNetworkCapabilities(nc3));
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 916b566..5778b0d 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -36,6 +36,7 @@
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static com.android.testutils.Cleanup.testAndCleanup;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -71,7 +72,6 @@
import android.net.VpnTransportInfo;
import android.net.cts.util.CtsNetUtils;
import android.net.wifi.WifiManager;
-import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
@@ -830,7 +830,7 @@
.getCaps().getUnderlyingNetworks())));
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.S)
+ @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
public void testChangeUnderlyingNetworks() throws Exception {
assumeTrue(supportedHardware());
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 07a319d..e979a3b 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -49,6 +49,7 @@
"FrameworksNetCommonTests",
"core-tests-support",
"cts-net-utils",
+ "CtsNetTestsNonUpdatableLib",
"ctstestrunner-axt",
"junit",
"junit-params",
diff --git a/tests/cts/net/native/Android.bp b/tests/cts/net/native/Android.bp
index 1d1c18e..153ff51 100644
--- a/tests/cts/net/native/Android.bp
+++ b/tests/cts/net/native/Android.bp
@@ -43,6 +43,7 @@
static_libs: [
"libbpf_android",
"libgtest",
+ "libmodules-utils-build",
],
// Tag this module as a cts test artifact
diff --git a/tests/cts/net/native/src/BpfCompatTest.cpp b/tests/cts/net/native/src/BpfCompatTest.cpp
index 874bad4..97ecb9e 100644
--- a/tests/cts/net/native/src/BpfCompatTest.cpp
+++ b/tests/cts/net/native/src/BpfCompatTest.cpp
@@ -21,6 +21,8 @@
#include <gtest/gtest.h>
+#include "android-modules-utils/sdk_level.h"
+
#include "libbpf_android.h"
using namespace android::bpf;
@@ -33,11 +35,17 @@
EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
}
-TEST(BpfTest, bpfStructSizeTest) {
+TEST(BpfTest, bpfStructSizeTestPreT) {
+ if (android::modules::sdklevel::IsAtLeastT()) GTEST_SKIP() << "T+ device.";
doBpfStructSizeTest("/system/etc/bpf/netd.o");
doBpfStructSizeTest("/system/etc/bpf/clatd.o");
}
+TEST(BpfTest, bpfStructSizeTest) {
+ doBpfStructSizeTest("/system/etc/bpf/gpu_mem.o");
+ doBpfStructSizeTest("/system/etc/bpf/time_in_state.o");
+}
+
int main(int argc, char **argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 80c2db4..53e4ab7 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -168,6 +168,7 @@
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRuleKt;
+import com.android.testutils.DumpTestUtils;
import com.android.testutils.RecorderCallback.CallbackEntry;
import com.android.testutils.TestHttpServer;
import com.android.testutils.TestNetworkTracker;
@@ -3021,6 +3022,13 @@
}
}
+ @Test
+ public void testDump() throws Exception {
+ final String dumpOutput = DumpTestUtils.dumpServiceWithShellPermission(
+ Context.CONNECTIVITY_SERVICE, "--short");
+ assertTrue(dumpOutput, dumpOutput.contains("Active default network"));
+ }
+
private void unregisterRegisteredCallbacks() {
for (NetworkCallback callback: mRegisteredCallbacks) {
mCm.unregisterNetworkCallback(callback);
diff --git a/tests/cts/net/src/android/net/cts/DhcpOptionTest.kt b/tests/cts/net/src/android/net/cts/DhcpOptionTest.kt
index 1a62560..555dd87 100644
--- a/tests/cts/net/src/android/net/cts/DhcpOptionTest.kt
+++ b/tests/cts/net/src/android/net/cts/DhcpOptionTest.kt
@@ -16,11 +16,11 @@
package android.net.cts
-import android.os.Build
import android.net.DhcpOption
import androidx.test.filters.SmallTest
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.SC_V2
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@@ -28,7 +28,7 @@
import org.junit.Test
@SmallTest
-@IgnoreUpTo(Build.VERSION_CODES.S)
+@IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
@RunWith(DevSdkIgnoreRunner::class)
class DhcpOptionTest {
private val DHCP_OPTION_TYPE: Byte = 2
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index 4992795..c6fc38f 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -25,6 +25,8 @@
import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
import static android.system.OsConstants.ETIMEDOUT;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -43,7 +45,6 @@
import android.net.NetworkRequest;
import android.net.ParseException;
import android.net.cts.util.CtsNetUtils;
-import android.os.Build;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Looper;
@@ -814,7 +815,7 @@
}
/** Verifies that DnsResolver.DnsException can be subclassed and its constructor re-used. */
- @Test @IgnoreUpTo(Build.VERSION_CODES.S)
+ @Test @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
public void testDnsExceptionConstructor() throws InterruptedException {
class TestDnsException extends DnsResolver.DnsException {
TestDnsException(int code, @Nullable Throwable cause) {
diff --git a/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
new file mode 100644
index 0000000..1f39fee
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DscpPolicyTest.kt
@@ -0,0 +1,522 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.net.cts.util.CtsNetUtils.TestNetworkCallback
+
+import android.app.Instrumentation
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.cts.util.CtsNetUtils
+import android.net.DscpPolicy
+import android.net.DscpPolicy.STATUS_DELETED
+import android.net.DscpPolicy.STATUS_SUCCESS
+import android.net.InetAddresses
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkAgent
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.RouteInfo
+import android.net.util.SocketUtils
+import android.os.Build
+import android.os.HandlerThread
+import android.os.Looper
+import android.platform.test.annotations.AppModeFull
+import android.system.Os
+import android.system.OsConstants
+import android.system.OsConstants.AF_INET
+import android.system.OsConstants.IPPROTO_IP
+import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_DGRAM
+import android.system.OsConstants.SOCK_NONBLOCK
+import android.util.Log
+import android.util.Range
+import androidx.test.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.CompatUtil
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.OffsetFilter
+import com.android.testutils.assertParcelingIsLossless
+import com.android.testutils.runAsShell
+import com.android.testutils.SC_V2
+import com.android.testutils.TapPacketReader
+import com.android.testutils.TestableNetworkAgent
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
+import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnDscpPolicyStatusUpdated
+import com.android.testutils.TestableNetworkCallback
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.Inet4Address
+import java.net.Inet6Address
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.net.ServerSocket
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.util.UUID
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.concurrent.thread
+import kotlin.test.fail
+
+private const val MAX_PACKET_LENGTH = 1500
+
+private val instrumentation: Instrumentation
+ get() = InstrumentationRegistry.getInstrumentation()
+
+private const val TAG = "DscpPolicyTest"
+private const val PACKET_TIMEOUT_MS = 2_000L
+
+@AppModeFull(reason = "Instant apps cannot create test networks")
+@RunWith(AndroidJUnit4::class)
+class DscpPolicyTest {
+ @JvmField
+ @Rule
+ val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = SC_V2)
+
+ private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
+ private val TEST_TARGET_IPV4_ADDR =
+ InetAddresses.parseNumericAddress("8.8.8.8") as Inet4Address
+
+ private val realContext = InstrumentationRegistry.getContext()
+ private val cm = realContext.getSystemService(ConnectivityManager::class.java)
+
+ private val agentsToCleanUp = mutableListOf<NetworkAgent>()
+ private val callbacksToCleanUp = mutableListOf<TestableNetworkCallback>()
+
+ private val handlerThread = HandlerThread(DscpPolicyTest::class.java.simpleName)
+
+ private lateinit var iface: TestNetworkInterface
+ private lateinit var tunNetworkCallback: TestNetworkCallback
+ private lateinit var reader: TapPacketReader
+
+ @Before
+ fun setUp() {
+ runAsShell(MANAGE_TEST_NETWORKS) {
+ val tnm = realContext.getSystemService(TestNetworkManager::class.java)
+
+ iface = tnm.createTunInterface( Array(1){ LinkAddress(LOCAL_IPV4_ADDRESS, 32) } )
+ assertNotNull(iface)
+ }
+
+ handlerThread.start()
+ reader = TapPacketReader(
+ handlerThread.threadHandler,
+ iface.fileDescriptor.fileDescriptor,
+ MAX_PACKET_LENGTH)
+ reader.startAsyncForTest()
+ }
+
+ @After
+ fun tearDown() {
+ agentsToCleanUp.forEach { it.unregister() }
+ callbacksToCleanUp.forEach { cm.unregisterNetworkCallback(it) }
+ reader.handler.post { reader.stop() }
+ Os.close(iface.fileDescriptor.fileDescriptor)
+ handlerThread.quitSafely()
+ }
+
+ private fun requestNetwork(request: NetworkRequest, callback: TestableNetworkCallback) {
+ cm.requestNetwork(request, callback)
+ callbacksToCleanUp.add(callback)
+ }
+
+ private fun makeTestNetworkRequest(specifier: String? = null): NetworkRequest {
+ return NetworkRequest.Builder()
+ .clearCapabilities()
+ .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ .addTransportType(TRANSPORT_TEST)
+ .also {
+ if (specifier != null) {
+ it.setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(specifier))
+ }
+ }
+ .build()
+ }
+
+ private fun createConnectedNetworkAgent(
+ context: Context = realContext,
+ specifier: String? = iface.getInterfaceName(),
+ ): Pair<TestableNetworkAgent, TestableNetworkCallback> {
+ val callback = TestableNetworkCallback()
+ // Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
+ requestNetwork(makeTestNetworkRequest(specifier = specifier), callback)
+
+ val nc = NetworkCapabilities().apply {
+ addTransportType(TRANSPORT_TEST)
+ removeCapability(NET_CAPABILITY_TRUSTED)
+ removeCapability(NET_CAPABILITY_INTERNET)
+ addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NET_CAPABILITY_NOT_VPN)
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ if (null != specifier) {
+ setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(specifier))
+ }
+ }
+ val lp = LinkProperties().apply {
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+ setInterfaceName(iface.getInterfaceName())
+ }
+ val config = NetworkAgentConfig.Builder().build()
+ val agent = TestableNetworkAgent(context, handlerThread.looper, nc, lp, config)
+ agentsToCleanUp.add(agent)
+
+ // Connect the agent and verify initial status callbacks.
+ runAsShell(MANAGE_TEST_NETWORKS) { agent.register() }
+ agent.markConnected()
+ agent.expectCallback<OnNetworkCreated>()
+ agent.expectSignalStrengths(intArrayOf())
+ agent.expectValidationBypassedStatus()
+ val network = agent.network ?: fail("Expected a non-null network")
+ return agent to callback
+ }
+
+ fun ByteArray.toHex(): String = joinToString(separator = "") {
+ eachByte -> "%02x".format(eachByte)
+ }
+
+ fun checkDscpValue(
+ agent : TestableNetworkAgent,
+ callback : TestableNetworkCallback,
+ dscpValue : Int = 0,
+ dstPort : Int = 0,
+ ) {
+ val testString = "test string"
+ val testPacket = ByteBuffer.wrap(testString.toByteArray(Charsets.UTF_8))
+ var packetFound = false
+
+ val socket = Os.socket(AF_INET, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_UDP)
+ agent.network.bindSocket(socket)
+
+ val originalPacket = testPacket.readAsArray()
+ Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size,
+ 0 /* flags */, TEST_TARGET_IPV4_ADDR, dstPort)
+
+ Os.close(socket)
+ generateSequence { reader.poll(PACKET_TIMEOUT_MS) }.forEach { packet ->
+ val buffer = ByteBuffer.wrap(packet, 0, packet.size).order(ByteOrder.BIG_ENDIAN)
+ val ip_ver = buffer.get()
+ val tos = buffer.get()
+ val length = buffer.getShort()
+ val id = buffer.getShort()
+ val offset = buffer.getShort()
+ val ttl = buffer.get()
+ val ipType = buffer.get()
+ val checksum = buffer.getShort()
+
+ val ipAddr = ByteArray(4)
+ buffer.get(ipAddr)
+ val srcIp = Inet4Address.getByAddress(ipAddr);
+ buffer.get(ipAddr)
+ val dstIp = Inet4Address.getByAddress(ipAddr);
+ val packetSrcPort = buffer.getShort().toInt()
+ val packetDstPort = buffer.getShort().toInt()
+
+ // TODO: Add source port comparison.
+ if (srcIp == LOCAL_IPV4_ADDRESS && dstIp == TEST_TARGET_IPV4_ADDR &&
+ packetDstPort == dstPort) {
+ assertEquals(dscpValue, (tos.toInt().shr(2)))
+ packetFound = true
+ }
+ }
+ assertTrue(packetFound)
+ }
+
+ fun doRemovePolicyTest(
+ agent : TestableNetworkAgent,
+ callback : TestableNetworkCallback,
+ policyId : Int
+ ) {
+ val portNumber = 1111 * policyId
+ agent.sendRemoveDscpPolicy(policyId)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(policyId, it.policyId)
+ assertEquals(STATUS_DELETED, it.status)
+ checkDscpValue(agent, callback, dstPort = portNumber)
+ }
+ }
+
+ @Test
+ fun testDscpPolicyAddPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+ val policy = DscpPolicy.Builder(1, 1)
+ .setDestinationPortRange(Range(4444, 4444)).build()
+ agent.sendAddDscpPolicy(policy)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ }
+
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444)
+
+ agent.sendRemoveDscpPolicy(1)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(STATUS_DELETED, it.status)
+ }
+
+ val policy2 = DscpPolicy.Builder(1, 4)
+ .setDestinationPortRange(Range(5555, 5555)).setSourceAddress(LOCAL_IPV4_ADDRESS)
+ .setDestinationAddress(TEST_TARGET_IPV4_ADDR).setProtocol(IPPROTO_UDP).build()
+ agent.sendAddDscpPolicy(policy2)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ }
+
+ checkDscpValue(agent, callback, dscpValue = 4, dstPort = 5555)
+
+ agent.sendRemoveDscpPolicy(1)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(STATUS_DELETED, it.status)
+ }
+ }
+
+ @Test
+ // Remove policies in the same order as addition.
+ fun testRemoveDscpPolicy_RemoveSameOrderAsAdd(): Unit = createConnectedNetworkAgent().let {
+ (agent, callback) ->
+ val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(1111, 1111)).build()
+ agent.sendAddDscpPolicy(policy)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+ }
+
+ val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
+ agent.sendAddDscpPolicy(policy2)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(2, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+ }
+
+ val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
+ agent.sendAddDscpPolicy(policy3)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(3, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+ }
+
+ /* Remove Policies and check CE is no longer set */
+ doRemovePolicyTest(agent, callback, 1)
+ doRemovePolicyTest(agent, callback, 2)
+ doRemovePolicyTest(agent, callback, 3)
+ }
+
+ @Test
+ fun testRemoveDscpPolicy_RemoveImmediatelyAfterAdd(): Unit =
+ createConnectedNetworkAgent().let{ (agent, callback) ->
+ val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(1111, 1111)).build()
+ agent.sendAddDscpPolicy(policy)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+ }
+ doRemovePolicyTest(agent, callback, 1)
+
+ val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
+ agent.sendAddDscpPolicy(policy2)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(2, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+ }
+ doRemovePolicyTest(agent, callback, 2)
+
+ val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
+ agent.sendAddDscpPolicy(policy3)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(3, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+ }
+ doRemovePolicyTest(agent, callback, 3)
+ }
+
+ @Test
+ // Remove policies in reverse order from addition.
+ fun testRemoveDscpPolicy_RemoveReverseOrder(): Unit =
+ createConnectedNetworkAgent().let { (agent, callback) ->
+ val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(1111, 1111)).build()
+ agent.sendAddDscpPolicy(policy)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+ }
+
+ val policy2 = DscpPolicy.Builder(2, 1).setDestinationPortRange(Range(2222, 2222)).build()
+ agent.sendAddDscpPolicy(policy2)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(2, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+ }
+
+ val policy3 = DscpPolicy.Builder(3, 1).setDestinationPortRange(Range(3333, 3333)).build()
+ agent.sendAddDscpPolicy(policy3)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(3, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+ }
+
+ /* Remove Policies and check CE is no longer set */
+ doRemovePolicyTest(agent, callback, 3)
+ doRemovePolicyTest(agent, callback, 2)
+ doRemovePolicyTest(agent, callback, 1)
+ }
+
+ @Test
+ fun testRemoveDscpPolicy_InvalidPolicy(): Unit = createConnectedNetworkAgent().let {
+ (agent, callback) ->
+ agent.sendRemoveDscpPolicy(3)
+ // Is there something to add in TestableNetworkCallback to NOT expect a callback?
+ // Or should we send STATUS_DELETED in any case or a different STATUS?
+ }
+
+ @Test
+ fun testRemoveAllDscpPolicies(): Unit = createConnectedNetworkAgent().let { (agent, callback) ->
+ val policy = DscpPolicy.Builder(1, 1)
+ .setDestinationPortRange(Range(1111, 1111)).build()
+ agent.sendAddDscpPolicy(policy)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 1111)
+ }
+
+ val policy2 = DscpPolicy.Builder(2, 1)
+ .setDestinationPortRange(Range(2222, 2222)).build()
+ agent.sendAddDscpPolicy(policy2)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(2, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 2222)
+ }
+
+ val policy3 = DscpPolicy.Builder(3, 1)
+ .setDestinationPortRange(Range(3333, 3333)).build()
+ agent.sendAddDscpPolicy(policy3)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(3, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 3333)
+ }
+
+ agent.sendRemoveAllDscpPolicies()
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(STATUS_DELETED, it.status)
+ checkDscpValue(agent, callback, dstPort = 1111)
+ }
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(2, it.policyId)
+ assertEquals(STATUS_DELETED, it.status)
+ checkDscpValue(agent, callback, dstPort = 2222)
+ }
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(3, it.policyId)
+ assertEquals(STATUS_DELETED, it.status)
+ checkDscpValue(agent, callback, dstPort = 3333)
+ }
+ }
+
+ @Test
+ fun testAddDuplicateDscpPolicy(): Unit = createConnectedNetworkAgent().let {
+ (agent, callback) ->
+ val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)).build()
+ agent.sendAddDscpPolicy(policy)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 4444)
+ }
+
+ // TODO: send packet on socket and confirm that changing the DSCP policy
+ // updates the mark to the new value.
+
+ val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(5555, 5555)).build()
+ agent.sendAddDscpPolicy(policy2)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(STATUS_SUCCESS, it.status)
+
+ // Sending packet with old policy should fail
+ checkDscpValue(agent, callback, dstPort = 4444)
+ checkDscpValue(agent, callback, dscpValue = 1, dstPort = 5555)
+ }
+
+ agent.sendRemoveDscpPolicy(1)
+ agent.expectCallback<OnDscpPolicyStatusUpdated>().let {
+ assertEquals(1, it.policyId)
+ assertEquals(STATUS_DELETED, it.status)
+ }
+ }
+
+ @Test
+ fun testParcelingDscpPolicyIsLossless(): Unit = createConnectedNetworkAgent().let {
+ (agent, callback) ->
+ // Check that policy with partial parameters is lossless.
+ val policy = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444)).build()
+ assertParcelingIsLossless(policy);
+
+ // Check that policy with all parameters is lossless.
+ val policy2 = DscpPolicy.Builder(1, 1).setDestinationPortRange(Range(4444, 4444))
+ .setSourceAddress(LOCAL_IPV4_ADDRESS)
+ .setDestinationAddress(TEST_TARGET_IPV4_ADDR)
+ .setProtocol(IPPROTO_UDP).build()
+ assertParcelingIsLossless(policy2);
+ }
+}
+
+private fun ByteBuffer.readAsArray(): ByteArray {
+ val out = ByteArray(remaining())
+ get(out)
+ return out
+}
+
+private fun <T> Context.assertHasService(manager: Class<T>): T {
+ return getSystemService(manager) ?: fail("Service $manager not found")
+}
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/LocalSocketTest.java b/tests/cts/net/src/android/net/cts/LocalSocketTest.java
deleted file mode 100644
index 6e61705..0000000
--- a/tests/cts/net/src/android/net/cts/LocalSocketTest.java
+++ /dev/null
@@ -1,470 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.cts;
-
-import junit.framework.TestCase;
-
-import android.net.Credentials;
-import android.net.LocalServerSocket;
-import android.net.LocalSocket;
-import android.net.LocalSocketAddress;
-import android.system.Os;
-import android.system.OsConstants;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-
-public class LocalSocketTest extends TestCase {
- private final static String ADDRESS_PREFIX = "com.android.net.LocalSocketTest";
-
- public void testLocalConnections() throws IOException {
- String address = ADDRESS_PREFIX + "_testLocalConnections";
- // create client and server socket
- LocalServerSocket localServerSocket = new LocalServerSocket(address);
- LocalSocket clientSocket = new LocalSocket();
-
- // establish connection between client and server
- LocalSocketAddress locSockAddr = new LocalSocketAddress(address);
- assertFalse(clientSocket.isConnected());
- clientSocket.connect(locSockAddr);
- assertTrue(clientSocket.isConnected());
-
- LocalSocket serverSocket = localServerSocket.accept();
- assertTrue(serverSocket.isConnected());
- assertTrue(serverSocket.isBound());
- try {
- serverSocket.bind(localServerSocket.getLocalSocketAddress());
- fail("Cannot bind a LocalSocket from accept()");
- } catch (IOException expected) {
- }
- try {
- serverSocket.connect(locSockAddr);
- fail("Cannot connect a LocalSocket from accept()");
- } catch (IOException expected) {
- }
-
- Credentials credent = clientSocket.getPeerCredentials();
- assertTrue(0 != credent.getPid());
-
- // send data from client to server
- OutputStream clientOutStream = clientSocket.getOutputStream();
- clientOutStream.write(12);
- InputStream serverInStream = serverSocket.getInputStream();
- assertEquals(12, serverInStream.read());
-
- //send data from server to client
- OutputStream serverOutStream = serverSocket.getOutputStream();
- serverOutStream.write(3);
- InputStream clientInStream = clientSocket.getInputStream();
- assertEquals(3, clientInStream.read());
-
- // Test sending and receiving file descriptors
- clientSocket.setFileDescriptorsForSend(new FileDescriptor[]{FileDescriptor.in});
- clientOutStream.write(32);
- assertEquals(32, serverInStream.read());
-
- FileDescriptor[] out = serverSocket.getAncillaryFileDescriptors();
- assertEquals(1, out.length);
- FileDescriptor fd = clientSocket.getFileDescriptor();
- assertTrue(fd.valid());
-
- //shutdown input stream of client
- clientSocket.shutdownInput();
- assertEquals(-1, clientInStream.read());
-
- //shutdown output stream of client
- clientSocket.shutdownOutput();
- try {
- clientOutStream.write(10);
- fail("testLocalSocket shouldn't come to here");
- } catch (IOException e) {
- // expected
- }
-
- //shutdown input stream of server
- serverSocket.shutdownInput();
- assertEquals(-1, serverInStream.read());
-
- //shutdown output stream of server
- serverSocket.shutdownOutput();
- try {
- serverOutStream.write(10);
- fail("testLocalSocket shouldn't come to here");
- } catch (IOException e) {
- // expected
- }
-
- //close client socket
- clientSocket.close();
- try {
- clientInStream.read();
- fail("testLocalSocket shouldn't come to here");
- } catch (IOException e) {
- // expected
- }
-
- //close server socket
- serverSocket.close();
- try {
- serverInStream.read();
- fail("testLocalSocket shouldn't come to here");
- } catch (IOException e) {
- // expected
- }
- }
-
- public void testAccessors() throws IOException {
- String address = ADDRESS_PREFIX + "_testAccessors";
- LocalSocket socket = new LocalSocket();
- LocalSocketAddress addr = new LocalSocketAddress(address);
-
- assertFalse(socket.isBound());
- socket.bind(addr);
- assertTrue(socket.isBound());
- assertEquals(addr, socket.getLocalSocketAddress());
-
- String str = socket.toString();
- assertTrue(str.contains("impl:android.net.LocalSocketImpl"));
-
- socket.setReceiveBufferSize(1999);
- assertEquals(1999 << 1, socket.getReceiveBufferSize());
-
- socket.setSendBufferSize(3998);
- assertEquals(3998 << 1, socket.getSendBufferSize());
-
- assertEquals(0, socket.getSoTimeout());
- socket.setSoTimeout(1996);
- assertTrue(socket.getSoTimeout() > 0);
-
- try {
- socket.getRemoteSocketAddress();
- fail("testLocalSocketSecondary shouldn't come to here");
- } catch (UnsupportedOperationException e) {
- // expected
- }
-
- try {
- socket.isClosed();
- fail("testLocalSocketSecondary shouldn't come to here");
- } catch (UnsupportedOperationException e) {
- // expected
- }
-
- try {
- socket.isInputShutdown();
- fail("testLocalSocketSecondary shouldn't come to here");
- } catch (UnsupportedOperationException e) {
- // expected
- }
-
- try {
- socket.isOutputShutdown();
- fail("testLocalSocketSecondary shouldn't come to here");
- } catch (UnsupportedOperationException e) {
- // expected
- }
-
- try {
- socket.connect(addr, 2005);
- fail("testLocalSocketSecondary shouldn't come to here");
- } catch (UnsupportedOperationException e) {
- // expected
- }
-
- socket.close();
- }
-
- // http://b/31205169
- public void testSetSoTimeout_readTimeout() throws Exception {
- String address = ADDRESS_PREFIX + "_testSetSoTimeout_readTimeout";
-
- try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
- final LocalSocket clientSocket = socketPair.clientSocket;
-
- // Set the timeout in millis.
- int timeoutMillis = 1000;
- clientSocket.setSoTimeout(timeoutMillis);
-
- // Avoid blocking the test run if timeout doesn't happen by using a separate thread.
- Callable<Result> reader = () -> {
- try {
- clientSocket.getInputStream().read();
- return Result.noException("Did not block");
- } catch (IOException e) {
- return Result.exception(e);
- }
- };
- // Allow the configured timeout, plus some slop.
- int allowedTime = timeoutMillis + 2000;
- Result result = runInSeparateThread(allowedTime, reader);
-
- // Check the message was a timeout, it's all we have to go on.
- String expectedMessage = Os.strerror(OsConstants.EAGAIN);
- result.assertThrewIOException(expectedMessage);
- }
- }
-
- // http://b/31205169
- public void testSetSoTimeout_writeTimeout() throws Exception {
- String address = ADDRESS_PREFIX + "_testSetSoTimeout_writeTimeout";
-
- try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
- final LocalSocket clientSocket = socketPair.clientSocket;
-
- // Set the timeout in millis.
- int timeoutMillis = 1000;
- clientSocket.setSoTimeout(timeoutMillis);
-
- // Set a small buffer size so we know we can flood it.
- clientSocket.setSendBufferSize(100);
- final int bufferSize = clientSocket.getSendBufferSize();
-
- // Avoid blocking the test run if timeout doesn't happen by using a separate thread.
- Callable<Result> writer = () -> {
- try {
- byte[] toWrite = new byte[bufferSize * 2];
- clientSocket.getOutputStream().write(toWrite);
- return Result.noException("Did not block");
- } catch (IOException e) {
- return Result.exception(e);
- }
- };
- // Allow the configured timeout, plus some slop.
- int allowedTime = timeoutMillis + 2000;
-
- Result result = runInSeparateThread(allowedTime, writer);
-
- // Check the message was a timeout, it's all we have to go on.
- String expectedMessage = Os.strerror(OsConstants.EAGAIN);
- result.assertThrewIOException(expectedMessage);
- }
- }
-
- public void testAvailable() throws Exception {
- String address = ADDRESS_PREFIX + "_testAvailable";
-
- try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
- LocalSocket clientSocket = socketPair.clientSocket;
- LocalSocket serverSocket = socketPair.serverSocket.accept();
-
- OutputStream clientOutputStream = clientSocket.getOutputStream();
- InputStream serverInputStream = serverSocket.getInputStream();
- assertEquals(0, serverInputStream.available());
-
- byte[] buffer = new byte[50];
- clientOutputStream.write(buffer);
- assertEquals(50, serverInputStream.available());
-
- InputStream clientInputStream = clientSocket.getInputStream();
- OutputStream serverOutputStream = serverSocket.getOutputStream();
- assertEquals(0, clientInputStream.available());
- serverOutputStream.write(buffer);
- assertEquals(50, serverInputStream.available());
-
- serverSocket.close();
- }
- }
-
- // http://b/34095140
- public void testLocalSocketCreatedFromFileDescriptor() throws Exception {
- String address = ADDRESS_PREFIX + "_testLocalSocketCreatedFromFileDescriptor";
-
- // Establish connection between a local client and server to get a valid client socket file
- // descriptor.
- try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
- // Extract the client FileDescriptor we can use.
- FileDescriptor fileDescriptor = socketPair.clientSocket.getFileDescriptor();
- assertTrue(fileDescriptor.valid());
-
- // Create the LocalSocket we want to test.
- LocalSocket clientSocketCreatedFromFileDescriptor =
- LocalSocket.createConnectedLocalSocket(fileDescriptor);
- assertTrue(clientSocketCreatedFromFileDescriptor.isConnected());
- assertTrue(clientSocketCreatedFromFileDescriptor.isBound());
-
- // Test the LocalSocket can be used for communication.
- LocalSocket serverSocket = socketPair.serverSocket.accept();
- OutputStream clientOutputStream =
- clientSocketCreatedFromFileDescriptor.getOutputStream();
- InputStream serverInputStream = serverSocket.getInputStream();
-
- clientOutputStream.write(12);
- assertEquals(12, serverInputStream.read());
-
- // Closing clientSocketCreatedFromFileDescriptor does not close the file descriptor.
- clientSocketCreatedFromFileDescriptor.close();
- assertTrue(fileDescriptor.valid());
-
- // .. while closing the LocalSocket that owned the file descriptor does.
- socketPair.clientSocket.close();
- assertFalse(fileDescriptor.valid());
- }
- }
-
- public void testFlush() throws Exception {
- String address = ADDRESS_PREFIX + "_testFlush";
-
- try (LocalSocketPair socketPair = LocalSocketPair.createConnectedSocketPair(address)) {
- LocalSocket clientSocket = socketPair.clientSocket;
- LocalSocket serverSocket = socketPair.serverSocket.accept();
-
- OutputStream clientOutputStream = clientSocket.getOutputStream();
- InputStream serverInputStream = serverSocket.getInputStream();
- testFlushWorks(clientOutputStream, serverInputStream);
-
- OutputStream serverOutputStream = serverSocket.getOutputStream();
- InputStream clientInputStream = clientSocket.getInputStream();
- testFlushWorks(serverOutputStream, clientInputStream);
-
- serverSocket.close();
- }
- }
-
- private void testFlushWorks(OutputStream outputStream, InputStream inputStream)
- throws Exception {
- final int bytesToTransfer = 50;
- StreamReader inputStreamReader = new StreamReader(inputStream, bytesToTransfer);
-
- byte[] buffer = new byte[bytesToTransfer];
- outputStream.write(buffer);
- assertEquals(bytesToTransfer, inputStream.available());
-
- // Start consuming the data.
- inputStreamReader.start();
-
- // This doesn't actually flush any buffers, it just polls until the reader has read all the
- // bytes.
- outputStream.flush();
-
- inputStreamReader.waitForCompletion(5000);
- inputStreamReader.assertBytesRead(bytesToTransfer);
- assertEquals(0, inputStream.available());
- }
-
- private static class StreamReader extends Thread {
- private final InputStream is;
- private final int expectedByteCount;
- private final CountDownLatch completeLatch = new CountDownLatch(1);
-
- private volatile Exception exception;
- private int bytesRead;
-
- private StreamReader(InputStream is, int expectedByteCount) {
- this.is = is;
- this.expectedByteCount = expectedByteCount;
- }
-
- @Override
- public void run() {
- try {
- byte[] buffer = new byte[10];
- int readCount;
- while ((readCount = is.read(buffer)) >= 0) {
- bytesRead += readCount;
- if (bytesRead >= expectedByteCount) {
- break;
- }
- }
- } catch (IOException e) {
- exception = e;
- } finally {
- completeLatch.countDown();
- }
- }
-
- public void waitForCompletion(long waitMillis) throws Exception {
- if (!completeLatch.await(waitMillis, TimeUnit.MILLISECONDS)) {
- fail("Timeout waiting for completion");
- }
- if (exception != null) {
- throw new Exception("Read failed", exception);
- }
- }
-
- public void assertBytesRead(int expected) {
- assertEquals(expected, bytesRead);
- }
- }
-
- private static class Result {
- private final String type;
- private final Exception e;
-
- private Result(String type, Exception e) {
- this.type = type;
- this.e = e;
- }
-
- static Result noException(String description) {
- return new Result(description, null);
- }
-
- static Result exception(Exception e) {
- return new Result(e.getClass().getName(), e);
- }
-
- void assertThrewIOException(String expectedMessage) {
- assertEquals("Unexpected result type", IOException.class.getName(), type);
- assertEquals("Unexpected exception message", expectedMessage, e.getMessage());
- }
- }
-
- private static Result runInSeparateThread(int allowedTime, final Callable<Result> callable)
- throws Exception {
- ExecutorService service = Executors.newSingleThreadScheduledExecutor();
- Future<Result> future = service.submit(callable);
- Result result = future.get(allowedTime, TimeUnit.MILLISECONDS);
- if (!future.isDone()) {
- fail("Worker thread appears blocked");
- }
- return result;
- }
-
- private static class LocalSocketPair implements AutoCloseable {
- static LocalSocketPair createConnectedSocketPair(String address) throws Exception {
- LocalServerSocket localServerSocket = new LocalServerSocket(address);
- final LocalSocket clientSocket = new LocalSocket();
-
- // Establish connection between client and server
- LocalSocketAddress locSockAddr = new LocalSocketAddress(address);
- clientSocket.connect(locSockAddr);
- assertTrue(clientSocket.isConnected());
- return new LocalSocketPair(localServerSocket, clientSocket);
- }
-
- final LocalServerSocket serverSocket;
- final LocalSocket clientSocket;
-
- LocalSocketPair(LocalServerSocket serverSocket, LocalSocket clientSocket) {
- this.serverSocket = serverSocket;
- this.clientSocket = clientSocket;
- }
-
- public void close() throws Exception {
- serverSocket.close();
- clientSocket.close();
- }
- }
-}
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 7b5b44f..4d4e7b9 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -53,6 +53,7 @@
// android_library does not include JNI libs: include NetworkStack dependencies here
"libnativehelper_compat_libc++",
"libnetworkstackutilsjni",
+ "libcom_android_connectivity_com_android_net_module_util_jni",
],
jarjar_rules: ":connectivity-jarjar-rules",
}
diff --git a/tests/mts/Android.bp b/tests/mts/Android.bp
index a56f76e..2c44010 100644
--- a/tests/mts/Android.bp
+++ b/tests/mts/Android.bp
@@ -23,6 +23,9 @@
"general-tests",
"mts-tethering",
],
+ defaults: [
+ "connectivity-mainline-presubmit-cc-defaults",
+ ],
require_root: true,
static_libs: [
"libbase",
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 48751f4..25b391a 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -23,6 +23,7 @@
name: "FrameworksNetTests-jni-defaults",
jni_libs: [
"ld-android",
+ "libandroid_net_frameworktests_util_jni",
"libbase",
"libbinder",
"libbpf_bcc",
@@ -38,8 +39,8 @@
"liblog",
"liblzma",
"libnativehelper",
- "libnetdbpf",
"libnetdutils",
+ "libnetworkstats",
"libnetworkstatsfactorytestjni",
"libpackagelistparser",
"libpcre2",
@@ -74,6 +75,7 @@
"java/android/net/TelephonyNetworkSpecifierTest.java",
"java/android/net/VpnManagerTest.java",
"java/android/net/ipmemorystore/*.java",
+ "java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt",
"java/android/net/nsd/*.java",
"java/com/android/internal/net/NetworkUtilsInternalTest.java",
"java/com/android/internal/net/VpnProfileTest.java",
@@ -88,7 +90,9 @@
"java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
"java/com/android/server/connectivity/VpnTest.java",
"java/com/android/server/net/ipmemorystore/*.java",
+ "java/com/android/server/net/BpfInterfaceMapUpdaterTest.java",
"java/com/android/server/net/NetworkStats*.java",
+ "java/com/android/server/net/TestableUsageCallback.kt",
]
}
@@ -141,6 +145,7 @@
"ServiceConnectivityResources",
],
visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
+ exclude_kotlinc_generated_files: false,
}
android_test {
diff --git a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
index 6e51069..561e621 100644
--- a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
+++ b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java
@@ -240,6 +240,27 @@
assertFalse(stats.hasNextBucket());
}
+
+ @Test
+ public void testQueryDetailsForDevice() throws Exception {
+ final long startTime = 1;
+ final long endTime = 100;
+
+ reset(mStatsSession);
+ when(mService.openSessionForUsageStats(anyInt(), anyString())).thenReturn(mStatsSession);
+ when(mStatsSession.getHistoryIntervalForNetwork(any(NetworkTemplate.class),
+ anyInt(), anyLong(), anyLong()))
+ .thenReturn(new NetworkStatsHistory(10, 0));
+ final NetworkTemplate template = new NetworkTemplate.Builder(NetworkTemplate.MATCH_MOBILE)
+ .setMeteredness(NetworkStats.Bucket.METERED_YES).build();
+ NetworkStats stats = mManager.queryDetailsForDevice(template, startTime, endTime);
+
+ verify(mStatsSession, times(1)).getHistoryIntervalForNetwork(
+ eq(template), eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime));
+
+ assertFalse(stats.hasNextBucket());
+ }
+
private void assertBucketMatches(Entry expected, NetworkStats.Bucket actual) {
assertEquals(expected.uid, actual.getUid());
assertEquals(expected.rxBytes, actual.getRxBytes());
diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt
index b1ffc92..4b2d874 100644
--- a/tests/unit/java/android/net/NetworkIdentityTest.kt
+++ b/tests/unit/java/android/net/NetworkIdentityTest.kt
@@ -17,11 +17,16 @@
package android.net
import android.content.Context
+import android.net.ConnectivityManager.MAX_NETWORK_TYPE
+import android.net.ConnectivityManager.TYPE_ETHERNET
import android.net.ConnectivityManager.TYPE_MOBILE
+import android.net.ConnectivityManager.TYPE_NONE
+import android.net.ConnectivityManager.TYPE_WIFI
import android.net.NetworkIdentity.OEM_NONE
import android.net.NetworkIdentity.OEM_PAID
import android.net.NetworkIdentity.OEM_PRIVATE
import android.net.NetworkIdentity.getOemBitfield
+import android.app.usage.NetworkStatsManager
import android.telephony.TelephonyManager
import android.os.Build
import com.android.testutils.DevSdkIgnoreRule
@@ -30,10 +35,12 @@
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue
private const val TEST_IMSI = "testimsi"
+private const val TEST_WIFI_KEY = "testwifikey"
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
@@ -74,12 +81,12 @@
}
@Test
- fun testGetMetered() {
+ fun testIsMetered() {
// Verify network is metered.
val netIdent1 = NetworkIdentity.buildNetworkIdentity(mockContext,
buildMobileNetworkStateSnapshot(NetworkCapabilities(), TEST_IMSI),
false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
- assertTrue(netIdent1.getMetered())
+ assertTrue(netIdent1.isMetered())
// Verify network is not metered because it has NET_CAPABILITY_NOT_METERED capability.
val capsNotMetered = NetworkCapabilities.Builder().apply {
@@ -88,7 +95,7 @@
val netIdent2 = NetworkIdentity.buildNetworkIdentity(mockContext,
buildMobileNetworkStateSnapshot(capsNotMetered, TEST_IMSI),
false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
- assertFalse(netIdent2.getMetered())
+ assertFalse(netIdent2.isMetered())
// Verify network is not metered because it has NET_CAPABILITY_TEMPORARILY_NOT_METERED
// capability .
@@ -98,6 +105,121 @@
val netIdent3 = NetworkIdentity.buildNetworkIdentity(mockContext,
buildMobileNetworkStateSnapshot(capsTempNotMetered, TEST_IMSI),
false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
- assertFalse(netIdent3.getMetered())
+ assertFalse(netIdent3.isMetered())
+ }
+
+ @Test
+ fun testBuilder() {
+ val oemPrivateRoamingNotMeteredCap = NetworkCapabilities().apply {
+ addCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE)
+ addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
+ }
+ val identFromSnapshot = NetworkIdentity.Builder().setNetworkStateSnapshot(
+ buildMobileNetworkStateSnapshot(oemPrivateRoamingNotMeteredCap, TEST_IMSI))
+ .setDefaultNetwork(true)
+ .setRatType(TelephonyManager.NETWORK_TYPE_UMTS)
+ .build()
+ val identFromLegacyBuild = NetworkIdentity.buildNetworkIdentity(mockContext,
+ buildMobileNetworkStateSnapshot(oemPrivateRoamingNotMeteredCap, TEST_IMSI),
+ true /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+ val identFromConstructor = NetworkIdentity(TYPE_MOBILE,
+ TelephonyManager.NETWORK_TYPE_UMTS,
+ TEST_IMSI,
+ null /* wifiNetworkKey */,
+ true /* roaming */,
+ false /* metered */,
+ true /* defaultNetwork */,
+ NetworkTemplate.OEM_MANAGED_PRIVATE)
+ assertEquals(identFromLegacyBuild, identFromSnapshot)
+ assertEquals(identFromConstructor, identFromSnapshot)
+
+ // Assert non-wifi can't have wifi network key.
+ assertFailsWith<IllegalArgumentException> {
+ NetworkIdentity.Builder()
+ .setType(TYPE_ETHERNET)
+ .setWifiNetworkKey(TEST_WIFI_KEY)
+ .build()
+ }
+
+ // Assert non-mobile can't have ratType.
+ assertFailsWith<IllegalArgumentException> {
+ NetworkIdentity.Builder()
+ .setType(TYPE_WIFI)
+ .setRatType(TelephonyManager.NETWORK_TYPE_LTE)
+ .build()
+ }
+ }
+
+ @Test
+ fun testBuilder_type() {
+ // Assert illegal type values cannot make an identity.
+ listOf(Integer.MIN_VALUE, TYPE_NONE - 1, MAX_NETWORK_TYPE + 1, Integer.MAX_VALUE)
+ .forEach { type ->
+ assertFailsWith<IllegalArgumentException> {
+ NetworkIdentity.Builder().setType(type).build()
+ }
+ }
+
+ // Verify legitimate type values can make an identity.
+ for (type in TYPE_NONE..MAX_NETWORK_TYPE) {
+ NetworkIdentity.Builder().setType(type).build().also {
+ assertEquals(it.type, type)
+ }
+ }
+ }
+
+ @Test
+ fun testBuilder_ratType() {
+ // Assert illegal ratTypes cannot make an identity.
+ listOf(Integer.MIN_VALUE, NetworkTemplate.NETWORK_TYPE_ALL,
+ NetworkStatsManager.NETWORK_TYPE_5G_NSA - 1, Integer.MAX_VALUE)
+ .forEach {
+ assertFailsWith<IllegalArgumentException> {
+ NetworkIdentity.Builder()
+ .setType(TYPE_MOBILE)
+ .setRatType(it)
+ .build()
+ }
+ }
+
+ // Verify legitimate ratTypes can make an identity.
+ TelephonyManager.getAllNetworkTypes().toMutableList().also {
+ it.add(TelephonyManager.NETWORK_TYPE_UNKNOWN)
+ it.add(NetworkStatsManager.NETWORK_TYPE_5G_NSA)
+ }.forEach { rat ->
+ NetworkIdentity.Builder()
+ .setType(TYPE_MOBILE)
+ .setRatType(rat)
+ .build().also {
+ assertEquals(it.ratType, rat)
+ }
+ }
+ }
+
+ @Test
+ fun testBuilder_oemManaged() {
+ // Assert illegal oemManage values cannot make an identity.
+ listOf(Integer.MIN_VALUE, NetworkTemplate.OEM_MANAGED_ALL, NetworkTemplate.OEM_MANAGED_YES,
+ Integer.MAX_VALUE)
+ .forEach { oemManaged ->
+ assertFailsWith<IllegalArgumentException> {
+ NetworkIdentity.Builder()
+ .setType(TYPE_MOBILE)
+ .setOemManaged(oemManaged)
+ .build()
+ }
+ }
+
+ // Verify legitimate oem managed values can make an identity.
+ listOf(NetworkTemplate.OEM_MANAGED_NO, NetworkTemplate.OEM_MANAGED_PAID,
+ NetworkTemplate.OEM_MANAGED_PRIVATE, NetworkTemplate.OEM_MANAGED_PAID or
+ NetworkTemplate.OEM_MANAGED_PRIVATE)
+ .forEach { oemManaged ->
+ NetworkIdentity.Builder()
+ .setOemManaged(oemManaged)
+ .build().also {
+ assertEquals(it.oemManaged, oemManaged)
+ }
+ }
}
}
diff --git a/tests/unit/java/android/net/NetworkStatsAccessTest.java b/tests/unit/java/android/net/NetworkStatsAccessTest.java
index e4fc118..97a93ca 100644
--- a/tests/unit/java/android/net/NetworkStatsAccessTest.java
+++ b/tests/unit/java/android/net/NetworkStatsAccessTest.java
@@ -16,6 +16,8 @@
package android.net;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;
@@ -25,7 +27,6 @@
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.pm.PackageManager;
-import android.os.Build;
import android.telephony.TelephonyManager;
import androidx.test.filters.SmallTest;
@@ -42,7 +43,7 @@
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
public class NetworkStatsAccessTest {
private static final String TEST_PKG = "com.example.test";
private static final int TEST_PID = 1234;
diff --git a/tests/unit/java/android/net/NetworkStatsCollectionTest.java b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
index 1c557d6..c27ee93 100644
--- a/tests/unit/java/android/net/NetworkStatsCollectionTest.java
+++ b/tests/unit/java/android/net/NetworkStatsCollectionTest.java
@@ -29,6 +29,7 @@
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.MiscAsserts.assertThrows;
import static org.junit.Assert.assertArrayEquals;
@@ -37,12 +38,13 @@
import static org.junit.Assert.fail;
import android.content.res.Resources;
-import android.os.Build;
+import android.net.NetworkStatsCollection.Key;
import android.os.Process;
import android.os.UserHandle;
import android.telephony.SubscriptionPlan;
import android.telephony.TelephonyManager;
import android.text.format.DateUtils;
+import android.util.ArrayMap;
import android.util.RecurrenceRule;
import androidx.test.InstrumentationRegistry;
@@ -59,6 +61,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mockito;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -72,13 +75,14 @@
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
/**
* Tests for {@link NetworkStatsCollection}.
*/
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
public class NetworkStatsCollectionTest {
private static final String TEST_FILE = "test.bin";
@@ -195,8 +199,8 @@
// record empty data straddling between buckets
final NetworkStats.Entry entry = new NetworkStats.Entry();
entry.rxBytes = 32;
- collection.recordData(null, UID_ALL, SET_DEFAULT, TAG_NONE, 30 * MINUTE_IN_MILLIS,
- 90 * MINUTE_IN_MILLIS, entry);
+ collection.recordData(Mockito.mock(NetworkIdentitySet.class), UID_ALL, SET_DEFAULT,
+ TAG_NONE, 30 * MINUTE_IN_MILLIS, 90 * MINUTE_IN_MILLIS, entry);
// assert that we report boundary in atomic buckets
assertEquals(0, collection.getStartMillis());
@@ -529,6 +533,52 @@
assertThrows(ArithmeticException.class, () -> multiplySafeByRational(30, 3, 0));
}
+ @Test
+ public void testBuilder() {
+ final Map<Key, NetworkStatsHistory> expectedEntries = new ArrayMap<>();
+ final NetworkStats.Entry entry = new NetworkStats.Entry();
+ final NetworkIdentitySet ident = new NetworkIdentitySet();
+ final Key key1 = new Key(ident, 0, 0, 0);
+ final Key key2 = new Key(ident, 1, 0, 0);
+ final long bucketDuration = 10;
+
+ final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 10, 40,
+ 4, 50, 5, 60);
+ final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 10, 3,
+ 41, 7, 1, 0);
+
+ NetworkStatsHistory history1 = new NetworkStatsHistory.Builder(10, 5)
+ .addEntry(entry1)
+ .addEntry(entry2)
+ .build();
+
+ NetworkStatsHistory history2 = new NetworkStatsHistory(10, 5);
+
+ NetworkStatsCollection actualCollection = new NetworkStatsCollection.Builder(bucketDuration)
+ .addEntry(key1, history1)
+ .addEntry(key2, history2)
+ .build();
+
+ // The builder will omit any entry with empty history. Thus, history2
+ // is not expected in the result collection.
+ expectedEntries.put(key1, history1);
+
+ final Map<Key, NetworkStatsHistory> actualEntries = actualCollection.getEntries();
+
+ assertEquals(expectedEntries.size(), actualEntries.size());
+ for (Key expectedKey : expectedEntries.keySet()) {
+ final NetworkStatsHistory expectedHistory = expectedEntries.get(expectedKey);
+
+ final NetworkStatsHistory actualHistory = actualEntries.get(expectedKey);
+ assertNotNull(actualHistory);
+
+ assertEquals(expectedHistory.getEntries(), actualHistory.getEntries());
+
+ actualEntries.remove(expectedKey);
+ }
+ assertEquals(0, actualEntries.size());
+ }
+
/**
* Copy a {@link Resources#openRawResource(int)} into {@link File} for
* testing purposes.
@@ -586,6 +636,14 @@
actual.txBytes, actual.txPackets, 0L));
}
+ private static void assertEntry(NetworkStatsHistory.Entry expected,
+ NetworkStatsHistory.Entry actual) {
+ assertEntry(new NetworkStats.Entry(actual.rxBytes, actual.rxPackets,
+ actual.txBytes, actual.txPackets, 0L),
+ new NetworkStats.Entry(actual.rxBytes, actual.rxPackets,
+ actual.txBytes, actual.txPackets, 0L));
+ }
+
private static void assertEntry(NetworkStats.Entry expected,
NetworkStats.Entry actual) {
assertEquals("unexpected rxBytes", expected.rxBytes, actual.rxBytes);
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index c5f8c00..c170605 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -56,6 +56,7 @@
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
+import java.util.List;
import java.util.Random;
@RunWith(DevSdkIgnoreRunner.class)
@@ -532,6 +533,40 @@
assertEquals(512L + 4096L, stats.getTotalBytes());
}
+ @Test
+ public void testBuilder() {
+ final NetworkStatsHistory.Entry entry1 = new NetworkStatsHistory.Entry(10, 30, 40,
+ 4, 50, 5, 60);
+ final NetworkStatsHistory.Entry entry2 = new NetworkStatsHistory.Entry(30, 15, 3,
+ 41, 7, 1, 0);
+ final NetworkStatsHistory.Entry entry3 = new NetworkStatsHistory.Entry(7, 301, 11,
+ 14, 31, 2, 80);
+
+ final NetworkStatsHistory statsEmpty = new NetworkStatsHistory
+ .Builder(HOUR_IN_MILLIS, 10).build();
+ assertEquals(0, statsEmpty.getEntries().size());
+ assertEquals(HOUR_IN_MILLIS, statsEmpty.getBucketDuration());
+
+ NetworkStatsHistory statsSingle = new NetworkStatsHistory
+ .Builder(HOUR_IN_MILLIS, 8)
+ .addEntry(entry1)
+ .build();
+ assertEquals(1, statsSingle.getEntries().size());
+ assertEquals(HOUR_IN_MILLIS, statsSingle.getBucketDuration());
+ assertEquals(entry1, statsSingle.getEntries().get(0));
+
+ NetworkStatsHistory statsMultiple = new NetworkStatsHistory
+ .Builder(SECOND_IN_MILLIS, 0)
+ .addEntry(entry1).addEntry(entry2).addEntry(entry3)
+ .build();
+ final List<NetworkStatsHistory.Entry> entries = statsMultiple.getEntries();
+ assertEquals(3, entries.size());
+ assertEquals(SECOND_IN_MILLIS, statsMultiple.getBucketDuration());
+ assertEquals(entry1, entries.get(0));
+ assertEquals(entry2, entries.get(1));
+ assertEquals(entry3, entries.get(2));
+ }
+
private static void assertIndexBeforeAfter(
NetworkStatsHistory stats, int before, int after, long time) {
assertEquals("unexpected before", before, stats.getIndexBefore(time));
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index 15db45c..453612f 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -16,13 +16,13 @@
package android.net
+import android.app.usage.NetworkStatsManager.NETWORK_TYPE_5G_NSA
import android.content.Context
import android.net.ConnectivityManager.TYPE_MOBILE
import android.net.ConnectivityManager.TYPE_WIFI
import android.net.NetworkIdentity.OEM_NONE
import android.net.NetworkIdentity.OEM_PAID
import android.net.NetworkIdentity.OEM_PRIVATE
-import android.net.NetworkIdentity.SUBTYPE_COMBINED
import android.net.NetworkIdentity.buildNetworkIdentity
import android.net.NetworkStats.DEFAULT_NETWORK_ALL
import android.net.NetworkStats.METERED_ALL
@@ -37,7 +37,6 @@
import android.net.NetworkTemplate.MATCH_PROXY
import android.net.NetworkTemplate.MATCH_WIFI
import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD
-import android.net.NetworkTemplate.NETWORK_TYPE_5G_NSA
import android.net.NetworkTemplate.NETWORK_TYPE_ALL
import android.net.NetworkTemplate.OEM_MANAGED_ALL
import android.net.NetworkTemplate.OEM_MANAGED_NO
@@ -57,6 +56,7 @@
import com.android.net.module.util.NetworkStatsUtils.SUBSCRIBER_ID_MATCH_RULE_EXACT
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.SC_V2
import com.android.testutils.assertParcelSane
import org.junit.Before
import org.junit.Test
@@ -95,7 +95,7 @@
oemManaged: Int = OEM_NONE,
metered: Boolean = true
): NetworkStateSnapshot {
- `when`(mockWifiInfo.getCurrentNetworkKey()).thenReturn(wifiKey)
+ `when`(mockWifiInfo.getNetworkKey()).thenReturn(wifiKey)
val lp = LinkProperties()
val caps = NetworkCapabilities().apply {
setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, !metered)
@@ -312,7 +312,7 @@
val identLteMetered = buildNetworkIdentity(
mockContext, stateMobileImsi1Metered, false, TelephonyManager.NETWORK_TYPE_LTE)
val identCombinedMetered = buildNetworkIdentity(
- mockContext, stateMobileImsi1Metered, false, SUBTYPE_COMBINED)
+ mockContext, stateMobileImsi1Metered, false, NetworkTemplate.NETWORK_TYPE_ALL)
val identImsi2UmtsMetered = buildNetworkIdentity(mockContext,
buildMobileNetworkState(TEST_IMSI2), false, TelephonyManager.NETWORK_TYPE_UMTS)
val identWifi = buildNetworkIdentity(
@@ -326,7 +326,7 @@
val identLteNonMetered = buildNetworkIdentity(
mockContext, stateMobileImsi1NonMetered, false, TelephonyManager.NETWORK_TYPE_LTE)
val identCombinedNonMetered = buildNetworkIdentity(
- mockContext, stateMobileImsi1NonMetered, false, SUBTYPE_COMBINED)
+ mockContext, stateMobileImsi1NonMetered, false, NetworkTemplate.NETWORK_TYPE_ALL)
val identImsi2UmtsNonMetered = buildNetworkIdentity(mockContext,
stateMobileImsi2NonMetered, false, TelephonyManager.NETWORK_TYPE_UMTS)
@@ -556,7 +556,7 @@
}
}
- @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S)
+ @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
@Test
fun testBuilderMatchRules() {
// Verify unknown match rules cannot construct templates.
@@ -657,7 +657,7 @@
}
}
- @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S)
+ @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
@Test
fun testBuilderWifiNetworkKeys() {
// Verify template builder which generates same template with the given different
diff --git a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
new file mode 100644
index 0000000..e4943ea
--- /dev/null
+++ b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
@@ -0,0 +1,112 @@
+/*
+ * 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 android.net.netstats
+
+import android.net.NetworkStatsCollection
+import androidx.test.InstrumentationRegistry
+import androidx.test.filters.SmallTest
+import com.android.frameworks.tests.net.R
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.SC_V2
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import java.io.DataInputStream
+import java.net.ProtocolException
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.fail
+
+private const val BUCKET_DURATION_MS = 2 * 60 * 60 * 1000L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+class NetworkStatsDataMigrationUtilsTest {
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun testReadPlatformCollection() {
+ // Verify the method throws for wrong file version.
+ assertFailsWith<ProtocolException> {
+ NetworkStatsDataMigrationUtils.readPlatformCollection(
+ NetworkStatsCollection.Builder(BUCKET_DURATION_MS),
+ getInputStreamForResource(R.raw.netstats_uid_v4))
+ }
+
+ val builder = NetworkStatsCollection.Builder(BUCKET_DURATION_MS)
+ NetworkStatsDataMigrationUtils.readPlatformCollection(builder,
+ getInputStreamForResource(R.raw.netstats_uid_v16))
+ // The values are obtained by dumping from NetworkStatsCollection that
+ // read by the logic inside the service.
+ assertValues(builder.build(), 55, 1814302L, 21050L, 31001636L, 26152L)
+ }
+
+ @Test
+ fun testMaybeReadLegacyUid() {
+ val builder = NetworkStatsCollection.Builder(BUCKET_DURATION_MS)
+ NetworkStatsDataMigrationUtils.readLegacyUid(builder,
+ getInputStreamForResource(R.raw.netstats_uid_v4), false /* taggedData */)
+ assertValues(builder.build(), 223, 106245210L, 710722L, 1130647496L, 1103989L)
+ }
+
+ private fun assertValues(
+ collection: NetworkStatsCollection,
+ expectedSize: Int,
+ expectedTxBytes: Long,
+ expectedTxPackets: Long,
+ expectedRxBytes: Long,
+ expectedRxPackets: Long
+ ) {
+ var txBytes = 0L
+ var txPackets = 0L
+ var rxBytes = 0L
+ var rxPackets = 0L
+ val entries = collection.entries
+
+ for (history in entries.values) {
+ for (historyEntry in history.entries) {
+ txBytes += historyEntry.txBytes
+ txPackets += historyEntry.txPackets
+ rxBytes += historyEntry.rxBytes
+ rxPackets += historyEntry.rxPackets
+ }
+ }
+ if (expectedSize != entries.size ||
+ expectedTxBytes != txBytes ||
+ expectedTxPackets != txPackets ||
+ expectedRxBytes != rxBytes ||
+ expectedRxPackets != rxPackets) {
+ fail("expected size=$expectedSize" +
+ "txb=$expectedTxBytes txp=$expectedTxPackets " +
+ "rxb=$expectedRxBytes rxp=$expectedRxPackets bus was " +
+ "size=${entries.size} txb=$txBytes txp=$txPackets " +
+ "rxb=$rxBytes rxp=$rxPackets")
+ }
+ assertEquals(txBytes + rxBytes, collection.totalBytes)
+ }
+
+ private fun getInputStreamForResource(resourceId: Int): DataInputStream {
+ return DataInputStream(InstrumentationRegistry.getContext()
+ .getResources().openRawResource(resourceId))
+ }
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index abb34dc..652aee9 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -101,6 +101,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_VSIM;
import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP;
+import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
@@ -338,6 +339,7 @@
import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
import com.android.server.connectivity.ProxyTracker;
import com.android.server.connectivity.QosCallbackTracker;
+import com.android.server.connectivity.UidRangeUtils;
import com.android.server.connectivity.Vpn;
import com.android.server.connectivity.VpnProfileStore;
import com.android.server.net.NetworkPinner;
@@ -350,6 +352,7 @@
import com.android.testutils.TestableNetworkOfferCallback;
import org.junit.After;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -443,6 +446,10 @@
private static final int TEST_WORK_PROFILE_USER_ID = 2;
private static final int TEST_WORK_PROFILE_APP_UID =
UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID);
+ private static final int TEST_APP_ID_2 = 104;
+ private static final int TEST_WORK_PROFILE_APP_UID_2 =
+ UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID_2);
+
private static final String CLAT_PREFIX = "v4-";
private static final String MOBILE_IFNAME = "test_rmnet_data0";
private static final String CLAT_MOBILE_IFNAME = CLAT_PREFIX + MOBILE_IFNAME;
@@ -492,6 +499,8 @@
private TestNetworkCallback mSystemDefaultNetworkCallback;
private TestNetworkCallback mProfileDefaultNetworkCallback;
private TestNetworkCallback mTestPackageDefaultNetworkCallback;
+ private TestNetworkCallback mProfileDefaultNetworkCallbackAsAppUid2;
+ private TestNetworkCallback mTestPackageDefaultNetworkCallback2;
// State variables required to emulate NetworkPolicyManagerService behaviour.
private int mBlockedReasons = BLOCKED_REASON_NONE;
@@ -12247,6 +12256,8 @@
private void registerDefaultNetworkCallbacks() {
if (mSystemDefaultNetworkCallback != null || mDefaultNetworkCallback != null
|| mProfileDefaultNetworkCallback != null
+ || mProfileDefaultNetworkCallbackAsAppUid2 != null
+ || mTestPackageDefaultNetworkCallback2 != null
|| mTestPackageDefaultNetworkCallback != null) {
throw new IllegalStateException("Default network callbacks already registered");
}
@@ -12257,12 +12268,18 @@
mDefaultNetworkCallback = new TestNetworkCallback();
mProfileDefaultNetworkCallback = new TestNetworkCallback();
mTestPackageDefaultNetworkCallback = new TestNetworkCallback();
+ mProfileDefaultNetworkCallbackAsAppUid2 = new TestNetworkCallback();
+ mTestPackageDefaultNetworkCallback2 = new TestNetworkCallback();
mCm.registerSystemDefaultNetworkCallback(mSystemDefaultNetworkCallback,
new Handler(ConnectivityThread.getInstanceLooper()));
mCm.registerDefaultNetworkCallback(mDefaultNetworkCallback);
registerDefaultNetworkCallbackAsUid(mProfileDefaultNetworkCallback,
TEST_WORK_PROFILE_APP_UID);
registerDefaultNetworkCallbackAsUid(mTestPackageDefaultNetworkCallback, TEST_PACKAGE_UID);
+ registerDefaultNetworkCallbackAsUid(mProfileDefaultNetworkCallbackAsAppUid2,
+ TEST_WORK_PROFILE_APP_UID_2);
+ registerDefaultNetworkCallbackAsUid(mTestPackageDefaultNetworkCallback2,
+ TEST_PACKAGE_UID2);
// TODO: test using ConnectivityManager#registerDefaultNetworkCallbackAsUid as well.
mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED);
}
@@ -12280,6 +12297,12 @@
if (null != mTestPackageDefaultNetworkCallback) {
mCm.unregisterNetworkCallback(mTestPackageDefaultNetworkCallback);
}
+ if (null != mProfileDefaultNetworkCallbackAsAppUid2) {
+ mCm.unregisterNetworkCallback(mProfileDefaultNetworkCallbackAsAppUid2);
+ }
+ if (null != mTestPackageDefaultNetworkCallback2) {
+ mCm.unregisterNetworkCallback(mTestPackageDefaultNetworkCallback2);
+ }
}
private void setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest(
@@ -13708,10 +13731,34 @@
}
private UidRangeParcel[] uidRangeFor(final UserHandle handle) {
- UidRange range = UidRange.createForUser(handle);
+ final UidRange range = UidRange.createForUser(handle);
return new UidRangeParcel[] { new UidRangeParcel(range.start, range.stop) };
}
+ private UidRangeParcel[] uidRangeFor(final UserHandle handle,
+ ProfileNetworkPreference profileNetworkPreference) {
+ final Set<UidRange> uidRangeSet;
+ UidRange range = UidRange.createForUser(handle);
+ if (profileNetworkPreference.getIncludedUids().size() != 0) {
+ uidRangeSet = UidRangeUtils.convertListToUidRange(
+ profileNetworkPreference.getIncludedUids());
+ } else if (profileNetworkPreference.getExcludedUids().size() != 0) {
+ uidRangeSet = UidRangeUtils.removeRangeSetFromUidRange(
+ range, UidRangeUtils.convertListToUidRange(
+ profileNetworkPreference.getExcludedUids()));
+ } else {
+ uidRangeSet = new ArraySet<>();
+ uidRangeSet.add(range);
+ }
+ UidRangeParcel[] uidRangeParcels = new UidRangeParcel[uidRangeSet.size()];
+ int i = 0;
+ for (UidRange range1 : uidRangeSet) {
+ uidRangeParcels[i] = new UidRangeParcel(range1.start, range1.stop);
+ i++;
+ }
+ return uidRangeParcels;
+ }
+
private static class TestOnCompleteListener implements Runnable {
final class OnComplete {}
final ArrayTrackRecord<OnComplete>.ReadHead mHistory =
@@ -13734,6 +13781,14 @@
return new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), workNc);
}
+ private TestNetworkAgentWrapper makeEnterpriseNetworkAgent(int enterpriseId) throws Exception {
+ final NetworkCapabilities workNc = new NetworkCapabilities();
+ workNc.addCapability(NET_CAPABILITY_ENTERPRISE);
+ workNc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+ workNc.addEnterpriseId(enterpriseId);
+ return new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), workNc);
+ }
+
private TestNetworkCallback mEnterpriseCallback;
private UserHandle setupEnterpriseNetwork() {
final UserHandle userHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
@@ -13762,21 +13817,27 @@
*/
public void testPreferenceForUserNetworkUpDownForGivenPreference(
ProfileNetworkPreference profileNetworkPreference,
- boolean connectWorkProfileAgentAhead) throws Exception {
+ boolean connectWorkProfileAgentAhead,
+ UserHandle testHandle,
+ TestNetworkCallback profileDefaultNetworkCallback,
+ TestNetworkCallback disAllowProfileDefaultNetworkCallback) throws Exception {
final InOrder inOrder = inOrder(mMockNetd);
- final UserHandle testHandle = setupEnterpriseNetwork();
- registerDefaultNetworkCallbacks();
mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
mCellNetworkAgent.connect(true);
mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
- mProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+ profileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ disAllowProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(
+ mCellNetworkAgent);
+ }
inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
- final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent();
+ final TestNetworkAgentWrapper workAgent =
+ makeEnterpriseNetworkAgent(profileNetworkPreference.getPreferenceEnterpriseId());
if (connectWorkProfileAgentAhead) {
workAgent.connect(false);
}
@@ -13790,7 +13851,7 @@
== PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK) {
allowFallback = false;
}
- if (allowFallback) {
+ if (allowFallback && !connectWorkProfileAgentAhead) {
// Setting a network preference for this user will create a new set of routing rules for
// the UID range that corresponds to this user, inorder to define the default network
// for these apps separately. This is true because the multi-layer request relevant to
@@ -13800,33 +13861,43 @@
// system default is not handled specially, the rules are always active as long as
// a preference is set.
inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
- mCellNetworkAgent.getNetwork().netId, uidRangeFor(testHandle),
+ mCellNetworkAgent.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreference),
PREFERENCE_ORDER_PROFILE));
}
// The enterprise network is not ready yet.
assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
- if (allowFallback) {
- assertNoCallbacks(mProfileDefaultNetworkCallback);
+ if (allowFallback && !connectWorkProfileAgentAhead) {
+ assertNoCallbacks(profileDefaultNetworkCallback);
} else if (!connectWorkProfileAgentAhead) {
- mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+ profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+ }
}
if (!connectWorkProfileAgentAhead) {
workAgent.connect(false);
}
- mProfileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent);
+ profileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent);
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ disAllowProfileDefaultNetworkCallback.assertNoCallback();
+ }
mSystemDefaultNetworkCallback.assertNoCallback();
mDefaultNetworkCallback.assertNoCallback();
inOrder.verify(mMockNetd).networkCreate(
nativeNetworkConfigPhysical(workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM));
inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
- workAgent.getNetwork().netId, uidRangeFor(testHandle), PREFERENCE_ORDER_PROFILE));
+ workAgent.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreference),
+ PREFERENCE_ORDER_PROFILE));
- if (allowFallback) {
+ if (allowFallback && !connectWorkProfileAgentAhead) {
inOrder.verify(mMockNetd).networkRemoveUidRangesParcel(new NativeUidRangeConfig(
- mCellNetworkAgent.getNetwork().netId, uidRangeFor(testHandle),
+ mCellNetworkAgent.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreference),
PREFERENCE_ORDER_PROFILE));
}
@@ -13834,14 +13905,23 @@
// not to the other apps.
workAgent.setNetworkValid(true /* isStrictMode */);
workAgent.mNetworkMonitor.forceReevaluation(Process.myUid());
- mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent,
+ profileDefaultNetworkCallback.expectCapabilitiesThat(workAgent,
nc -> nc.hasCapability(NET_CAPABILITY_VALIDATED)
- && nc.hasCapability(NET_CAPABILITY_ENTERPRISE));
+ && nc.hasCapability(NET_CAPABILITY_ENTERPRISE)
+ && nc.hasEnterpriseId(
+ profileNetworkPreference.getPreferenceEnterpriseId())
+ && nc.getEnterpriseIds().length == 1);
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+ }
assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
workAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
- mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, nc ->
+ profileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, nc ->
nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+ }
assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
// Conversely, change a capability on the system-wide default network and make sure
@@ -13851,7 +13931,11 @@
nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
mDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc ->
nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
- mProfileDefaultNetworkCallback.assertNoCallback();
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ disAllowProfileDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc ->
+ nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+ }
+ profileDefaultNetworkCallback.assertNoCallback();
// Disconnect and reconnect the system-wide default network and make sure that the
// apps on this network see the appropriate callbacks, and the app on the work profile
@@ -13859,28 +13943,41 @@
mCellNetworkAgent.disconnect();
mSystemDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
- mProfileDefaultNetworkCallback.assertNoCallback();
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ disAllowProfileDefaultNetworkCallback.expectCallback(
+ CallbackEntry.LOST, mCellNetworkAgent);
+ }
+ profileDefaultNetworkCallback.assertNoCallback();
inOrder.verify(mMockNetd).networkDestroy(mCellNetworkAgent.getNetwork().netId);
mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
mCellNetworkAgent.connect(true);
mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
- mProfileDefaultNetworkCallback.assertNoCallback();
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ disAllowProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(
+ mCellNetworkAgent);
+
+ }
+ profileDefaultNetworkCallback.assertNoCallback();
inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE));
// When the agent disconnects, test that the app on the work profile falls back to the
// default network.
workAgent.disconnect();
- mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent);
+ profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent);
if (allowFallback) {
- mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+ profileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+ }
}
assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
if (allowFallback) {
inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
- mCellNetworkAgent.getNetwork().netId, uidRangeFor(testHandle),
+ mCellNetworkAgent.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreference),
PREFERENCE_ORDER_PROFILE));
}
inOrder.verify(mMockNetd).networkDestroy(workAgent.getNetwork().netId);
@@ -13888,8 +13985,12 @@
mCellNetworkAgent.disconnect();
mSystemDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ disAllowProfileDefaultNetworkCallback.expectCallback(
+ CallbackEntry.LOST, mCellNetworkAgent);
+ }
if (allowFallback) {
- mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
+ profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
}
// Waiting for the handler to be idle before checking for networkDestroy is necessary
@@ -13900,33 +14001,47 @@
// If the control comes here, callbacks seem to behave correctly in the presence of
// a default network when the enterprise network goes up and down. Now, make sure they
// also behave correctly in the absence of a system-wide default network.
- final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent();
+ final TestNetworkAgentWrapper workAgent2 =
+ makeEnterpriseNetworkAgent(profileNetworkPreference.getPreferenceEnterpriseId());
workAgent2.connect(false);
- mProfileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent2);
+ profileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent2);
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+ }
assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical(
workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM));
inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
- workAgent2.getNetwork().netId, uidRangeFor(testHandle), PREFERENCE_ORDER_PROFILE));
+ workAgent2.getNetwork().netId,
+ uidRangeFor(testHandle, profileNetworkPreference), PREFERENCE_ORDER_PROFILE));
workAgent2.setNetworkValid(true /* isStrictMode */);
workAgent2.mNetworkMonitor.forceReevaluation(Process.myUid());
- mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent2,
+ profileDefaultNetworkCallback.expectCapabilitiesThat(workAgent2,
nc -> nc.hasCapability(NET_CAPABILITY_ENTERPRISE)
- && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+ && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ && nc.hasEnterpriseId(
+ profileNetworkPreference.getPreferenceEnterpriseId())
+ && nc.getEnterpriseIds().length == 1);
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+ }
assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
inOrder.verify(mMockNetd, never()).networkAddUidRangesParcel(any());
// When the agent disconnects, test that the app on the work profile fall back to the
// default network.
workAgent2.disconnect();
- mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent2);
+ profileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent2);
+ if (disAllowProfileDefaultNetworkCallback != null) {
+ assertNoCallbacks(disAllowProfileDefaultNetworkCallback);
+ }
assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback);
inOrder.verify(mMockNetd).networkDestroy(workAgent2.getNetwork().netId);
assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback,
- mProfileDefaultNetworkCallback);
+ profileDefaultNetworkCallback);
// Callbacks will be unregistered by tearDown()
}
@@ -13938,11 +14053,15 @@
*/
@Test
public void testPreferenceForUserNetworkUpDown() throws Exception {
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ registerDefaultNetworkCallbacks();
ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
new ProfileNetworkPreference.Builder();
profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
testPreferenceForUserNetworkUpDownForGivenPreference(
- profileNetworkPreferenceBuilder.build(), false);
+ profileNetworkPreferenceBuilder.build(), false,
+ testHandle, mProfileDefaultNetworkCallback, null);
}
/**
@@ -13952,12 +14071,16 @@
*/
@Test
public void testPreferenceForUserNetworkUpDownWithNoFallback() throws Exception {
+ final UserHandle testHandle = setupEnterpriseNetwork();
ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
new ProfileNetworkPreference.Builder();
profileNetworkPreferenceBuilder.setPreference(
PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ registerDefaultNetworkCallbacks();
testPreferenceForUserNetworkUpDownForGivenPreference(
- profileNetworkPreferenceBuilder.build(), false);
+ profileNetworkPreferenceBuilder.build(), false,
+ testHandle, mProfileDefaultNetworkCallback, null);
}
/**
@@ -13969,12 +14092,227 @@
@Test
public void testPreferenceForUserNetworkUpDownWithNoFallbackWithAlreadyConnectedWorkAgent()
throws Exception {
+ final UserHandle testHandle = setupEnterpriseNetwork();
ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
new ProfileNetworkPreference.Builder();
profileNetworkPreferenceBuilder.setPreference(
PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ registerDefaultNetworkCallbacks();
testPreferenceForUserNetworkUpDownForGivenPreference(
- profileNetworkPreferenceBuilder.build(), true);
+ profileNetworkPreferenceBuilder.build(), true, testHandle,
+ mProfileDefaultNetworkCallback, null);
+ }
+
+ /**
+ * Make sure per-profile networking preference for specific uid of test handle
+ * behaves as expected
+ */
+ @Test
+ public void testPreferenceForDefaultUidOfTestHandle() throws Exception {
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ profileNetworkPreferenceBuilder.setIncludedUids(
+ List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID)));
+ registerDefaultNetworkCallbacks();
+ testPreferenceForUserNetworkUpDownForGivenPreference(
+ profileNetworkPreferenceBuilder.build(), false, testHandle,
+ mProfileDefaultNetworkCallback, null);
+ }
+
+ /**
+ * Make sure per-profile networking preference for specific uid of test handle
+ * behaves as expected
+ */
+ @Test
+ public void testPreferenceForSpecificUidOfOnlyOneApp() throws Exception {
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ profileNetworkPreferenceBuilder.setIncludedUids(
+ List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+ registerDefaultNetworkCallbacks();
+ testPreferenceForUserNetworkUpDownForGivenPreference(
+ profileNetworkPreferenceBuilder.build(), false,
+ testHandle, mProfileDefaultNetworkCallbackAsAppUid2, null);
+ }
+
+ /**
+ * Make sure per-profile networking preference for specific uid of test handle
+ * behaves as expected
+ */
+ @Test
+ public void testPreferenceForDisallowSpecificUidOfApp() throws Exception {
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ profileNetworkPreferenceBuilder.setExcludedUids(
+ List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+ registerDefaultNetworkCallbacks();
+ testPreferenceForUserNetworkUpDownForGivenPreference(
+ profileNetworkPreferenceBuilder.build(), false,
+ testHandle, mProfileDefaultNetworkCallback,
+ mProfileDefaultNetworkCallbackAsAppUid2);
+ }
+
+ /**
+ * Make sure per-profile networking preference for specific uid of test handle
+ * invalid uid inputs
+ */
+ @Test
+ public void testPreferenceForInvalidUids() throws Exception {
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ profileNetworkPreferenceBuilder.setExcludedUids(
+ List.of(testHandle.getUid(0) - 1));
+ final TestOnCompleteListener listener = new TestOnCompleteListener();
+ Assert.assertThrows(IllegalArgumentException.class, () -> mCm.setProfileNetworkPreferences(
+ testHandle, List.of(profileNetworkPreferenceBuilder.build()),
+ r -> r.run(), listener));
+
+ profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder.setIncludedUids(
+ List.of(testHandle.getUid(0) - 1));
+ Assert.assertThrows(IllegalArgumentException.class,
+ () -> mCm.setProfileNetworkPreferences(
+ testHandle, List.of(profileNetworkPreferenceBuilder.build()),
+ r -> r.run(), listener));
+
+
+ profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder.setIncludedUids(
+ List.of(testHandle.getUid(0) - 1));
+ profileNetworkPreferenceBuilder.setExcludedUids(
+ List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+ Assert.assertThrows(IllegalArgumentException.class,
+ () -> mCm.setProfileNetworkPreferences(
+ testHandle, List.of(profileNetworkPreferenceBuilder.build()),
+ r -> r.run(), listener));
+
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder2 =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder2.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ profileNetworkPreferenceBuilder2.setIncludedUids(
+ List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+ profileNetworkPreferenceBuilder.setIncludedUids(
+ List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+ Assert.assertThrows(IllegalArgumentException.class,
+ () -> mCm.setProfileNetworkPreferences(
+ testHandle, List.of(profileNetworkPreferenceBuilder.build(),
+ profileNetworkPreferenceBuilder2.build()),
+ r -> r.run(), listener));
+
+ profileNetworkPreferenceBuilder2.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder2.setExcludedUids(
+ List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+ profileNetworkPreferenceBuilder.setExcludedUids(
+ List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+ Assert.assertThrows(IllegalArgumentException.class,
+ () -> mCm.setProfileNetworkPreferences(
+ testHandle, List.of(profileNetworkPreferenceBuilder.build(),
+ profileNetworkPreferenceBuilder2.build()),
+ r -> r.run(), listener));
+
+ profileNetworkPreferenceBuilder2.setPreference(
+ PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+ profileNetworkPreferenceBuilder2.setExcludedUids(
+ List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+ profileNetworkPreferenceBuilder.setExcludedUids(
+ List.of(testHandle.getUid(TEST_WORK_PROFILE_APP_UID_2)));
+ Assert.assertThrows(IllegalArgumentException.class,
+ () -> mCm.setProfileNetworkPreferences(
+ testHandle, List.of(profileNetworkPreferenceBuilder.build(),
+ profileNetworkPreferenceBuilder2.build()),
+ r -> r.run(), listener));
+ }
+
+ /**
+ * Make sure per-profile networking preference behaves as expected when the enterprise network
+ * goes up and down while the preference is active. Make sure they behave as expected whether
+ * there is a general default network or not when configured to fallback to default network
+ * along with already connected enterprise work agent
+ */
+ @Test
+ public void testPreferenceForUserNetworkUpDownWithFallbackWithAlreadyConnectedWorkAgent()
+ throws Exception {
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ registerDefaultNetworkCallbacks();
+ testPreferenceForUserNetworkUpDownForGivenPreference(
+ profileNetworkPreferenceBuilder.build(), true,
+ testHandle, mProfileDefaultNetworkCallback,
+ null);
+ }
+
+ /**
+ * Make sure per-profile networking preference behaves as expected when the enterprise network
+ * goes up and down while the preference is active for a given enterprise identifier
+ */
+ @Test
+ public void testPreferenceForUserNetworkUpDownWithDefaultEnterpriseId()
+ throws Exception {
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder.setPreference(
+ PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
+ registerDefaultNetworkCallbacks();
+ testPreferenceForUserNetworkUpDownForGivenPreference(
+ profileNetworkPreferenceBuilder.build(), true,
+ testHandle, mProfileDefaultNetworkCallback,
+ null);
+ }
+
+ /**
+ * Make sure per-profile networking preference behaves as expected when the enterprise network
+ * goes up and down while the preference is active for a given enterprise identifier
+ */
+ @Test
+ public void testPreferenceForUserNetworkUpDownWithId2()
+ throws Exception {
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder.setPreference(
+ PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(
+ NetworkCapabilities.NET_ENTERPRISE_ID_2);
+ registerDefaultNetworkCallbacks();
+ testPreferenceForUserNetworkUpDownForGivenPreference(
+ profileNetworkPreferenceBuilder.build(), true,
+ testHandle, mProfileDefaultNetworkCallback, null);
+ }
+
+ /**
+ * Make sure per-profile networking preference behaves as expected when the enterprise network
+ * goes up and down while the preference is active for a given enterprise identifier
+ */
+ @Test
+ public void testPreferenceForUserNetworkUpDownWithInvalidId()
+ throws Exception {
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder.setPreference(
+ PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(0);
+ registerDefaultNetworkCallbacks();
+ assertThrows("Should not be able to set invalid enterprise id",
+ IllegalStateException.class, () -> profileNetworkPreferenceBuilder.build());
}
/**
@@ -14127,10 +14465,15 @@
public void testProfileNetworkPrefWrongPreference() throws Exception {
final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID);
mServiceContext.setWorkProfile(testHandle, true);
+ ProfileNetworkPreference.Builder profileNetworkPreferenceBuilder =
+ new ProfileNetworkPreference.Builder();
+ profileNetworkPreferenceBuilder.setPreference(
+ PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK + 1);
+ profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
assertThrows("Should not be able to set an illegal preference",
IllegalArgumentException.class,
- () -> mCm.setProfileNetworkPreference(testHandle,
- PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK + 1,
+ () -> mCm.setProfileNetworkPreferences(testHandle,
+ List.of(profileNetworkPreferenceBuilder.build()),
null, null));
}
diff --git a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
index ea29da0..0c58582 100644
--- a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
+++ b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java
@@ -22,6 +22,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -32,6 +33,7 @@
import android.annotation.NonNull;
import android.content.Context;
+import android.net.ConnectivityManager;
import android.net.INetd;
import android.net.INetdUnsolicitedEventListener;
import android.net.LinkAddress;
@@ -71,6 +73,7 @@
public class NetworkManagementServiceTest {
private NetworkManagementService mNMService;
@Mock private Context mContext;
+ @Mock private ConnectivityManager mCm;
@Mock private IBatteryStats.Stub mBatteryStatsService;
@Mock private INetd.Stub mNetdService;
@@ -113,6 +116,9 @@
MockitoAnnotations.initMocks(this);
doNothing().when(mNetdService)
.registerUnsolicitedEventListener(mUnsolListenerCaptor.capture());
+ doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName(
+ eq(ConnectivityManager.class));
+ doReturn(mCm).when(mContext).getSystemService(eq(Context.CONNECTIVITY_SERVICE));
// Start the service and wait until it connects to our socket.
mNMService = NetworkManagementService.create(mContext, mDeps);
}
@@ -239,6 +245,7 @@
mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, true);
assertTrue("Should be true since mobile data usage is restricted",
mNMService.isNetworkRestricted(TEST_UID));
+ verify(mCm).updateMeteredNetworkDenyList(TEST_UID, true /* enabled */);
mNMService.setDataSaverModeEnabled(true);
verify(mNetdService).bandwidthEnableDataSaver(true);
@@ -246,13 +253,16 @@
mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, false);
assertTrue("Should be true since data saver is on and the uid is not allowlisted",
mNMService.isNetworkRestricted(TEST_UID));
+ verify(mCm).updateMeteredNetworkDenyList(TEST_UID, true /* false */);
mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, true);
assertFalse("Should be false since data saver is on and the uid is allowlisted",
mNMService.isNetworkRestricted(TEST_UID));
+ verify(mCm).updateMeteredNetworkAllowList(TEST_UID, true /* enabled */);
// remove uid from allowlist and turn datasaver off again
mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false);
+ verify(mCm).updateMeteredNetworkAllowList(TEST_UID, false /* enabled */);
mNMService.setDataSaverModeEnabled(false);
verify(mNetdService).bandwidthEnableDataSaver(false);
assertFalse("Network should not be restricted when data saver is off",
@@ -306,12 +316,14 @@
for (int chain : chains) {
final ArrayMap<Integer, Boolean> expectedValues = expected.get(chain);
mNMService.setFirewallChainEnabled(chain, true);
+ verify(mCm).setFirewallChainEnabled(chain, true /* enabled */);
for (int state : states) {
mNMService.setFirewallUidRule(chain, TEST_UID, state);
assertEquals(errorMsg.apply(chain, state),
expectedValues.get(state), mNMService.isNetworkRestricted(TEST_UID));
}
mNMService.setFirewallChainEnabled(chain, false);
+ verify(mCm).setFirewallChainEnabled(chain, false /* enabled */);
}
}
}
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
new file mode 100644
index 0000000..84e02ce
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -0,0 +1,385 @@
+/*
+ * 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 com.android.server.connectivity;
+
+import static android.net.INetd.IF_STATE_UP;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU;
+import static com.android.server.connectivity.ClatCoordinator.CLAT_MAX_MTU;
+import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_PREFIX_LEN;
+import static com.android.server.connectivity.ClatCoordinator.INIT_V4ADDR_STRING;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+
+import android.annotation.NonNull;
+import android.net.INetd;
+import android.net.IpPrefix;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Objects;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class ClatCoordinatorTest {
+ private static final String BASE_IFACE = "test0";
+ private static final String STACKED_IFACE = "v4-test0";
+ private static final int BASE_IFINDEX = 1000;
+
+ private static final IpPrefix NAT64_IP_PREFIX = new IpPrefix("64:ff9b::/96");
+ private static final String NAT64_PREFIX_STRING = "64:ff9b::";
+ private static final int GOOGLE_DNS_4 = 0x08080808; // 8.8.8.8
+ private static final int NETID = 42;
+
+ // The test fwmark means: PERMISSION_SYSTEM (0x2), protectedFromVpn: true,
+ // explicitlySelected: true, netid: 42. For bit field structure definition, see union Fwmark in
+ // system/netd/include/Fwmark.h
+ private static final int MARK = 0xb002a;
+
+ private static final String XLAT_LOCAL_IPV4ADDR_STRING = "192.0.0.46";
+ private static final String XLAT_LOCAL_IPV6ADDR_STRING = "2001:db8:0:b11::464";
+ private static final int CLATD_PID = 10483;
+
+ private static final int TUN_FD = 534;
+ private static final int RAW_SOCK_FD = 535;
+ private static final int PACKET_SOCK_FD = 536;
+ private static final ParcelFileDescriptor TUN_PFD = new ParcelFileDescriptor(
+ new FileDescriptor());
+ private static final ParcelFileDescriptor RAW_SOCK_PFD = new ParcelFileDescriptor(
+ new FileDescriptor());
+ private static final ParcelFileDescriptor PACKET_SOCK_PFD = new ParcelFileDescriptor(
+ new FileDescriptor());
+
+ @Mock private INetd mNetd;
+ @Spy private TestDependencies mDeps = new TestDependencies();
+
+ /**
+ * The dependency injection class is used to mock the JNI functions and system functions
+ * for clatd coordinator control plane. Note that any testing used JNI functions need to
+ * be overridden to avoid calling native methods.
+ */
+ protected class TestDependencies extends ClatCoordinator.Dependencies {
+ /**
+ * Get netd.
+ */
+ @Override
+ public INetd getNetd() {
+ return mNetd;
+ }
+
+ /**
+ * @see ParcelFileDescriptor#adoptFd(int).
+ */
+ @Override
+ public ParcelFileDescriptor adoptFd(int fd) {
+ switch (fd) {
+ case TUN_FD:
+ return TUN_PFD;
+ case RAW_SOCK_FD:
+ return RAW_SOCK_PFD;
+ case PACKET_SOCK_FD:
+ return PACKET_SOCK_PFD;
+ default:
+ fail("unsupported arg: " + fd);
+ return null;
+ }
+ }
+
+ /**
+ * Get interface index for a given interface.
+ */
+ @Override
+ public int getInterfaceIndex(String ifName) {
+ if (BASE_IFACE.equals(ifName)) {
+ return BASE_IFINDEX;
+ }
+ fail("unsupported arg: " + ifName);
+ return -1;
+ }
+
+ /**
+ * Create tun interface for a given interface name.
+ */
+ @Override
+ public int createTunInterface(@NonNull String tuniface) throws IOException {
+ if (STACKED_IFACE.equals(tuniface)) {
+ return TUN_FD;
+ }
+ fail("unsupported arg: " + tuniface);
+ return -1;
+ }
+
+ /**
+ * Pick an IPv4 address for clat.
+ */
+ @Override
+ public String selectIpv4Address(@NonNull String v4addr, int prefixlen)
+ throws IOException {
+ if (INIT_V4ADDR_STRING.equals(v4addr) && INIT_V4ADDR_PREFIX_LEN == prefixlen) {
+ return XLAT_LOCAL_IPV4ADDR_STRING;
+ }
+ fail("unsupported args: " + v4addr + ", " + prefixlen);
+ return null;
+ }
+
+ /**
+ * Generate a checksum-neutral IID.
+ */
+ @Override
+ public String generateIpv6Address(@NonNull String iface, @NonNull String v4,
+ @NonNull String prefix64) throws IOException {
+ if (BASE_IFACE.equals(iface) && XLAT_LOCAL_IPV4ADDR_STRING.equals(v4)
+ && NAT64_PREFIX_STRING.equals(prefix64)) {
+ return XLAT_LOCAL_IPV6ADDR_STRING;
+ }
+ fail("unsupported args: " + iface + ", " + v4 + ", " + prefix64);
+ return null;
+ }
+
+ /**
+ * Detect MTU.
+ */
+ @Override
+ public int detectMtu(@NonNull String platSubnet, int platSuffix, int mark)
+ throws IOException {
+ if (NAT64_PREFIX_STRING.equals(platSubnet) && GOOGLE_DNS_4 == platSuffix
+ && MARK == mark) {
+ return ETHER_MTU;
+ }
+ fail("unsupported args: " + platSubnet + ", " + platSuffix + ", " + mark);
+ return -1;
+ }
+
+ /**
+ * Open IPv6 raw socket and set SO_MARK.
+ */
+ @Override
+ public int openRawSocket6(int mark) throws IOException {
+ if (mark == MARK) {
+ return RAW_SOCK_FD;
+ }
+ fail("unsupported arg: " + mark);
+ return -1;
+ }
+
+ /**
+ * Open packet socket.
+ */
+ @Override
+ public int openPacketSocket() throws IOException {
+ // assume that open socket always successfully because there is no argument to check.
+ return PACKET_SOCK_FD;
+ }
+
+ /**
+ * Add anycast setsockopt.
+ */
+ @Override
+ public void addAnycastSetsockopt(@NonNull FileDescriptor sock, String v6, int ifindex)
+ throws IOException {
+ if (Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), sock)
+ && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)
+ && BASE_IFINDEX == ifindex) return;
+ fail("unsupported args: " + sock + ", " + v6 + ", " + ifindex);
+ }
+
+ /**
+ * Configure packet socket.
+ */
+ @Override
+ public void configurePacketSocket(@NonNull FileDescriptor sock, String v6, int ifindex)
+ throws IOException {
+ if (Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), sock)
+ && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)
+ && BASE_IFINDEX == ifindex) return;
+ fail("unsupported args: " + sock + ", " + v6 + ", " + ifindex);
+ }
+
+ /**
+ * Start clatd.
+ */
+ @Override
+ public int startClatd(@NonNull FileDescriptor tunfd, @NonNull FileDescriptor readsock6,
+ @NonNull FileDescriptor writesock6, @NonNull String iface, @NonNull String pfx96,
+ @NonNull String v4, @NonNull String v6) throws IOException {
+ if (Objects.equals(TUN_PFD.getFileDescriptor(), tunfd)
+ && Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), readsock6)
+ && Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), writesock6)
+ && BASE_IFACE.equals(iface)
+ && NAT64_PREFIX_STRING.equals(pfx96)
+ && XLAT_LOCAL_IPV4ADDR_STRING.equals(v4)
+ && XLAT_LOCAL_IPV6ADDR_STRING.equals(v6)) {
+ return CLATD_PID;
+ }
+ fail("unsupported args: " + tunfd + ", " + readsock6 + ", " + writesock6 + ", "
+ + ", " + iface + ", " + v4 + ", " + v6);
+ return -1;
+ }
+
+ /**
+ * Stop clatd.
+ */
+ public void stopClatd(@NonNull String iface, @NonNull String pfx96, @NonNull String v4,
+ @NonNull String v6, int pid) throws IOException {
+ if (pid == -1) {
+ fail("unsupported arg: " + pid);
+ }
+ }
+ };
+
+ @NonNull
+ private ClatCoordinator makeClatCoordinator() throws Exception {
+ final ClatCoordinator coordinator = new ClatCoordinator(mDeps);
+ return coordinator;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ private boolean assertContainsFlag(String[] flags, String match) {
+ for (String flag : flags) {
+ if (flag.equals(match)) return true;
+ }
+ fail("Missing flag: " + match);
+ return false;
+ }
+
+ @Test
+ public void testStartStopClatd() throws Exception {
+ final ClatCoordinator coordinator = makeClatCoordinator();
+ final InOrder inOrder = inOrder(mNetd, mDeps);
+ clearInvocations(mNetd, mDeps);
+
+ // [1] Start clatd.
+ final String addr6For464xlat = coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX);
+ assertEquals(XLAT_LOCAL_IPV6ADDR_STRING, addr6For464xlat);
+
+ // Pick an IPv4 address.
+ inOrder.verify(mDeps).selectIpv4Address(eq(INIT_V4ADDR_STRING),
+ eq(INIT_V4ADDR_PREFIX_LEN));
+
+ // Generate a checksum-neutral IID.
+ inOrder.verify(mDeps).generateIpv6Address(eq(BASE_IFACE),
+ eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(NAT64_PREFIX_STRING));
+
+ // Open, configure and bring up the tun interface.
+ inOrder.verify(mDeps).createTunInterface(eq(STACKED_IFACE));
+ inOrder.verify(mDeps).adoptFd(eq(TUN_FD));
+ inOrder.verify(mNetd).interfaceSetEnableIPv6(eq(STACKED_IFACE), eq(false /* enable */));
+ inOrder.verify(mDeps).detectMtu(eq(NAT64_PREFIX_STRING), eq(GOOGLE_DNS_4), eq(MARK));
+ inOrder.verify(mNetd).interfaceSetMtu(eq(STACKED_IFACE),
+ eq(1472 /* ETHER_MTU(1500) - MTU_DELTA(28) */));
+ inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
+ STACKED_IFACE.equals(cfg.ifName)
+ && XLAT_LOCAL_IPV4ADDR_STRING.equals(cfg.ipv4Addr)
+ && (32 == cfg.prefixLength)
+ && "".equals(cfg.hwAddr)
+ && assertContainsFlag(cfg.flags, IF_STATE_UP)));
+
+ // Open and configure 464xlat read/write sockets.
+ inOrder.verify(mDeps).openPacketSocket();
+ inOrder.verify(mDeps).adoptFd(eq(PACKET_SOCK_FD));
+ inOrder.verify(mDeps).openRawSocket6(eq(MARK));
+ inOrder.verify(mDeps).adoptFd(eq(RAW_SOCK_FD));
+ inOrder.verify(mDeps).getInterfaceIndex(eq(BASE_IFACE));
+ inOrder.verify(mDeps).addAnycastSetsockopt(
+ argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
+ eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
+ inOrder.verify(mDeps).configurePacketSocket(
+ argThat(fd -> Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), fd)),
+ eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(BASE_IFINDEX));
+
+ // Start clatd.
+ inOrder.verify(mDeps).startClatd(
+ argThat(fd -> Objects.equals(TUN_PFD.getFileDescriptor(), fd)),
+ argThat(fd -> Objects.equals(PACKET_SOCK_PFD.getFileDescriptor(), fd)),
+ argThat(fd -> Objects.equals(RAW_SOCK_PFD.getFileDescriptor(), fd)),
+ eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
+ eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING));
+ inOrder.verifyNoMoreInteractions();
+
+ // [2] Start clatd again failed.
+ assertThrows("java.io.IOException: Clatd is already running on test0 (pid 10483)",
+ IOException.class,
+ () -> coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
+
+ // [3] Expect clatd to stop successfully.
+ coordinator.clatStop();
+ inOrder.verify(mDeps).stopClatd(eq(BASE_IFACE), eq(NAT64_PREFIX_STRING),
+ eq(XLAT_LOCAL_IPV4ADDR_STRING), eq(XLAT_LOCAL_IPV6ADDR_STRING), eq(CLATD_PID));
+ inOrder.verifyNoMoreInteractions();
+
+ // [4] Expect an IO exception while stopping a clatd that doesn't exist.
+ assertThrows("java.io.IOException: Clatd has not started", IOException.class,
+ () -> coordinator.clatStop());
+ inOrder.verify(mDeps, never()).stopClatd(anyString(), anyString(), anyString(),
+ anyString(), anyInt());
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void testGetFwmark() throws Exception {
+ assertEquals(0xb0064, ClatCoordinator.getFwmark(100));
+ assertEquals(0xb03e8, ClatCoordinator.getFwmark(1000));
+ assertEquals(0xb2710, ClatCoordinator.getFwmark(10000));
+ assertEquals(0xbffff, ClatCoordinator.getFwmark(65535));
+ }
+
+ @Test
+ public void testAdjustMtu() throws Exception {
+ // Expected mtu is that IPV6_MIN_MTU(1280) minus MTU_DELTA(28).
+ assertEquals(1252, ClatCoordinator.adjustMtu(-1 /* detect mtu failed */));
+ assertEquals(1252, ClatCoordinator.adjustMtu(500));
+ assertEquals(1252, ClatCoordinator.adjustMtu(1000));
+ assertEquals(1252, ClatCoordinator.adjustMtu(1280));
+
+ // Expected mtu is that the detected mtu minus MTU_DELTA(28).
+ assertEquals(1372, ClatCoordinator.adjustMtu(1400));
+ assertEquals(1472, ClatCoordinator.adjustMtu(ETHER_MTU));
+ assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU));
+
+ // Expected mtu is that CLAT_MAX_MTU(65536) minus MTU_DELTA(28).
+ assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
index c86e699..ec51537 100644
--- a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java
@@ -296,7 +296,7 @@
false /* roaming */);
verify(mStatsManager, times(1)).registerUsageCallback(
- any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
+ any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
}
@Test
@@ -315,7 +315,7 @@
// Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB
verify(mStatsManager, times(1)).registerUsageCallback(
- any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
+ any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
}
@Test
@@ -334,7 +334,7 @@
// Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB
verify(mStatsManager, times(1)).registerUsageCallback(
- any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
+ any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
}
@Test
@@ -351,7 +351,7 @@
// Default global setting should be used: 12 - 7 = 5
verify(mStatsManager, times(1)).registerUsageCallback(
- any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any());
+ any(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any());
}
@Test
@@ -366,7 +366,7 @@
false /* roaming */);
verify(mStatsManager, times(1)).registerUsageCallback(
- any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
+ any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
// Update setting
setDefaultQuotaGlobalSetting(DataUnit.MEGABYTES.toBytes(14));
@@ -376,7 +376,7 @@
// Callback must have been re-registered with new setting
verify(mStatsManager, times(1)).unregisterUsageCallback(any());
verify(mStatsManager, times(1)).registerUsageCallback(
- any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
+ any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
}
@Test
@@ -391,7 +391,7 @@
false /* roaming */);
verify(mStatsManager, times(1)).registerUsageCallback(
- any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
+ any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes))
.thenReturn((int) DataUnit.MEGABYTES.toBytes(16));
@@ -402,6 +402,6 @@
// Uses the new setting (16 - 2 = 14MB)
verify(mStatsManager, times(1)).registerUsageCallback(
- any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any());
+ any(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any());
}
}
diff --git a/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java b/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java
new file mode 100644
index 0000000..b8c2673
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/UidRangeUtilsTest.java
@@ -0,0 +1,354 @@
+/*
+ * 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 com.android.server.connectivity;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.UidRange;
+import android.os.Build;
+import android.util.ArraySet;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Tests for UidRangeUtils.
+ *
+ * Build, install and run with:
+ * runtest frameworks-net -c com.android.server.connectivity.UidRangeUtilsTest
+ */
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+public class UidRangeUtilsTest {
+ private static void assertInSameRange(@NonNull final String msg,
+ @Nullable final UidRange r1,
+ @Nullable final Set<UidRange> s2) {
+ assertTrue(msg + " : " + s2 + " unexpectedly is not in range of " + r1,
+ UidRangeUtils.isRangeSetInUidRange(r1, s2));
+ }
+
+ private static void assertNotInSameRange(@NonNull final String msg,
+ @Nullable final UidRange r1, @Nullable final Set<UidRange> s2) {
+ assertFalse(msg + " : " + s2 + " unexpectedly is in range of " + r1,
+ UidRangeUtils.isRangeSetInUidRange(r1, s2));
+ }
+
+ @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testRangeSetInUidRange() {
+ final UidRange uids1 = new UidRange(1, 100);
+ final UidRange uids2 = new UidRange(3, 300);
+ final UidRange uids3 = new UidRange(1, 1000);
+ final UidRange uids4 = new UidRange(1, 100);
+ final UidRange uids5 = new UidRange(2, 20);
+ final UidRange uids6 = new UidRange(3, 30);
+
+ assertThrows(NullPointerException.class,
+ () -> UidRangeUtils.isRangeSetInUidRange(null, null));
+ assertThrows(NullPointerException.class,
+ () -> UidRangeUtils.isRangeSetInUidRange(uids1, null));
+
+ final ArraySet<UidRange> set1 = new ArraySet<>();
+ final ArraySet<UidRange> set2 = new ArraySet<>();
+
+ assertThrows(NullPointerException.class,
+ () -> UidRangeUtils.isRangeSetInUidRange(null, set1));
+ assertInSameRange("uids1 <=> empty", uids1, set2);
+
+ set2.add(uids1);
+ assertInSameRange("uids1 <=> uids1", uids1, set2);
+
+ set2.clear();
+ set2.add(uids2);
+ assertNotInSameRange("uids1 <=> uids2", uids1, set2);
+ set2.clear();
+ set2.add(uids3);
+ assertNotInSameRange("uids1 <=> uids3", uids1, set2);
+ set2.clear();
+ set2.add(uids4);
+ assertInSameRange("uids1 <=> uids4", uids1, set2);
+
+ set2.clear();
+ set2.add(uids5);
+ set2.add(uids6);
+ assertInSameRange("uids1 <=> uids5, 6", uids1, set2);
+
+ set2.clear();
+ set2.add(uids2);
+ set2.add(uids6);
+ assertNotInSameRange("uids1 <=> uids2, 6", uids1, set2);
+ }
+
+ @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testRemoveRangeSetFromUidRange() {
+ final UidRange uids1 = new UidRange(1, 100);
+ final UidRange uids2 = new UidRange(3, 300);
+ final UidRange uids3 = new UidRange(1, 1000);
+ final UidRange uids4 = new UidRange(1, 100);
+ final UidRange uids5 = new UidRange(2, 20);
+ final UidRange uids6 = new UidRange(3, 30);
+ final UidRange uids7 = new UidRange(30, 39);
+
+ final UidRange uids8 = new UidRange(1, 1);
+ final UidRange uids9 = new UidRange(21, 100);
+ final UidRange uids10 = new UidRange(1, 2);
+ final UidRange uids11 = new UidRange(31, 100);
+
+ final UidRange uids12 = new UidRange(1, 1);
+ final UidRange uids13 = new UidRange(21, 29);
+ final UidRange uids14 = new UidRange(40, 100);
+
+ final UidRange uids15 = new UidRange(3, 30);
+ final UidRange uids16 = new UidRange(31, 39);
+
+ assertThrows(NullPointerException.class,
+ () -> UidRangeUtils.removeRangeSetFromUidRange(null, null));
+ Set<UidRange> expected = new ArraySet<>();
+ expected.add(uids1);
+ assertThrows(NullPointerException.class,
+ () -> UidRangeUtils.removeRangeSetFromUidRange(uids1, null));
+ assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, new ArraySet<>()));
+
+ expected.clear();
+ final ArraySet<UidRange> set2 = new ArraySet<>();
+ set2.add(uids1);
+ assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+ set2.clear();
+ set2.add(uids4);
+ assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+ expected.add(uids10);
+ set2.clear();
+ set2.add(uids2);
+ assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+ expected.clear();
+ set2.clear();
+ set2.add(uids3);
+ assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+ set2.clear();
+ set2.add(uids3);
+ set2.add(uids6);
+ assertThrows(IllegalArgumentException.class,
+ () -> UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+ expected.clear();
+ expected.add(uids8);
+ expected.add(uids9);
+ set2.clear();
+ set2.add(uids5);
+ assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+ expected.clear();
+ expected.add(uids10);
+ expected.add(uids11);
+ set2.clear();
+ set2.add(uids6);
+ assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+ expected.clear();
+ expected.add(uids12);
+ expected.add(uids13);
+ expected.add(uids14);
+ set2.clear();
+ set2.add(uids5);
+ set2.add(uids7);
+ assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+
+ expected.clear();
+ expected.add(uids10);
+ expected.add(uids14);
+ set2.clear();
+ set2.add(uids15);
+ set2.add(uids16);
+ assertEquals(expected, UidRangeUtils.removeRangeSetFromUidRange(uids1, set2));
+ }
+
+ private static void assertRangeOverlaps(@NonNull final String msg,
+ @Nullable final Set<UidRange> s1,
+ @Nullable final Set<UidRange> s2) {
+ assertTrue(msg + " : " + s2 + " unexpectedly does not overlap with " + s1,
+ UidRangeUtils.doesRangeSetOverlap(s1, s2));
+ }
+
+ private static void assertRangeDoesNotOverlap(@NonNull final String msg,
+ @Nullable final Set<UidRange> s1, @Nullable final Set<UidRange> s2) {
+ assertFalse(msg + " : " + s2 + " unexpectedly ovelaps with " + s1,
+ UidRangeUtils.doesRangeSetOverlap(s1, s2));
+ }
+
+ @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testRangeSetOverlap() {
+ final UidRange uids1 = new UidRange(1, 100);
+ final UidRange uids2 = new UidRange(3, 300);
+ final UidRange uids3 = new UidRange(1, 1000);
+ final UidRange uids4 = new UidRange(1, 100);
+ final UidRange uids5 = new UidRange(2, 20);
+ final UidRange uids6 = new UidRange(3, 30);
+ final UidRange uids7 = new UidRange(0, 0);
+ final UidRange uids8 = new UidRange(1, 500);
+ final UidRange uids9 = new UidRange(101, 200);
+
+ assertThrows(NullPointerException.class,
+ () -> UidRangeUtils.doesRangeSetOverlap(null, null));
+
+ final ArraySet<UidRange> set1 = new ArraySet<>();
+ final ArraySet<UidRange> set2 = new ArraySet<>();
+ assertThrows(NullPointerException.class,
+ () -> UidRangeUtils.doesRangeSetOverlap(set1, null));
+ assertThrows(NullPointerException.class,
+ () -> UidRangeUtils.doesRangeSetOverlap(null, set2));
+ assertRangeDoesNotOverlap("empty <=> null", set1, set2);
+
+ set2.add(uids1);
+ set1.add(uids1);
+ assertRangeOverlaps("uids1 <=> uids1", set1, set2);
+
+ set1.clear();
+ set1.add(uids1);
+ set2.clear();
+ set2.add(uids2);
+ assertRangeOverlaps("uids1 <=> uids2", set1, set2);
+
+ set1.clear();
+ set1.add(uids1);
+ set2.clear();
+ set2.add(uids3);
+ assertRangeOverlaps("uids1 <=> uids3", set1, set2);
+
+ set1.clear();
+ set1.add(uids1);
+ set2.clear();
+ set2.add(uids4);
+ assertRangeOverlaps("uids1 <=> uids4", set1, set2);
+
+ set1.clear();
+ set1.add(uids1);
+ set2.clear();
+ set2.add(uids5);
+ set2.add(uids6);
+ assertRangeOverlaps("uids1 <=> uids5,6", set1, set2);
+
+ set1.clear();
+ set1.add(uids1);
+ set2.clear();
+ set2.add(uids7);
+ assertRangeDoesNotOverlap("uids1 <=> uids7", set1, set2);
+
+ set1.clear();
+ set1.add(uids1);
+ set2.clear();
+ set2.add(uids9);
+ assertRangeDoesNotOverlap("uids1 <=> uids9", set1, set2);
+
+ set1.clear();
+ set1.add(uids1);
+ set2.clear();
+ set2.add(uids8);
+ assertRangeOverlaps("uids1 <=> uids8", set1, set2);
+
+
+ set1.clear();
+ set1.add(uids1);
+ set2.clear();
+ set2.add(uids8);
+ set2.add(uids7);
+ assertRangeOverlaps("uids1 <=> uids7, 8", set1, set2);
+ }
+
+ @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testConvertListToUidRange() {
+ final UidRange uids1 = new UidRange(1, 1);
+ final UidRange uids2 = new UidRange(1, 2);
+ final UidRange uids3 = new UidRange(100, 100);
+ final UidRange uids4 = new UidRange(10, 10);
+
+ final UidRange uids5 = new UidRange(10, 14);
+ final UidRange uids6 = new UidRange(20, 24);
+
+ final Set<UidRange> expected = new ArraySet<>();
+ final List<Integer> input = new ArrayList<Integer>();
+
+ assertThrows(NullPointerException.class, () -> UidRangeUtils.convertListToUidRange(null));
+ assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+
+ input.add(1);
+ expected.add(uids1);
+ assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+
+ input.add(2);
+ expected.clear();
+ expected.add(uids2);
+ assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+
+ input.clear();
+ input.add(1);
+ input.add(100);
+ expected.clear();
+ expected.add(uids1);
+ expected.add(uids3);
+ assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+
+ input.clear();
+ input.add(100);
+ input.add(1);
+ expected.clear();
+ expected.add(uids1);
+ expected.add(uids3);
+ assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+
+ input.clear();
+ input.add(100);
+ input.add(1);
+ input.add(2);
+ input.add(1);
+ input.add(10);
+ expected.clear();
+ expected.add(uids2);
+ expected.add(uids4);
+ expected.add(uids3);
+ assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+
+ input.clear();
+ input.add(10);
+ input.add(11);
+ input.add(12);
+ input.add(13);
+ input.add(14);
+ input.add(20);
+ input.add(21);
+ input.add(22);
+ input.add(23);
+ input.add(24);
+ expected.clear();
+ expected.add(uids5);
+ expected.add(uids6);
+ assertEquals(expected, UidRangeUtils.convertListToUidRange(input));
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
new file mode 100644
index 0000000..987b7b7
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/BpfInterfaceMapUpdaterTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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 com.android.server.net;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.INetd;
+import android.net.MacAddress;
+import android.os.Handler;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.Struct.U32;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class BpfInterfaceMapUpdaterTest {
+ private static final int TEST_INDEX = 1;
+ private static final int TEST_INDEX2 = 2;
+ private static final String TEST_INTERFACE_NAME = "test1";
+ private static final String TEST_INTERFACE_NAME2 = "test2";
+
+ private final TestLooper mLooper = new TestLooper();
+ private BaseNetdUnsolicitedEventListener mListener;
+ private BpfInterfaceMapUpdater mUpdater;
+ @Mock private IBpfMap<U32, InterfaceMapValue> mBpfMap;
+ @Mock private INetd mNetd;
+ @Mock private Context mContext;
+
+ private class TestDependencies extends BpfInterfaceMapUpdater.Dependencies {
+ @Override
+ public IBpfMap<U32, InterfaceMapValue> getInterfaceMap() {
+ return mBpfMap;
+ }
+
+ @Override
+ public InterfaceParams getInterfaceParams(String ifaceName) {
+ if (ifaceName.equals(TEST_INTERFACE_NAME)) {
+ return new InterfaceParams(TEST_INTERFACE_NAME, TEST_INDEX,
+ MacAddress.ALL_ZEROS_ADDRESS);
+ } else if (ifaceName.equals(TEST_INTERFACE_NAME2)) {
+ return new InterfaceParams(TEST_INTERFACE_NAME2, TEST_INDEX2,
+ MacAddress.ALL_ZEROS_ADDRESS);
+ }
+
+ return null;
+ }
+
+ @Override
+ public INetd getINetd(Context ctx) {
+ return mNetd;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ when(mNetd.interfaceGetList()).thenReturn(new String[] {TEST_INTERFACE_NAME});
+ mUpdater = new BpfInterfaceMapUpdater(mContext, new Handler(mLooper.getLooper()),
+ new TestDependencies());
+ }
+
+ private void verifyStartUpdater() throws Exception {
+ mUpdater.start();
+ mLooper.dispatchAll();
+ final ArgumentCaptor<BaseNetdUnsolicitedEventListener> listenerCaptor =
+ ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener.class);
+ verify(mNetd).registerUnsolicitedEventListener(listenerCaptor.capture());
+ mListener = listenerCaptor.getValue();
+ verify(mBpfMap).updateEntry(eq(new U32(TEST_INDEX)),
+ eq(new InterfaceMapValue(TEST_INTERFACE_NAME)));
+ }
+
+ @Test
+ public void testUpdateInterfaceMap() throws Exception {
+ verifyStartUpdater();
+
+ mListener.onInterfaceAdded(TEST_INTERFACE_NAME2);
+ mLooper.dispatchAll();
+ verify(mBpfMap).updateEntry(eq(new U32(TEST_INDEX2)),
+ eq(new InterfaceMapValue(TEST_INTERFACE_NAME2)));
+
+ // Check that when onInterfaceRemoved is called, nothing happens.
+ mListener.onInterfaceRemoved(TEST_INTERFACE_NAME);
+ mLooper.dispatchAll();
+ verifyNoMoreInteractions(mBpfMap);
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
index 8340a13..0e2c293 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -32,9 +32,12 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import android.content.Context;
import android.content.res.Resources;
-import android.net.INetd;
+import android.net.ConnectivityManager;
import android.net.NetworkStats;
import android.net.TrafficStats;
import android.net.UnderlyingNetworkInfo;
@@ -73,8 +76,8 @@
private File mTestProc;
private NetworkStatsFactory mFactory;
- @Mock
- private INetd mNetd;
+ @Mock private Context mContext;
+ @Mock private ConnectivityManager mCm;
@Before
public void setUp() throws Exception {
@@ -85,7 +88,10 @@
// applications. So in order to have a test support native library, the native code
// related to networkStatsFactory is compiled to a minimal native library and loaded here.
System.loadLibrary("networkstatsfactorytestjni");
- mFactory = new NetworkStatsFactory(mTestProc, false, mNetd);
+ doReturn(Context.CONNECTIVITY_SERVICE).when(mContext).getSystemServiceName(
+ eq(ConnectivityManager.class));
+ doReturn(mCm).when(mContext).getSystemService(eq(Context.CONNECTIVITY_SERVICE));
+ mFactory = new NetworkStatsFactory(mContext, mTestProc, false);
mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]);
}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 416549c..66dcf6d 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -29,25 +29,22 @@
import static android.net.TrafficStats.MB_IN_BYTES;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
-import android.app.usage.NetworkStatsManager;
import android.net.DataUsageRequest;
import android.net.NetworkIdentity;
import android.net.NetworkIdentitySet;
import android.net.NetworkStats;
import android.net.NetworkStatsAccess;
import android.net.NetworkTemplate;
-import android.os.Build;
-import android.os.ConditionVariable;
-import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
-import android.os.Messenger;
import android.os.Process;
import android.os.UserHandle;
import android.telephony.TelephonyManager;
@@ -55,7 +52,6 @@
import androidx.test.filters.SmallTest;
-import com.android.server.net.NetworkStatsServiceTest.LatchedHandler;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
@@ -74,7 +70,7 @@
*/
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
public class NetworkStatsObserversTest {
private static final String TEST_IFACE = "test0";
private static final String TEST_IFACE2 = "test1";
@@ -96,21 +92,15 @@
private static final long WAIT_TIMEOUT_MS = 500;
private static final long THRESHOLD_BYTES = 2 * MB_IN_BYTES;
private static final long BASE_BYTES = 7 * MB_IN_BYTES;
- private static final int INVALID_TYPE = -1;
-
- private long mElapsedRealtime;
private HandlerThread mObserverHandlerThread;
- private Handler mObserverNoopHandler;
-
- private LatchedHandler mHandler;
private NetworkStatsObservers mStatsObservers;
- private Messenger mMessenger;
private ArrayMap<String, NetworkIdentitySet> mActiveIfaces;
private ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces;
- @Mock private IBinder mockBinder;
+ @Mock private IBinder mUsageCallbackBinder;
+ private TestableUsageCallback mUsageCallback;
@Before
public void setUp() throws Exception {
@@ -126,24 +116,29 @@
}
};
- mHandler = new LatchedHandler(Looper.getMainLooper(), new ConditionVariable());
- mMessenger = new Messenger(mHandler);
-
mActiveIfaces = new ArrayMap<>();
mActiveUidIfaces = new ArrayMap<>();
+ mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
}
@Test
public void testRegister_thresholdTooLow_setsDefaultThreshold() throws Exception {
- long thresholdTooLowBytes = 1L;
- DataUsageRequest inputRequest = new DataUsageRequest(
+ final long thresholdTooLowBytes = 1L;
+ final DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdTooLowBytes);
- DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
- Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
- assertTrue(request.requestId > 0);
- assertTrue(Objects.equals(sTemplateWifi, request.template));
- assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
+ final DataUsageRequest requestByApp = mStatsObservers.register(inputRequest, mUsageCallback,
+ UID_RED, NetworkStatsAccess.Level.DEVICE);
+ assertTrue(requestByApp.requestId > 0);
+ assertTrue(Objects.equals(sTemplateWifi, requestByApp.template));
+ assertEquals(THRESHOLD_BYTES, requestByApp.thresholdInBytes);
+
+ // Verify the threshold requested by system uid won't be overridden.
+ final DataUsageRequest requestBySystem = mStatsObservers.register(inputRequest,
+ mUsageCallback, Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
+ assertTrue(requestBySystem.requestId > 0);
+ assertTrue(Objects.equals(sTemplateWifi, requestBySystem.template));
+ assertEquals(1, requestBySystem.thresholdInBytes);
}
@Test
@@ -152,7 +147,7 @@
DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, highThresholdBytes);
- DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+ DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateWifi, request.template));
@@ -164,13 +159,13 @@
DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES);
- DataUsageRequest request1 = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+ DataUsageRequest request1 = mStatsObservers.register(inputRequest, mUsageCallback,
Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
assertTrue(request1.requestId > 0);
assertTrue(Objects.equals(sTemplateWifi, request1.template));
assertEquals(THRESHOLD_BYTES, request1.thresholdInBytes);
- DataUsageRequest request2 = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+ DataUsageRequest request2 = mStatsObservers.register(inputRequest, mUsageCallback,
Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
assertTrue(request2.requestId > request1.requestId);
assertTrue(Objects.equals(sTemplateWifi, request2.template));
@@ -190,17 +185,19 @@
DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
- DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+ DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
- Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+ Mockito.verify(mUsageCallbackBinder).linkToDeath(any(IBinder.DeathRecipient.class),
+ anyInt());
mStatsObservers.unregister(request, Process.SYSTEM_UID);
waitForObserverToIdle();
- Mockito.verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+ Mockito.verify(mUsageCallbackBinder).unlinkToDeath(any(IBinder.DeathRecipient.class),
+ anyInt());
}
@Test
@@ -208,17 +205,18 @@
DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
- DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+ DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
UID_RED, NetworkStatsAccess.Level.DEVICE);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
assertEquals(THRESHOLD_BYTES, request.thresholdInBytes);
- Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+ Mockito.verify(mUsageCallbackBinder)
+ .linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
mStatsObservers.unregister(request, UID_BLUE);
waitForObserverToIdle();
- Mockito.verifyZeroInteractions(mockBinder);
+ Mockito.verifyZeroInteractions(mUsageCallbackBinder);
}
private NetworkIdentitySet makeTestIdentSet() {
@@ -235,7 +233,7 @@
DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
- DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+ DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -259,7 +257,7 @@
DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
- DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+ DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -289,7 +287,7 @@
DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
- DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+ DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -312,7 +310,7 @@
mStatsObservers.updateStats(
xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
waitForObserverToIdle();
- assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
+ mUsageCallback.expectOnThresholdReached(request);
}
@Test
@@ -320,7 +318,7 @@
DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
- DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+ DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
UID_RED, NetworkStatsAccess.Level.DEFAULT);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -345,7 +343,7 @@
mStatsObservers.updateStats(
xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
waitForObserverToIdle();
- assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
+ mUsageCallback.expectOnThresholdReached(request);
}
@Test
@@ -353,7 +351,7 @@
DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
- DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+ DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
UID_BLUE, NetworkStatsAccess.Level.DEFAULT);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -385,7 +383,7 @@
DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
- DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+ DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
UID_BLUE, NetworkStatsAccess.Level.USER);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -410,7 +408,7 @@
mStatsObservers.updateStats(
xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START);
waitForObserverToIdle();
- assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType);
+ mUsageCallback.expectOnThresholdReached(request);
}
@Test
@@ -418,7 +416,7 @@
DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES);
- DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder,
+ DataUsageRequest request = mStatsObservers.register(inputRequest, mUsageCallback,
UID_RED, NetworkStatsAccess.Level.USER);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateImsi1, request.template));
@@ -447,6 +445,5 @@
private void waitForObserverToIdle() {
HandlerUtils.waitForIdle(mObserverHandlerThread, WAIT_TIMEOUT_MS);
- HandlerUtils.waitForIdle(mHandler, WAIT_TIMEOUT_MS);
}
}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 40587c5..45f033c 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -66,21 +66,22 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import android.app.AlarmManager;
-import android.app.usage.NetworkStatsManager;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
@@ -101,13 +102,9 @@
import android.net.netstats.provider.INetworkStatsProviderCallback;
import android.net.wifi.WifiInfo;
import android.os.Build;
-import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Messenger;
import android.os.PowerManager;
import android.os.SimpleClock;
import android.provider.Settings;
@@ -117,8 +114,11 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
-import com.android.internal.util.ArrayUtils;
import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.LocationPermissionChecker;
+import com.android.net.module.util.Struct.U32;
+import com.android.net.module.util.Struct.U8;
import com.android.server.net.NetworkStatsService.AlertObserver;
import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config;
@@ -188,11 +188,16 @@
private @Mock TetheringManager mTetheringManager;
private @Mock NetworkStatsFactory mStatsFactory;
private @Mock NetworkStatsSettings mSettings;
- private @Mock IBinder mBinder;
+ private @Mock IBinder mUsageCallbackBinder;
+ private TestableUsageCallback mUsageCallback;
private @Mock AlarmManager mAlarmManager;
@Mock
private NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor;
+ private @Mock BpfInterfaceMapUpdater mBpfInterfaceMapUpdater;
private HandlerThread mHandlerThread;
+ @Mock
+ private LocationPermissionChecker mLocationPermissionChecker;
+ private @Mock IBpfMap<U32, U8> mUidCounterSetMap;
private NetworkStatsService mService;
private INetworkStatsSession mSession;
@@ -262,7 +267,9 @@
MockitoAnnotations.initMocks(this);
final Context context = InstrumentationRegistry.getContext();
mServiceContext = new MockContext(context);
- when(sWifiInfo.getCurrentNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY);
+ when(mLocationPermissionChecker.checkCallersLocationPermission(
+ any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true);
+ when(sWifiInfo.getNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY);
mStatsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
PowerManager powerManager = (PowerManager) mServiceContext.getSystemService(
@@ -309,6 +316,8 @@
verify(mTetheringManager).registerTetheringEventCallback(
any(), tetheringEventCbCaptor.capture());
mTetheringEventCallback = tetheringEventCbCaptor.getValue();
+
+ mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
}
@NonNull
@@ -334,6 +343,21 @@
return mContentObserver = super.makeContentObserver(handler, settings, monitor);
}
+ @Override
+ public LocationPermissionChecker makeLocationPermissionChecker(final Context context) {
+ return mLocationPermissionChecker;
+ }
+
+ @Override
+ public BpfInterfaceMapUpdater makeBpfInterfaceMapUpdater(
+ @NonNull Context ctx, @NonNull Handler handler) {
+ return mBpfInterfaceMapUpdater;
+ }
+
+ @Override
+ public IBpfMap<U32, U8> getUidCounterSetMap() {
+ return mUidCounterSetMap;
+ }
};
}
@@ -459,8 +483,11 @@
.insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
.insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
mService.setUidForeground(UID_RED, false);
+ verify(mUidCounterSetMap, never()).deleteEntry(any());
mService.incrementOperationCount(UID_RED, 0xFAAD, 4);
mService.setUidForeground(UID_RED, true);
+ verify(mUidCounterSetMap).updateEntry(
+ eq(new U32(UID_RED)), eq(new U8((short) SET_FOREGROUND)));
mService.incrementOperationCount(UID_RED, 0xFAAD, 6);
forcePollAndWaitForIdle();
@@ -985,7 +1012,7 @@
}
@Test
- public void testDetailedUidStats() throws Exception {
+ public void testUidStatsForTransport() throws Exception {
// pretend that network comes online
expectDefaultSettings();
NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
@@ -1011,7 +1038,7 @@
.insertEntry(entry3));
mService.incrementOperationCount(UID_RED, 0xF00D, 1);
- NetworkStats stats = mService.getDetailedUidStats(INTERFACES_ALL);
+ NetworkStats stats = mService.getUidStatsForTransport(NetworkCapabilities.TRANSPORT_WIFI);
assertEquals(3, stats.size());
entry1.operations = 1;
@@ -1022,68 +1049,6 @@
}
@Test
- public void testDetailedUidStats_Filtered() throws Exception {
- // pretend that network comes online
- expectDefaultSettings();
-
- final String stackedIface = "stacked-test0";
- final LinkProperties stackedProp = new LinkProperties();
- stackedProp.setInterfaceName(stackedIface);
- final NetworkStateSnapshot wifiState = buildWifiState();
- wifiState.getLinkProperties().addStackedLink(stackedProp);
- NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {wifiState};
-
- expectNetworkStatsSummary(buildEmptyStats());
- expectNetworkStatsUidDetail(buildEmptyStats());
-
- mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
- new UnderlyingNetworkInfo[0]);
-
- NetworkStats.Entry uidStats = new NetworkStats.Entry(
- TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L);
- // Stacked on matching interface
- NetworkStats.Entry tetheredStats1 = new NetworkStats.Entry(
- stackedIface, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L);
- TetherStatsParcel tetherStatsParcel1 =
- buildTetherStatsParcel(stackedIface, 1024L, 8L, 512L, 4L, 0);
- // Different interface
- TetherStatsParcel tetherStatsParcel2 =
- buildTetherStatsParcel("otherif", 1024L, 8L, 512L, 4L, 0);
-
- final String[] ifaceFilter = new String[] { TEST_IFACE };
- final String[] augmentedIfaceFilter = new String[] { stackedIface, TEST_IFACE };
- incrementCurrentTime(HOUR_IN_MILLIS);
- expectDefaultSettings();
- expectNetworkStatsSummary(buildEmptyStats());
- when(mStatsFactory.augmentWithStackedInterfaces(eq(ifaceFilter)))
- .thenReturn(augmentedIfaceFilter);
- when(mStatsFactory.readNetworkStatsDetail(eq(UID_ALL), any(), eq(TAG_ALL)))
- .thenReturn(new NetworkStats(getElapsedRealtime(), 1)
- .insertEntry(uidStats));
- final TetherStatsParcel[] tetherStatsParcels = {tetherStatsParcel1, tetherStatsParcel2};
- when(mNetd.tetherGetStats()).thenReturn(tetherStatsParcels);
-
- NetworkStats stats = mService.getDetailedUidStats(ifaceFilter);
-
- // mStatsFactory#readNetworkStatsDetail() has the following invocations:
- // 1) NetworkStatsService#systemReady from #setUp.
- // 2) mService#notifyNetworkStatus in the test above.
- //
- // Additionally, we should have one call from the above call to mService#getDetailedUidStats
- // with the augmented ifaceFilter.
- verify(mStatsFactory, times(2)).readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL);
- verify(mStatsFactory, times(1)).readNetworkStatsDetail(
- eq(UID_ALL),
- eq(augmentedIfaceFilter),
- eq(TAG_ALL));
- assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), TEST_IFACE));
- assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), stackedIface));
- assertEquals(2, stats.size());
- assertEquals(uidStats, stats.getValues(0, null));
- assertEquals(tetheredStats1, stats.getValues(1, null));
- }
-
- @Test
public void testForegroundBackground() throws Exception {
// pretend that network comes online
expectDefaultSettings();
@@ -1119,6 +1084,8 @@
.insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 32L, 2L, 32L, 2L, 0L)
.insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 1L, 1L, 1L, 1L, 0L));
mService.setUidForeground(UID_RED, true);
+ verify(mUidCounterSetMap).updateEntry(
+ eq(new U32(UID_RED)), eq(new U8((short) SET_FOREGROUND)));
mService.incrementOperationCount(UID_RED, 0xFAAD, 1);
forcePollAndWaitForIdle();
@@ -1294,20 +1261,14 @@
DataUsageRequest inputRequest = new DataUsageRequest(
DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdInBytes);
- // Create a messenger that waits for callback activity
- ConditionVariable cv = new ConditionVariable(false);
- LatchedHandler latchedHandler = new LatchedHandler(Looper.getMainLooper(), cv);
- Messenger messenger = new Messenger(latchedHandler);
-
// Force poll
expectDefaultSettings();
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
// Register and verify request and that binder was called
- DataUsageRequest request =
- mService.registerUsageCallback(mServiceContext.getOpPackageName(), inputRequest,
- messenger, mBinder);
+ DataUsageRequest request = mService.registerUsageCallback(
+ mServiceContext.getOpPackageName(), inputRequest, mUsageCallback);
assertTrue(request.requestId > 0);
assertTrue(Objects.equals(sTemplateWifi, request.template));
long minThresholdInBytes = 2 * 1024 * 1024; // 2 MB
@@ -1316,7 +1277,7 @@
HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
// Make sure that the caller binder gets connected
- verify(mBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+ verify(mUsageCallbackBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
// modify some number on wifi, and trigger poll event
// not enough traffic to call data usage callback
@@ -1331,7 +1292,7 @@
assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0);
// make sure callback has not being called
- assertEquals(INVALID_TYPE, latchedHandler.lastMessageType);
+ mUsageCallback.assertNoCallback();
// and bump forward again, with counters going higher. this is
// important, since it will trigger the data usage callback
@@ -1346,23 +1307,21 @@
assertNetworkTotal(sTemplateWifi, 4096000L, 4L, 8192000L, 8L, 0);
- // Wait for the caller to ack receipt of CALLBACK_LIMIT_REACHED
- assertTrue(cv.block(WAIT_TIMEOUT));
- assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, latchedHandler.lastMessageType);
- cv.close();
+ // Wait for the caller to invoke expectOnThresholdReached.
+ mUsageCallback.expectOnThresholdReached(request);
// Allow binder to disconnect
- when(mBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt())).thenReturn(true);
+ when(mUsageCallbackBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt()))
+ .thenReturn(true);
// Unregister request
mService.unregisterUsageRequest(request);
- // Wait for the caller to ack receipt of CALLBACK_RELEASED
- assertTrue(cv.block(WAIT_TIMEOUT));
- assertEquals(NetworkStatsManager.CALLBACK_RELEASED, latchedHandler.lastMessageType);
+ // Wait for the caller to invoke expectOnCallbackReleased.
+ mUsageCallback.expectOnCallbackReleased(request);
// Make sure that the caller binder gets disconnected
- verify(mBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
+ verify(mUsageCallbackBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
}
@Test
@@ -1688,6 +1647,28 @@
provider.expectOnRequestStatsUpdate(0 /* unused */);
}
+ /**
+ * Verify the service will throw exceptions if the template is location sensitive but
+ * the permission is not granted.
+ */
+ @Test
+ public void testEnforceTemplateLocationPermission() throws Exception {
+ when(mLocationPermissionChecker.checkCallersLocationPermission(
+ any(), any(), anyInt(), anyBoolean(), any())).thenReturn(false);
+ initWifiStats(buildWifiState(true, TEST_IFACE, IMSI_1));
+ assertThrows(SecurityException.class, () ->
+ assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0));
+ // Templates w/o wifi network keys can query stats as usual.
+ assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
+ assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
+
+ when(mLocationPermissionChecker.checkCallersLocationPermission(
+ any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true);
+ assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
+ assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
+ assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
+ }
+
private static File getBaseDir(File statsDir) {
File baseDir = new File(statsDir, "netstats");
baseDir.mkdirs();
@@ -1703,7 +1684,8 @@
private void assertNetworkTotal(NetworkTemplate template, long start, long end, long rxBytes,
long rxPackets, long txBytes, long txPackets, int operations) throws Exception {
// verify history API
- final NetworkStatsHistory history = mSession.getHistoryForNetwork(template, FIELD_ALL);
+ final NetworkStatsHistory history =
+ mSession.getHistoryIntervalForNetwork(template, FIELD_ALL, start, end);
assertValues(history, start, end, rxBytes, rxPackets, txBytes, txPackets, operations);
// verify summary API
@@ -1915,21 +1897,4 @@
private void waitForIdle() {
HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
}
-
- static class LatchedHandler extends Handler {
- private final ConditionVariable mCv;
- int lastMessageType = INVALID_TYPE;
-
- LatchedHandler(Looper looper, ConditionVariable cv) {
- super(looper);
- mCv = cv;
- }
-
- @Override
- public void handleMessage(Message msg) {
- lastMessageType = msg.what;
- mCv.open();
- super.handleMessage(msg);
- }
- }
}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
index 43aeec6..0d34609 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
@@ -35,8 +35,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.usage.NetworkStatsManager;
import android.content.Context;
-import android.net.NetworkTemplate;
import android.os.Build;
import android.os.Looper;
import android.os.Parcel;
@@ -282,7 +282,7 @@
// NETWORK_TYPE_5G_NSA.
setRatTypeForSub(TEST_SUBID1, TelephonyManager.NETWORK_TYPE_LTE,
OVERRIDE_NETWORK_TYPE_NR_NSA);
- assertRatTypeChangedForSub(TEST_IMSI1, NetworkTemplate.NETWORK_TYPE_5G_NSA);
+ assertRatTypeChangedForSub(TEST_IMSI1, NetworkStatsManager.NETWORK_TYPE_5G_NSA);
reset(mDelegate);
// Set RAT type to LTE without NR connected, the RAT type should be downgraded to LTE.
diff --git a/tests/unit/java/com/android/server/net/TestableUsageCallback.kt b/tests/unit/java/com/android/server/net/TestableUsageCallback.kt
new file mode 100644
index 0000000..1917ec3
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/TestableUsageCallback.kt
@@ -0,0 +1,79 @@
+/*
+ * 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 com.android.server.net
+
+import android.net.DataUsageRequest
+import android.net.netstats.IUsageCallback
+import android.os.IBinder
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.TimeUnit
+import kotlin.test.fail
+
+private const val DEFAULT_TIMEOUT_MS = 200L
+
+// TODO: Move the class to static libs once all downstream have IUsageCallback definition.
+class TestableUsageCallback(private val binder: IBinder) : IUsageCallback.Stub() {
+ sealed class CallbackType(val request: DataUsageRequest) {
+ class OnThresholdReached(request: DataUsageRequest) : CallbackType(request)
+ class OnCallbackReleased(request: DataUsageRequest) : CallbackType(request)
+ }
+
+ // TODO: Change to use ArrayTrackRecord once moved into to the module.
+ private val history = LinkedBlockingQueue<CallbackType>()
+
+ override fun onThresholdReached(request: DataUsageRequest) {
+ history.add(CallbackType.OnThresholdReached(request))
+ }
+
+ override fun onCallbackReleased(request: DataUsageRequest) {
+ history.add(CallbackType.OnCallbackReleased(request))
+ }
+
+ fun expectOnThresholdReached(request: DataUsageRequest) {
+ expectCallback<CallbackType.OnThresholdReached>(request, DEFAULT_TIMEOUT_MS)
+ }
+
+ fun expectOnCallbackReleased(request: DataUsageRequest) {
+ expectCallback<CallbackType.OnCallbackReleased>(request, DEFAULT_TIMEOUT_MS)
+ }
+
+ @JvmOverloads
+ fun assertNoCallback(timeout: Long = DEFAULT_TIMEOUT_MS) {
+ val cb = history.poll(timeout, TimeUnit.MILLISECONDS)
+ cb?.let { fail("Expected no callback but got $cb") }
+ }
+
+ // Expects a callback of the specified request on the specified network within the timeout.
+ // If no callback arrives, or a different callback arrives, fail.
+ private inline fun <reified T : CallbackType> expectCallback(
+ expectedRequest: DataUsageRequest,
+ timeoutMs: Long
+ ) {
+ history.poll(timeoutMs, TimeUnit.MILLISECONDS).let {
+ if (it !is T || it.request != expectedRequest) {
+ fail("Unexpected callback : $it," +
+ " expected ${T::class} with Request[$expectedRequest]")
+ } else {
+ it
+ }
+ }
+ }
+
+ override fun asBinder(): IBinder {
+ return binder
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index fe971e7..04ba98f 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -24,7 +24,28 @@
"libbpf_android",
"liblog",
"libnativehelper",
- "libnetdbpf",
"libnetdutils",
+ "libnetworkstats",
+ ],
+}
+
+cc_library_shared {
+ name: "libandroid_net_frameworktests_util_jni",
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ "-Wthread-safety",
+ ],
+ srcs: [
+ "android_net_frameworktests_util/onload.cpp",
+ ],
+ static_libs: [
+ "libnet_utils_device_common_bpfjni",
+ "libtcutils",
+ ],
+ shared_libs: [
+ "liblog",
+ "libnativehelper",
],
}
diff --git a/tests/unit/jni/android_net_frameworktests_util/onload.cpp b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
new file mode 100644
index 0000000..06a3986
--- /dev/null
+++ b/tests/unit/jni/android_net_frameworktests_util/onload.cpp
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+#include <nativehelper/JNIHelp.h>
+#include "jni.h"
+
+#define LOG_TAG "NetFrameworkTestsJni"
+#include <android/log.h>
+
+namespace android {
+
+int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
+int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
+ JNIEnv *env;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "ERROR: GetEnv failed");
+ return JNI_ERR;
+ }
+
+ if (register_com_android_net_module_util_BpfMap(env,
+ "android/net/frameworktests/util/BpfMap") < 0) return JNI_ERR;
+
+ if (register_com_android_net_module_util_TcUtils(env,
+ "android/net/frameworktests/util/TcUtils") < 0) return JNI_ERR;
+
+ return JNI_VERSION_1_6;
+}
+
+}; // namespace android
diff --git a/tests/unit/res/raw/netstats_uid_v16 b/tests/unit/res/raw/netstats_uid_v16
new file mode 100644
index 0000000..a6ee430
--- /dev/null
+++ b/tests/unit/res/raw/netstats_uid_v16
Binary files differ