Merge "Format MdnsAdvertiser with ktlint" into main
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 4d173a5..091849b 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -58,6 +58,7 @@
":framework-connectivity-shared-srcs",
":services-tethering-shared-srcs",
":statslog-connectivity-java-gen",
+ ":statslog-framework-connectivity-java-gen",
":statslog-tethering-java-gen",
],
static_libs: [
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index f123dca..0ac97f0 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -333,6 +333,11 @@
public static final int TETHER_ERROR_UNKNOWN_REQUEST = 17;
@FlaggedApi(Flags.FLAG_TETHERING_WITH_SOFT_AP_CONFIG)
public static final int TETHER_ERROR_DUPLICATE_REQUEST = 18;
+ /**
+ * Never used outside Tethering.java.
+ * @hide
+ */
+ public static final int TETHER_ERROR_BLUETOOTH_SERVICE_PENDING = 19;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index a942166..900b505 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -62,6 +62,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.FrameworkConnectivityStatsLog;
import com.android.net.module.util.SharedLog;
import java.io.PrintWriter;
@@ -154,14 +155,27 @@
// Only launch entitlement UI for the current user if it is allowed to
// change tethering. This usually means the system user or the admin users in HSUM.
- // TODO (b/382624069): Figure out whether it is safe to call createContextAsUser
- // from secondary user. And re-enable the check or remove the code accordingly.
- if (false) {
+ if (SdkLevel.isAtLeastT()) {
// Create a user context for the current foreground user as UserManager#isAdmin()
// operates on the context user.
final int currentUserId = getCurrentUser();
final UserHandle currentUser = UserHandle.of(currentUserId);
- final Context userContext = mContext.createContextAsUser(currentUser, 0);
+ final Context userContext;
+ try {
+ // There is no safe way to invoke this method since tethering package
+ // might not be installed for a certain user on the OEM devices,
+ // refer to b/382628161.
+ userContext = mContext.createContextAsUser(currentUser, 0);
+ } catch (IllegalStateException e) {
+ FrameworkConnectivityStatsLog.write(
+ FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+ FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_ENTITLEMENT_CREATE_CONTEXT_AS_USER_THROWS
+ );
+ // Fallback to startActivity if createContextAsUser failed.
+ mLog.e("createContextAsUser failed, fallback to startActivity", e);
+ mContext.startActivity(intent);
+ return intent;
+ }
final UserManager userManager = userContext.getSystemService(UserManager.class);
if (userManager.isAdminUser()) {
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 1249e85..b50831d 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -43,6 +43,7 @@
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
import static android.net.TetheringManager.TETHERING_WIGIG;
+import static android.net.TetheringManager.TETHER_ERROR_BLUETOOTH_SERVICE_PENDING;
import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL;
@@ -132,7 +133,6 @@
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
-import android.util.Pair;
import android.util.SparseArray;
import androidx.annotation.NonNull;
@@ -292,7 +292,7 @@
private SettingsObserver mSettingsObserver;
private BluetoothPan mBluetoothPan;
private PanServiceListener mBluetoothPanListener;
- private ArrayList<Pair<Boolean, IIntResultListener>> mPendingPanRequests;
+ private final ArrayList<IIntResultListener> mPendingPanRequestListeners;
// AIDL doesn't support Set<Integer>. Maintain a int bitmap here. When the bitmap is passed to
// TetheringManager, TetheringManager would convert it to a set of Integer types.
// mSupportedTypeBitmap should always be updated inside tethering internal thread but it may be
@@ -312,7 +312,7 @@
// This is intended to ensrure that if something calls startTethering(bluetooth) just after
// bluetooth is enabled. Before onServiceConnected is called, store the calls into this
// list and handle them as soon as onServiceConnected is called.
- mPendingPanRequests = new ArrayList<>();
+ mPendingPanRequestListeners = new ArrayList<>();
mTetherStates = new ArrayMap<>();
mConnectedClientsTracker = new ConnectedClientsTracker();
@@ -708,8 +708,7 @@
// If tethering is already enabled with a different request,
// disable before re-enabling.
if (unfinishedRequest != null && !unfinishedRequest.equalsIgnoreUidPackage(request)) {
- enableTetheringInternal(type, false /* disabled */,
- unfinishedRequest.getInterfaceName(), null);
+ enableTetheringInternal(false /* disabled */, unfinishedRequest, null);
mEntitlementMgr.stopProvisioningIfNeeded(type);
}
mPendingTetheringRequests.put(type, request);
@@ -720,7 +719,7 @@
mEntitlementMgr.startProvisioningIfNeeded(type,
request.getShouldShowEntitlementUi());
}
- enableTetheringInternal(type, true /* enabled */, request.getInterfaceName(), listener);
+ enableTetheringInternal(true /* enabled */, request, listener);
mTetheringMetrics.createBuilder(type, callerPkg);
});
}
@@ -767,7 +766,10 @@
void stopTetheringInternal(int type) {
mPendingTetheringRequests.remove(type);
- enableTetheringInternal(type, false /* disabled */, null, null);
+ // Using a placeholder here is ok since none of the disable APIs use the request for
+ // anything. We simply need the tethering type to know which link layer to poke for removal.
+ // TODO: Remove the placeholder here and loop through each pending/serving request.
+ enableTetheringInternal(false /* disabled */, createPlaceholderRequest(type), null);
mEntitlementMgr.stopProvisioningIfNeeded(type);
}
@@ -775,9 +777,10 @@
* Enables or disables tethering for the given type. If provisioning is required, it will
* schedule provisioning rechecks for the specified interface.
*/
- private void enableTetheringInternal(int type, boolean enable,
- String iface, final IIntResultListener listener) {
- int result = TETHER_ERROR_NO_ERROR;
+ private void enableTetheringInternal(boolean enable, @NonNull final TetheringRequest request,
+ final IIntResultListener listener) {
+ final int type = request.getTetheringType();
+ final int result;
switch (type) {
case TETHERING_WIFI:
result = setWifiTethering(enable);
@@ -786,7 +789,7 @@
result = setUsbTethering(enable);
break;
case TETHERING_BLUETOOTH:
- setBluetoothTethering(enable, listener);
+ result = setBluetoothTethering(enable, listener);
break;
case TETHERING_NCM:
result = setNcmTethering(enable);
@@ -795,17 +798,17 @@
result = setEthernetTethering(enable);
break;
case TETHERING_VIRTUAL:
- result = setVirtualMachineTethering(enable, iface);
+ result = setVirtualMachineTethering(enable, request);
break;
default:
Log.w(TAG, "Invalid tether type.");
result = TETHER_ERROR_UNKNOWN_TYPE;
}
- // The result of Bluetooth tethering will be sent by #setBluetoothTethering.
- if (type != TETHERING_BLUETOOTH) {
- sendTetherResult(listener, result, type);
- }
+ // The result of Bluetooth tethering will be sent after the pan service connects.
+ if (result == TETHER_ERROR_BLUETOOTH_SERVICE_PENDING) return;
+
+ sendTetherResult(listener, result, type);
}
private void sendTetherResult(final IIntResultListener listener, final int result,
@@ -844,13 +847,12 @@
return TETHER_ERROR_INTERNAL_ERROR;
}
- private void setBluetoothTethering(final boolean enable, final IIntResultListener listener) {
+ private int setBluetoothTethering(final boolean enable, final IIntResultListener listener) {
final BluetoothAdapter adapter = mDeps.getBluetoothAdapter();
if (adapter == null || !adapter.isEnabled()) {
Log.w(TAG, "Tried to enable bluetooth tethering with null or disabled adapter. null: "
+ (adapter == null));
- sendTetherResult(listener, TETHER_ERROR_SERVICE_UNAVAIL, TETHERING_BLUETOOTH);
- return;
+ return TETHER_ERROR_SERVICE_UNAVAIL;
}
if (mBluetoothPanListener != null && mBluetoothPanListener.isConnected()) {
@@ -858,16 +860,21 @@
// When bluetooth tethering is enabled, any time a PAN client pairs with this
// host, bluetooth will bring up a bt-pan interface and notify tethering to
// enable IP serving.
- setBluetoothTetheringSettings(mBluetoothPan, enable, listener);
- return;
+ return setBluetoothTetheringSettings(mBluetoothPan, enable);
}
- // The reference of IIntResultListener should only exist when application want to start
- // tethering but tethering is not bound to pan service yet. Even if the calling process
- // dies, the referenice of IIntResultListener would still keep in mPendingPanRequests. Once
- // tethering bound to pan service (onServiceConnected) or bluetooth just crash
- // (onServiceDisconnected), all the references from mPendingPanRequests would be cleared.
- mPendingPanRequests.add(new Pair(enable, listener));
+ if (!enable) {
+ // The service is not connected. If disabling tethering, there's no point starting
+ // the service just to stop tethering since tethering is not started. Just remove
+ // any pending requests to enable tethering, and notify them that they have failed.
+ for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+ sendTetherResult(pendingListener, TETHER_ERROR_SERVICE_UNAVAIL,
+ TETHERING_BLUETOOTH);
+ }
+ mPendingPanRequestListeners.clear();
+ return TETHER_ERROR_NO_ERROR;
+ }
+ mPendingPanRequestListeners.add(listener);
// Bluetooth tethering is not a popular feature. To avoid bind to bluetooth pan service all
// the time but user never use bluetooth tethering. mBluetoothPanListener is created first
@@ -877,6 +884,7 @@
mBluetoothPanListener = new PanServiceListener();
adapter.getProfileProxy(mContext, mBluetoothPanListener, BluetoothProfile.PAN);
}
+ return TETHER_ERROR_BLUETOOTH_SERVICE_PENDING;
}
private class PanServiceListener implements ServiceListener {
@@ -893,10 +901,12 @@
mBluetoothPan = (BluetoothPan) proxy;
mIsConnected = true;
- for (Pair<Boolean, IIntResultListener> request : mPendingPanRequests) {
- setBluetoothTetheringSettings(mBluetoothPan, request.first, request.second);
+ for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+ final int result = setBluetoothTetheringSettings(mBluetoothPan,
+ true /* enable */);
+ sendTetherResult(pendingListener, result, TETHERING_BLUETOOTH);
}
- mPendingPanRequests.clear();
+ mPendingPanRequestListeners.clear();
});
}
@@ -907,11 +917,11 @@
// reachable before next onServiceConnected.
mIsConnected = false;
- for (Pair<Boolean, IIntResultListener> request : mPendingPanRequests) {
- sendTetherResult(request.second, TETHER_ERROR_SERVICE_UNAVAIL,
+ for (IIntResultListener pendingListener : mPendingPanRequestListeners) {
+ sendTetherResult(pendingListener, TETHER_ERROR_SERVICE_UNAVAIL,
TETHERING_BLUETOOTH);
}
- mPendingPanRequests.clear();
+ mPendingPanRequestListeners.clear();
mBluetoothIfaceRequest = null;
mBluetoothCallback = null;
maybeDisableBluetoothIpServing();
@@ -923,8 +933,8 @@
}
}
- private void setBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
- final boolean enable, final IIntResultListener listener) {
+ private int setBluetoothTetheringSettings(@NonNull final BluetoothPan bluetoothPan,
+ final boolean enable) {
if (SdkLevel.isAtLeastT()) {
changeBluetoothTetheringSettings(bluetoothPan, enable);
} else {
@@ -933,9 +943,8 @@
// Enabling bluetooth tethering settings can silently fail. Send internal error if the
// result is not expected.
- final int result = bluetoothPan.isTetheringOn() == enable
+ return bluetoothPan.isTetheringOn() == enable
? TETHER_ERROR_NO_ERROR : TETHER_ERROR_INTERNAL_ERROR;
- sendTetherResult(listener, result, TETHERING_BLUETOOTH);
}
private void changeBluetoothTetheringSettingsPreT(@NonNull final BluetoothPan bluetoothPan,
@@ -1054,14 +1063,15 @@
}
}
- private int setVirtualMachineTethering(final boolean enable, String iface) {
+ private int setVirtualMachineTethering(final boolean enable,
+ @NonNull final TetheringRequest request) {
+ final String iface = request.getInterfaceName();
if (enable) {
if (TextUtils.isEmpty(iface)) {
mConfiguredVirtualIface = "avf_tap_fixed";
} else {
mConfiguredVirtualIface = iface;
}
- final TetheringRequest request = getOrCreatePendingTetheringRequest(TETHERING_VIRTUAL);
enableIpServing(request, mConfiguredVirtualIface);
} else if (mConfiguredVirtualIface != null) {
ensureIpServerStopped(mConfiguredVirtualIface);
@@ -1094,6 +1104,19 @@
}
/**
+ * Create a placeholder request. This is used in case we try to find a pending request but there
+ * is none (e.g. stopTethering removed a pending request), or for cases where we only have the
+ * tethering type (e.g. stopTethering(int)).
+ */
+ @NonNull
+ private TetheringRequest createPlaceholderRequest(int type) {
+ final TetheringRequest request = new TetheringRequest.Builder(type).build();
+ request.getParcel().requestType = TetheringRequest.REQUEST_TYPE_LEGACY;
+ request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_GLOBAL;
+ return request;
+ }
+
+ /**
* Gets the TetheringRequest that #startTethering was called with but is waiting for the link
* layer event to indicate the interface is available to tether.
* Note: There are edge cases where the pending request is absent and we must temporarily
@@ -1109,9 +1132,7 @@
Log.w(TAG, "No pending TetheringRequest for type " + type + " found, creating a placeholder"
+ " request");
- TetheringRequest placeholder = new TetheringRequest.Builder(type).build();
- placeholder.getParcel().requestType = REQUEST_TYPE_PLACEHOLDER;
- return placeholder;
+ return createPlaceholderRequest(type);
}
private void handleLegacyTether(String iface, final IIntResultListener listener) {
@@ -1730,27 +1751,7 @@
break;
case IFACE_IP_MODE_LOCAL_ONLY:
type = maybeInferWifiTetheringType(ifname);
- // BUG: this request is incorrect - instead of LOHS, it will reflect whatever
- // request (if any) is being processed for TETHERING_WIFI. However, this is the
- // historical behaviour. It mostly works because a) most of the time there is no
- // such request b) tetherinternal doesn't look at the connectivity scope of the
- // request, it takes the scope from requestedState.
- request = getPendingTetheringRequest(type);
- if (request == null) {
- request = createImplicitLocalOnlyTetheringRequest(TETHERING_WIFI);
- } else {
- // If we've taken this request from the pending requests, then force the
- // connectivity scope to local so we start IpServer in local-only mode (this
- // matches historical behavior). This should be OK since the connectivity scope
- // is only used to start IpServer in the correct mode.
- // TODO: This will break fuzzy-matching logic for start/stop tethering in the
- // future. Figure out how to reconcile that with this forced scope.
- // Possibly ignore the connectivity scope for wifi if both requests are
- // explicit, since explicit Wifi requests may only have
- // CONNECTIVITY_SCOPE_GLOBAL. Or possibly, don't add any edge case and
- // treat it as a different request entirely.
- request.getParcel().connectivityScope = CONNECTIVITY_SCOPE_LOCAL;
- }
+ request = createImplicitLocalOnlyTetheringRequest(type);
break;
default:
mLog.e("Cannot enable IP serving in unknown WiFi mode: " + wifiIpMode);
@@ -2427,9 +2428,14 @@
break;
}
case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
- final int tetheringType = message.arg1;
+ final int type = message.arg1;
final Boolean enabled = (Boolean) message.obj;
- enableTetheringInternal(tetheringType, enabled, null, null);
+ // Using a placeholder here is ok since we just need to the type of
+ // tethering to poke the link layer. When the link layer comes up, we won't
+ // have a pending request to use, but this matches the historical behavior.
+ // TODO: Get the TetheringRequest from IpServer and make sure to put it in
+ // the pending list too.
+ enableTetheringInternal(enabled, createPlaceholderRequest(type), null);
break;
}
default:
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index 16ebbbb..58e1894 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -38,6 +38,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
@@ -49,6 +50,7 @@
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -159,10 +161,20 @@
return super.getSystemServiceName(serviceClass);
}
+ @NonNull
@Override
public Context createContextAsUser(UserHandle user, int flags) {
+ if (mCreateContextAsUserException != null) {
+ throw mCreateContextAsUserException;
+ }
return mMockContext; // Return self for easier test injection.
}
+
+ private RuntimeException mCreateContextAsUserException = null;
+
+ private void setCreateContextAsUserException(RuntimeException e) {
+ mCreateContextAsUserException = e;
+ }
}
class TestDependencies extends EntitlementManager.Dependencies {
@@ -591,8 +603,24 @@
.onTetherProvisioningFailed(TETHERING_WIFI, FAILED_TETHERING_REASON);
}
+ @IgnoreUpTo(SC_V2)
@Test
- public void testUiProvisioningMultiUser() {
+ public void testUiProvisioningMultiUser_aboveT_createContextAsUserThrows() {
+ mMockContext.setCreateContextAsUserException(new IllegalStateException());
+ doTestUiProvisioningMultiUser(true, 1);
+ doTestUiProvisioningMultiUser(false, 1);
+ }
+
+ @IgnoreUpTo(SC_V2)
+ @Test
+ public void testUiProvisioningMultiUser_aboveT() {
+ doTestUiProvisioningMultiUser(true, 1);
+ doTestUiProvisioningMultiUser(false, 0);
+ }
+
+ @IgnoreAfter(SC_V2)
+ @Test
+ public void testUiProvisioningMultiUser_belowT() {
doTestUiProvisioningMultiUser(true, 1);
doTestUiProvisioningMultiUser(false, 1);
}
@@ -630,6 +658,7 @@
doReturn(isAdminUser).when(mUserManager).isAdminUser();
mDeps.reset();
+ clearInvocations(mTetherProvisioningFailedListener);
mDeps.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.notifyUpstream(true);
mLooper.dispatchAll();
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 f7a44f1..e1c2db9 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -93,7 +93,6 @@
import static org.junit.Assert.assertNull;
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;
@@ -2787,6 +2786,10 @@
public void assertHasResult() {
if (!mHasResult) fail("No callback result");
}
+
+ public void assertDoesNotHaveResult() {
+ if (mHasResult) fail("Has callback result");
+ }
}
@Test
@@ -3395,10 +3398,9 @@
}
@Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testBluetoothTethering() throws Exception {
initTetheringOnTestThread();
- // 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 */);
@@ -3432,10 +3434,9 @@
}
@Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
public void testBluetoothTetheringBeforeT() throws Exception {
initTetheringOnTestThread();
- // 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 */);
@@ -3515,6 +3516,23 @@
verifyNetdCommandForBtTearDown();
}
+ @Test
+ public void testPendingPanEnableRequestFailedUponDisableRequest() throws Exception {
+ initTetheringOnTestThread();
+
+ mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
+ final ResultListener failedEnable = new ResultListener(TETHER_ERROR_SERVICE_UNAVAIL);
+ mTethering.startTethering(createTetheringRequest(TETHERING_BLUETOOTH),
+ TEST_CALLER_PKG, failedEnable);
+ mLooper.dispatchAll();
+ failedEnable.assertDoesNotHaveResult();
+
+ // Stop tethering before the pan service connects. This should fail the enable request.
+ mTethering.stopTethering(TETHERING_BLUETOOTH);
+ mLooper.dispatchAll();
+ failedEnable.assertHasResult();
+ }
+
private void mockBluetoothSettings(boolean bluetoothOn, boolean tetheringOn) {
when(mBluetoothAdapter.isEnabled()).thenReturn(bluetoothOn);
when(mBluetoothPan.isTetheringOn()).thenReturn(tetheringOn);
diff --git a/bpf/headers/include/bpf/BpfClassic.h b/bpf/headers/include/bpf/BpfClassic.h
index e6cef89..26d8ad5 100644
--- a/bpf/headers/include/bpf/BpfClassic.h
+++ b/bpf/headers/include/bpf/BpfClassic.h
@@ -170,6 +170,9 @@
// IPv6 extension headers (HOPOPTS, DSTOPS, FRAG) begin with a u8 nexthdr
#define BPF_LOAD_NETX_RELATIVE_V6EXTHDR_NEXTHDR BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+// IPv6 MLD start with u8 type
+#define BPF_LOAD_NETX_RELATIVE_MLD_TYPE BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+
// IPv6 fragment header is always exactly 8 bytes long
#define BPF_LOAD_CONSTANT_V6FRAGHDR_LEN \
BPF_STMT(BPF_LD | BPF_IMM, 8)
diff --git a/bpf/headers/include/bpf/KernelUtils.h b/bpf/headers/include/bpf/KernelUtils.h
index 68bc607..a36085a 100644
--- a/bpf/headers/include/bpf/KernelUtils.h
+++ b/bpf/headers/include/bpf/KernelUtils.h
@@ -55,12 +55,12 @@
isKernelVersion(4, 9) || // minimum for Android S & T
isKernelVersion(4, 14) || // minimum for Android U
isKernelVersion(4, 19) || // minimum for Android V
- isKernelVersion(5, 4) || // first supported in Android R, min for W
+ isKernelVersion(5, 4) || // first supported in Android R, min for 25Q2
isKernelVersion(5, 10) || // first supported in Android S
isKernelVersion(5, 15) || // first supported in Android T
isKernelVersion(6, 1) || // first supported in Android U
isKernelVersion(6, 6) || // first supported in Android V
- isKernelVersion(6, 12); // first supported in Android W
+ isKernelVersion(6, 12); // first supported in Android 25Q2
}
// Figure out the bitness of userspace.
diff --git a/bpf/headers/include/bpf_map_def.h b/bpf/headers/include/bpf_map_def.h
index d67da48..e95ca5f 100644
--- a/bpf/headers/include/bpf_map_def.h
+++ b/bpf/headers/include/bpf_map_def.h
@@ -106,8 +106,12 @@
// Here sizeof & __alignof__ are consistent, but _Alignof is not: compile for 'aosp_cf_x86_phone'
_Static_assert(sizeof(unsigned long long) == 8, "sizeof unsigned long long != 8");
_Static_assert(__alignof__(unsigned long long) == 8, "__alignof__ unsigned long long != 8");
-// BPF wants 8, but 32-bit x86 wants 4
-//_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+// BPF & everyone else wants 8, but 32-bit x86 wants 4
+#if defined(__i386__)
+_Static_assert(_Alignof(unsigned long long) == 4, "x86-32 _Alignof unsigned long long != 4");
+#else
+_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+#endif
// for maps:
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index 04d7492..c67be66 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -823,14 +823,14 @@
"tmp_map_" + objName + "_" + mapNames[i];
ret = bpfFdPin(fd, createLoc.c_str());
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
return -err;
}
ret = renameat2(AT_FDCWD, createLoc.c_str(),
AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
err, strerror(err));
return -err;
@@ -838,32 +838,34 @@
} else {
ret = bpfFdPin(fd, mapPinLoc.c_str());
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
return -err;
}
}
ret = chmod(mapPinLoc.c_str(), md[i].mode);
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
strerror(err));
return -err;
}
ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
ret, err, strerror(err));
return -err;
}
}
- int mapId = bpfGetFdMapId(fd);
- if (mapId == -1) {
- if (isAtLeastKernelVersion(4, 14, 0))
- ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
- } else {
+ if (isAtLeastKernelVersion(4, 14, 0)) {
+ int mapId = bpfGetFdMapId(fd);
+ if (mapId == -1) {
+ const int err = errno;
+ ALOGE("bpfGetFdMapId failed, errno: %d", err);
+ return -err;
+ }
ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
}
@@ -1006,7 +1008,7 @@
if (access(progPinLoc.c_str(), F_OK) == 0) {
fd.reset(retrieveProgram(progPinLoc.c_str()));
ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
- (!fd.ok() ? std::strerror(errno) : "no error"));
+ !fd.ok() ? std::strerror(errno) : "ok");
reuse = true;
} else {
static char log_buf[1 << 20]; // 1 MiB logging buffer
@@ -1037,7 +1039,7 @@
ALOGD("BPF_PROG_LOAD call for %s (%s) returned '%s' fd: %d (%s)", elfPath,
cs[i].name.c_str(), log_oneline ? log_buf : "{multiline}",
- fd.get(), (!fd.ok() ? std::strerror(errno) : "ok"));
+ fd.get(), !fd.ok() ? std::strerror(errno) : "ok");
if (!fd.ok()) {
// kernel NULL terminates log_buf, so this checks for non-empty string
@@ -1066,14 +1068,14 @@
"tmp_prog_" + objName + '_' + string(name);
ret = bpfFdPin(fd, createLoc.c_str());
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
return -err;
}
ret = renameat2(AT_FDCWD, createLoc.c_str(),
AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
err, strerror(err));
return -err;
@@ -1081,19 +1083,19 @@
} else {
ret = bpfFdPin(fd, progPinLoc.c_str());
if (ret) {
- int err = errno;
+ const int err = errno;
ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
return -err;
}
}
if (chmod(progPinLoc.c_str(), 0440)) {
- int err = errno;
+ const int err = errno;
ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
return -err;
}
if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
(gid_t)cs[i].prog_def->gid)) {
- int err = errno;
+ const int err = errno;
ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
cs[i].prog_def->gid, err, strerror(err));
return -err;
@@ -1490,6 +1492,11 @@
if (!isTV()) return 1;
}
+ if (isKernel32Bit() && isAtLeast25Q2) {
+ ALOGE("Android 25Q2 requires 64 bit kernel.");
+ return 1;
+ }
+
// 6.6 is highest version supported by Android V, so this is effectively W+ (sdk=36+)
if (isKernel32Bit() && isAtLeastKernelVersion(6, 7, 0)) {
ALOGE("Android platform with 32 bit kernel version >= 6.7.0 is unsupported");
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index 6af7228..125f26b 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -318,7 +318,6 @@
RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
// initialized last so mCookieTagMap.isValid() implies everything else is valid too
RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH));
- ALOGI("%s successfully", __func__);
return netdutils::status::ok;
}
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 60a827b..51b4fc0 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -165,3 +165,12 @@
bug: "372936361"
is_fixed_read_only: true
}
+
+flag {
+ name: "restrict_local_network"
+ is_exported: true
+ namespace: "android_core_networking"
+ description: "Flag for controlling access to the local network behind a new runtime permission. Requires ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK to enable feature."
+ bug: "388774939"
+ is_fixed_read_only: true
+}
diff --git a/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
new file mode 100644
index 0000000..95265b9
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/LocalNetAccessKey.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 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.net.module.util.bpf;
+
+import com.android.net.module.util.InetAddressUtils;
+import com.android.net.module.util.Struct;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+public class LocalNetAccessKey extends Struct {
+
+ @Field(order = 0, type = Type.U32)
+ public final long lpmBitlen;
+ @Field(order = 1, type = Type.U32)
+ public final long ifIndex;
+ @Field(order = 2, type = Type.Ipv6Address)
+ public final Inet6Address remoteAddress;
+ @Field(order = 3, type = Type.U16)
+ public final int protocol;
+ @Field(order = 4, type = Type.UBE16)
+ public final int remotePort;
+
+ public LocalNetAccessKey(long lpmBitlen, long ifIndex, InetAddress remoteAddress, int protocol,
+ int remotePort) {
+ this.lpmBitlen = lpmBitlen;
+ this.ifIndex = ifIndex;
+ this.protocol = protocol;
+ this.remotePort = remotePort;
+
+ if (remoteAddress instanceof Inet4Address) {
+ this.remoteAddress = InetAddressUtils.v4MappedV6Address((Inet4Address) remoteAddress);
+ } else {
+ this.remoteAddress = (Inet6Address) remoteAddress;
+ }
+ }
+
+ public LocalNetAccessKey(long lpmBitlen, long ifIndex, Inet6Address remoteAddress, int protocol,
+ int remotePort) {
+ this.lpmBitlen = lpmBitlen;
+ this.ifIndex = ifIndex;
+ this.remoteAddress = remoteAddress;
+ this.protocol = protocol;
+ this.remotePort = remotePort;
+ }
+
+ @Override
+ public String toString() {
+ return "LocalNetAccessKey{"
+ + "lpmBitlen=" + lpmBitlen
+ + ", ifIndex=" + ifIndex
+ + ", remoteAddress=" + remoteAddress
+ + ", protocol=" + protocol
+ + ", remotePort=" + remotePort
+ + "}";
+ }
+}
diff --git a/framework/Android.bp b/framework/Android.bp
index a1c6a15..d6ee759 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -160,7 +160,10 @@
java_defaults {
name: "CronetJavaDefaults",
srcs: [":httpclient_api_sources"],
- static_libs: ["com.android.net.http.flags-aconfig-java"],
+ static_libs: [
+ "com.android.net.http.flags-aconfig-java",
+ "modules-utils-expresslog",
+ ],
libs: [
"androidx.annotation_annotation",
],
diff --git a/framework/src/android/net/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java
index f3773de..f1a6f00 100644
--- a/framework/src/android/net/BpfNetMapsConstants.java
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -60,6 +60,11 @@
"/sys/fs/bpf/netd_shared/map_netd_data_saver_enabled_map";
public static final String INGRESS_DISCARD_MAP_PATH =
"/sys/fs/bpf/netd_shared/map_netd_ingress_discard_map";
+ public static final String LOCAL_NET_ACCESS_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_local_net_access_map";
+ public static final String LOCAL_NET_BLOCKED_UID_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_local_net_blocked_uid_map";
+
public static final Struct.S32 UID_RULES_CONFIGURATION_KEY = new Struct.S32(0);
public static final Struct.S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new Struct.S32(1);
public static final Struct.S32 DATA_SAVER_ENABLED_KEY = new Struct.S32(0);
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index 1fbb3f3..fb42c03 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -199,7 +199,6 @@
}
LogListUpdateStatus updateStatus = mSignatureVerifier.verify(contentUri, metadataUri);
- // TODO(b/391327942): parse file and log the timestamp of the log list
if (!updateStatus.isSignatureVerified()) {
Log.w(TAG, "Log list did not pass verification");
@@ -209,42 +208,30 @@
return;
}
- boolean success = false;
-
try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
- success = compatVersion.install(inputStream);
+ updateStatus = compatVersion.install(inputStream, updateStatus.toBuilder());
} catch (IOException e) {
Log.e(TAG, "Could not install new content", e);
return;
}
- if (success) {
- // Reset the number of consecutive log list failure updates back to zero.
- mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
- mDataStore.store();
- } else {
- mLogger.logCTLogListUpdateStateChangedEvent(
- updateStatus
- .toBuilder()
- .setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS)
- .build());
- }
- }
+ mLogger.logCTLogListUpdateStateChangedEvent(updateStatus);
+ }
private void handleDownloadFailed(DownloadStatus status) {
Log.e(TAG, "Download failed with " + status);
- LogListUpdateStatus.Builder updateStatus = LogListUpdateStatus.builder();
+ LogListUpdateStatus.Builder updateStatusBuilder = LogListUpdateStatus.builder();
if (status.isHttpError()) {
- updateStatus
+ updateStatusBuilder
.setState(CTLogListUpdateState.HTTP_ERROR)
.setHttpErrorStatusCode(status.reason());
} else {
// TODO(b/384935059): handle blocked domain logging
- updateStatus.setDownloadStatus(Optional.of(status.reason()));
+ updateStatusBuilder.setDownloadStatus(Optional.of(status.reason()));
}
- mLogger.logCTLogListUpdateStateChangedEvent(updateStatus.build());
+ mLogger.logCTLogListUpdateStateChangedEvent(updateStatusBuilder.build());
}
private long download(String url) {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
index 967a04b..2a37d8f 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLogger.java
@@ -35,10 +35,12 @@
enum CTLogListUpdateState {
UNKNOWN_STATE,
HTTP_ERROR,
+ LOG_LIST_INVALID,
PUBLIC_KEY_NOT_FOUND,
SIGNATURE_INVALID,
SIGNATURE_NOT_FOUND,
SIGNATURE_VERIFICATION_FAILED,
+ SUCCESS,
VERSION_ALREADY_EXISTS
}
}
\ No newline at end of file
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
index 9c3210d..f617523 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyLoggerImpl.java
@@ -20,6 +20,7 @@
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DEVICE_OFFLINE;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_DOWNLOAD_CANNOT_RESUME;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_NO_DISK_SPACE;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_INVALID;
@@ -29,6 +30,7 @@
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_UNKNOWN;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__PENDING_WAITING_FOR_WIFI;
+import static com.android.server.net.ct.CertificateTransparencyStatsLog.CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
import android.app.DownloadManager;
@@ -43,6 +45,12 @@
@Override
public void logCTLogListUpdateStateChangedEvent(LogListUpdateStatus updateStatus) {
+ if (updateStatus.isSuccessful()) {
+ resetFailureCount();
+ } else {
+ updateFailureCount();
+ }
+
int updateState =
updateStatus
.downloadStatus()
@@ -56,20 +64,31 @@
updateState,
failureCount,
updateStatus.httpErrorStatusCode(),
- updateStatus.signature());
+ updateStatus.signature(),
+ updateStatus.logListTimestamp());
}
private void logCTLogListUpdateStateChangedEvent(
- int updateState, int failureCount, int httpErrorStatusCode, String signature) {
- updateFailureCount();
-
+ int updateState,
+ int failureCount,
+ int httpErrorStatusCode,
+ String signature,
+ long logListTimestamp) {
CertificateTransparencyStatsLog.write(
CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED,
updateState,
failureCount,
httpErrorStatusCode,
signature,
- /* logListTimestampMs= */ 0);
+ logListTimestamp);
+ }
+
+ /**
+ * Resets the number of consecutive log list update failures in the data store back to zero.
+ */
+ private void resetFailureCount() {
+ mDataStore.setPropertyInt(Config.LOG_LIST_UPDATE_FAILURE_COUNT, /* value= */ 0);
+ mDataStore.store();
}
/**
@@ -112,6 +131,8 @@
switch (updateState) {
case HTTP_ERROR:
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_HTTP_ERROR;
+ case LOG_LIST_INVALID:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_LOG_LIST_INVALID;
case PUBLIC_KEY_NOT_FOUND:
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_PUBLIC_KEY_NOT_FOUND;
case SIGNATURE_INVALID:
@@ -120,6 +141,8 @@
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_NOT_FOUND;
case SIGNATURE_VERIFICATION_FAILED:
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_SIGNATURE_VERIFICATION;
+ case SUCCESS:
+ return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__SUCCESS;
case VERSION_ALREADY_EXISTS:
return CERTIFICATE_TRANSPARENCY_LOG_LIST_UPDATE_STATE_CHANGED__UPDATE_STATUS__FAILURE_VERSION_ALREADY_EXISTS;
case UNKNOWN_STATE:
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
index 9d60163..e8a6e64 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -23,6 +23,8 @@
import android.system.Os;
import android.util.Log;
+import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
+
import org.json.JSONException;
import org.json.JSONObject;
@@ -69,22 +71,29 @@
* Installs a log list within this compatibility version directory.
*
* @param newContent an input stream providing the log list
+ * @param statusBuilder status obj builder containing details of the log list update process
* @return true if the log list was installed successfully, false otherwise.
* @throws IOException if the list cannot be saved in the CT directory.
*/
- boolean install(InputStream newContent) throws IOException {
+ LogListUpdateStatus install(
+ InputStream newContent, LogListUpdateStatus.Builder statusBuilder) throws IOException {
String content = new String(newContent.readAllBytes(), UTF_8);
try {
+ JSONObject contentJson = new JSONObject(content);
return install(
new ByteArrayInputStream(content.getBytes()),
- new JSONObject(content).getString("version"));
+ contentJson.getString("version"),
+ statusBuilder.setLogListTimestamp(contentJson.getLong("log_list_timestamp")));
} catch (JSONException e) {
Log.e(TAG, "invalid log list format", e);
- return false;
+
+ return statusBuilder.setState(CTLogListUpdateState.LOG_LIST_INVALID).build();
}
}
- private boolean install(InputStream newContent, String version) throws IOException {
+ LogListUpdateStatus install(
+ InputStream newContent, String version, LogListUpdateStatus.Builder statusBuilder)
+ throws IOException {
// To support atomically replacing the old configuration directory with the new
// there's a bunch of steps. We create a new directory with the logs and then do
// an atomic update of the current symlink to point to the new directory.
@@ -100,7 +109,7 @@
if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) {
Log.i(TAG, newLogsDir + " already exists, skipping install.");
deleteOldLogDirectories();
- return false;
+ return statusBuilder.setState(CTLogListUpdateState.VERSION_ALREADY_EXISTS).build();
}
// If the symlink has not been updated then the previous installation failed and
// this is a re-attempt. Clean-up leftover files and try again.
@@ -134,7 +143,7 @@
// 7. Cleanup
Log.i(TAG, "New logs installed at " + newLogsDir);
deleteOldLogDirectories();
- return true;
+ return statusBuilder.setState(CTLogListUpdateState.SUCCESS).build();
}
String getCompatVersion() {
diff --git a/networksecurity/service/src/com/android/server/net/ct/DataStore.java b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
index 8180316..1f99efa 100644
--- a/networksecurity/service/src/com/android/server/net/ct/DataStore.java
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -44,8 +44,9 @@
}
try (InputStream in = new FileInputStream(mPropertyFile)) {
load(in);
- } catch (IOException e) {
+ } catch (IOException | IllegalArgumentException e) {
Log.e(TAG, "Error loading property store", e);
+ delete();
}
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
index 3d05857..3f9b762 100644
--- a/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
+++ b/networksecurity/service/src/com/android/server/net/ct/LogListUpdateStatus.java
@@ -19,6 +19,7 @@
import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_INVALID;
import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_NOT_FOUND;
import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
import com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState;
@@ -52,6 +53,14 @@
return signature() != null && signature().length() > 0;
}
+ boolean isSuccessful() {
+ return state() == SUCCESS;
+ }
+
+ static LogListUpdateStatus getDefaultInstance() {
+ return builder().build();
+ }
+
@AutoValue.Builder
abstract static class Builder {
abstract Builder setState(CTLogListUpdateState updateState);
diff --git a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
index 3ba56db..6040ef6 100644
--- a/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
+++ b/networksecurity/service/src/com/android/server/net/ct/SignatureVerifier.java
@@ -32,7 +32,6 @@
import java.io.IOException;
import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
@@ -105,9 +104,9 @@
verifier.update(fileStream.readAllBytes());
byte[] signatureBytes = signatureStream.readAllBytes();
+ statusBuilder.setSignature(new String(signatureBytes));
try {
byte[] decodedSigBytes = Base64.getDecoder().decode(signatureBytes);
- statusBuilder.setSignature(new String(decodedSigBytes, StandardCharsets.UTF_8));
if (!verifier.verify(decodedSigBytes)) {
// Leave the UpdateState as UNKNOWN_STATE if successful as there are other
@@ -116,7 +115,6 @@
}
} catch (IllegalArgumentException e) {
Log.w(TAG, "Invalid signature base64 encoding", e);
- statusBuilder.setSignature(new String(signatureBytes, StandardCharsets.UTF_8));
statusBuilder.setState(SIGNATURE_INVALID);
return statusBuilder.build();
}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 08704d1..2af0122 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -24,8 +24,6 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
import android.app.DownloadManager;
import android.app.DownloadManager.Query;
import android.app.DownloadManager.Request;
@@ -56,7 +54,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@@ -84,6 +81,7 @@
private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
private long mNextDownloadId = 666;
+ private static final long LOG_LIST_TIMESTAMP = 123456789L;
@Before
public void setUp() throws IOException, GeneralSecurityException {
@@ -398,14 +396,12 @@
mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
// Assert
- byte[] signatureBytes = Base64.getDecoder().decode(toByteArray(metadataFile));
verify(mLogger, times(1))
.logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
assertThat(statusValue.state())
.isEqualTo(CTLogListUpdateState.SIGNATURE_VERIFICATION_FAILED);
- assertThat(statusValue.signature())
- .isEqualTo(new String(signatureBytes, StandardCharsets.UTF_8));
+ assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
}
@Test
@@ -422,13 +418,11 @@
mCertificateTransparencyDownloader.onReceive(
mContext, makeContentDownloadCompleteIntent(mCompatVersion, invalidLogListFile));
- byte[] signatureBytes = Base64.getDecoder().decode(toByteArray(metadataFile));
verify(mLogger, times(1))
.logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
- assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.VERSION_ALREADY_EXISTS);
- assertThat(statusValue.signature())
- .isEqualTo(new String(signatureBytes, StandardCharsets.UTF_8));
+ assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.LOG_LIST_INVALID);
+ assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
}
@Test
@@ -466,7 +460,8 @@
}
@Test
- public void testDownloader_endToEndSuccess_installNewVersion() throws Exception {
+ public void testDownloader_endToEndSuccess_installNewVersion_andLogsSuccess() throws Exception {
+ // Arrange
String newVersion = "456";
File logListFile = makeLogListFile(newVersion);
File metadataFile = sign(logListFile);
@@ -474,6 +469,7 @@
assertNoVersionIsInstalled();
+ // Act
// 1. Start download of public key.
mCertificateTransparencyDownloader.startPublicKeyDownload();
@@ -491,7 +487,15 @@
mCertificateTransparencyDownloader.onReceive(
mContext, makeContentDownloadCompleteIntent(mCompatVersion, logListFile));
+ // Assert
assertInstallSuccessful(newVersion);
+ verify(mLogger, times(1))
+ .logCTLogListUpdateStateChangedEvent(mUpdateStatusCaptor.capture());
+
+ LogListUpdateStatus statusValue = mUpdateStatusCaptor.getValue();
+ assertThat(statusValue.state()).isEqualTo(CTLogListUpdateState.SUCCESS);
+ assertThat(statusValue.signature()).isEqualTo(new String(toByteArray(metadataFile)));
+ assertThat(statusValue.logListTimestamp()).isEqualTo(LOG_LIST_TIMESTAMP);
}
private void assertNoVersionIsInstalled() {
@@ -600,7 +604,11 @@
File logListFile = File.createTempFile("log_list", "json");
try (OutputStream outputStream = new FileOutputStream(logListFile)) {
- outputStream.write(new JSONObject().put("version", version).toString().getBytes(UTF_8));
+ JSONObject contentJson =
+ new JSONObject()
+ .put("version", version)
+ .put("log_list_timestamp", LOG_LIST_TIMESTAMP);
+ outputStream.write(contentJson.toString().getBytes());
}
return logListFile;
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
index 38fff48..2b8b3cd 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CompatibilityVersionTest.java
@@ -15,6 +15,11 @@
*/
package com.android.server.net.ct;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.LOG_LIST_INVALID;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.SUCCESS;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.UNKNOWN_STATE;
+import static com.android.server.net.ct.CertificateTransparencyLogger.CTLogListUpdateState.VERSION_ALREADY_EXISTS;
+
import static com.google.common.truth.Truth.assertThat;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -37,6 +42,8 @@
public class CompatibilityVersionTest {
private static final String TEST_VERSION = "v123";
+ private static final long LOG_LIST_TIMESTAMP = 123456789L;
+ private static final String SIGNATURE = "fake_signature";
private final File mTestDir =
InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
@@ -52,6 +59,7 @@
@Test
public void testCompatibilityVersion_versionDirectory_setupSuccessful() {
File versionDir = mCompatVersion.getVersionDir();
+
assertThat(versionDir.exists()).isFalse();
assertThat(versionDir.getAbsolutePath()).startsWith(mTestDir.getAbsolutePath());
assertThat(versionDir.getAbsolutePath()).endsWith(TEST_VERSION);
@@ -60,6 +68,7 @@
@Test
public void testCompatibilityVersion_symlink_setupSuccessful() {
File dirSymlink = mCompatVersion.getLogsDirSymlink();
+
assertThat(dirSymlink.exists()).isFalse();
assertThat(dirSymlink.getAbsolutePath())
.startsWith(mCompatVersion.getVersionDir().getAbsolutePath());
@@ -68,18 +77,44 @@
@Test
public void testCompatibilityVersion_logsFile_setupSuccessful() {
File logsFile = mCompatVersion.getLogsFile();
+
assertThat(logsFile.exists()).isFalse();
assertThat(logsFile.getAbsolutePath())
.startsWith(mCompatVersion.getLogsDirSymlink().getAbsolutePath());
}
@Test
+ public void testCompatibilityVersion_installSuccessful_keepsStatusDetails() throws Exception {
+ String version = "i_am_version";
+ JSONObject logList = makeLogList(version, "i_am_content");
+
+ try (InputStream inputStream = asStream(logList)) {
+ assertThat(
+ mCompatVersion.install(
+ inputStream,
+ LogListUpdateStatus.builder()
+ .setSignature(SIGNATURE)
+ .setState(UNKNOWN_STATE)))
+ .isEqualTo(
+ LogListUpdateStatus.builder()
+ .setSignature(SIGNATURE)
+ .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+ // Ensure the state is correctly overridden to SUCCESS
+ .setState(SUCCESS)
+ .build());
+ }
+ }
+
+ @Test
public void testCompatibilityVersion_installSuccessful() throws Exception {
String version = "i_am_version";
JSONObject logList = makeLogList(version, "i_am_content");
try (InputStream inputStream = asStream(logList)) {
- assertThat(mCompatVersion.install(inputStream)).isTrue();
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(getSuccessfulUpdateStatus());
}
File logListFile = mCompatVersion.getLogsFile();
@@ -107,7 +142,10 @@
@Test
public void testCompatibilityVersion_deleteSuccessfully() throws Exception {
try (InputStream inputStream = asStream(makeLogList(/* version= */ "123"))) {
- assertThat(mCompatVersion.install(inputStream)).isTrue();
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(getSuccessfulUpdateStatus());
}
mCompatVersion.delete();
@@ -118,7 +156,10 @@
@Test
public void testCompatibilityVersion_invalidLogList() throws Exception {
try (InputStream inputStream = new ByteArrayInputStream(("not_a_valid_list".getBytes()))) {
- assertThat(mCompatVersion.install(inputStream)).isFalse();
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(LogListUpdateStatus.builder().setState(LOG_LIST_INVALID).build());
}
assertThat(mCompatVersion.getLogsFile().exists()).isFalse();
@@ -138,7 +179,10 @@
JSONObject newLogList = makeLogList(existingVersion, "i_am_the_real_content");
try (InputStream inputStream = asStream(newLogList)) {
- assertThat(mCompatVersion.install(inputStream)).isTrue();
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(getSuccessfulUpdateStatus());
}
assertThat(readAsString(logsListFile)).isEqualTo(newLogList.toString());
@@ -149,11 +193,21 @@
String existingVersion = "666";
JSONObject existingLogList = makeLogList(existingVersion, "i_was_installed_successfully");
try (InputStream inputStream = asStream(existingLogList)) {
- assertThat(mCompatVersion.install(inputStream)).isTrue();
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(getSuccessfulUpdateStatus());
}
try (InputStream inputStream = asStream(makeLogList(existingVersion, "i_am_ignored"))) {
- assertThat(mCompatVersion.install(inputStream)).isFalse();
+ assertThat(
+ mCompatVersion.install(
+ inputStream, LogListUpdateStatus.builder()))
+ .isEqualTo(
+ LogListUpdateStatus.builder()
+ .setState(VERSION_ALREADY_EXISTS)
+ .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+ .build());
}
assertThat(readAsString(mCompatVersion.getLogsFile()))
@@ -165,13 +219,22 @@
}
private static JSONObject makeLogList(String version) throws JSONException {
- return new JSONObject().put("version", version);
+ return new JSONObject()
+ .put("version", version)
+ .put("log_list_timestamp", LOG_LIST_TIMESTAMP);
}
private static JSONObject makeLogList(String version, String content) throws JSONException {
return makeLogList(version).put("content", content);
}
+ private static LogListUpdateStatus getSuccessfulUpdateStatus() {
+ return LogListUpdateStatus.builder()
+ .setState(SUCCESS)
+ .setLogListTimestamp(LOG_LIST_TIMESTAMP)
+ .build();
+ }
+
private static String readAsString(File file) throws IOException {
try (InputStream in = new FileInputStream(file)) {
return new String(in.readAllBytes());
diff --git a/remoteauth/service/jni/Android.bp b/remoteauth/service/jni/Android.bp
index 57e3ec9..c7ad738 100644
--- a/remoteauth/service/jni/Android.bp
+++ b/remoteauth/service/jni/Android.bp
@@ -24,9 +24,6 @@
"libasync_trait",
],
prefer_rlib: true,
- apex_available: [
- "com.android.remoteauth",
- ],
host_supported: true,
}
diff --git a/service/Android.bp b/service/Android.bp
index c4e2ef0..8b469e4 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -161,6 +161,7 @@
],
libs: [
"framework-annotations-lib",
+ "framework-bluetooth.stubs.module_lib",
"framework-configinfrastructure.stubs.module_lib",
"framework-connectivity-pre-jarjar",
// The framework-connectivity-t library is only available on T+ platforms
diff --git a/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem b/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem
new file mode 100644
index 0000000..80dccbe
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/raw/ct_public_keys.pem
@@ -0,0 +1,42 @@
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnmb1lacOnP5H1bwb06mG
+fEUeC9PZRwNQskSs9KaWrpfrSkLKuHXkVCbgeagbUR/Sh1OeIhyJRSS0PLCO0JjC
+UpGhYMrIGRgEET4IrP9f8aMFqxxxBUEanI+OxAhIJlP9tiWfGdKAASYcxg/DyXXz
+bqSXBEFJqBLqmnfHcLB/NhO1ejV6AiU1NMrT+iWSrJG8b6mq/LlAqWvidK8oPBsW
+87M4pPLpUoA54ultjx2wEzJ9dBy6jtnKZ/dz4DkDhYug5izRDcYtEfzQBoum0etV
+s4EoogW1AMeqW5G+L4HjPNgp3gNGZ/2RaBy7gp8Br+byYu2wHwdQIBQjS310yaKc
+nuNFOd+Q0DrzvHKB7yYzzdwo+hNocPpkvOzSw74jd09kDZQ+S2peCJ5NPU7VKT6/
+tkvc3tYA9pAu4/T+BGqRft4FjgeNANfSIX/WhWDzzVWymTUGFUvt+D0fF3Cw+XSa
+b8uTgRZ1Ma/FvSGgXHVoG9E4QAFFG4I9mmRqsnzA+8fqSNAfieL5OWecq4PU+pMa
+uNVJ9hbvmW2yXuMgEg6K9kFLdxggRn+OcxowgXJJh79L0r7RN1d8kuHelDhOzcte
+dUTtLNOb/1PA0d/I2IVJwc9xSQZXurqqT/Z+c01B3/R0BgGDkIT/yZ5iHPoZFYPD
+U8UdQzUK0KXgGkc8P5pJW8kCAwEAAQ==
+-----END PUBLIC KEY-----
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAn1ssK1m56VVurVJ/fNKe
++aXDnytBy/hWY48ZT+ZC0S29llfjkaCBlmkngoI2hgwz9BI6pHwUINS15pT1sznw
+kHoEaNKr8sANHTQ0PYlDuk5iQSjnaGz7XmqbZ3c92BQrmLG/kwX2c17YNJI2qCk3
+MaBJBeS5YErR3xcucT9M7qtNWWIT9O0sLV1lDUZVCYedWBSnNz1/mAiLhttWmU5u
+GKl/5LmjWP/piNjh8whx0abJUGeGS2HH0JAOb0pjBV6UQvj3tO+gTiNDhdrE4CKh
+Qn8SKNjW/BY320f0A5t581Q0++cQUAisRgBQos0Pkvg5vb6wgII+pJq0SnZoYFfH
+oycuR4q3eOCgJmpEAPC0MhNpIDUQS6p3QabD9ID+21ymiQa/Zf8Mv2xMM6ZItKxX
+77vSKvBbimTGmB2IU+Zi484PKI5QwxBUCHVSmNpvHyXyjhBmpqik9Op26QYYT10b
+ADnJY1L/Q+i44nI4pfwgIncqAWuLnxg/XggJDWj48Un9SMNoyN7gzQX75M7rh7/t
+F6QtKvJreP0pP2UoVSgZKjXnL9tqeZbOdZU1kBHi1HOhlUKTfq5dn2fVUeYkE769
+clFF7Y1FiI259IPhTKiOIfARJ4BL4Sn8D9c9vpxDYPFl5bCJbspmFpwfzTMDnGVS
+/IlY6Putpv2/lD1B7aQGt1sCAwEAAQ==
+-----END PUBLIC KEY-----
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwMPNecDhLDamK9Rs8W5V
+c0LWZPbk9FNP+budBoFMX46dWyJ75O5e8xY9UhJSBFl8+Itm/rWO/h2zZ6qE3Cq8
+1LYbOg+x+rLnYOcAvD2O7EU73A3RD/vqoUDDVK3cwKMq3ry7CYu+NJW7TRIKV7Ct
+BMCBrvpmC1WlZ/jxSV0Soapza4/H+UV0hHYh/Wn3EWObGWYdI3yxwZ81AyU1QCR/
+oaO0PQPXqvo3gPBINnK3Qr0aLtYc4YCfTXe6i4g3DeAlkpqNLZtC2hyqiRB4Dg47
+zDzYECGofRAu8w9d8uI+eccacXfvI9zEcL7FAl5AzBKmMFOfBNTr08V7+aROWfGO
+7imWsj2MQ6RG17zqJak5QX/1bqDxwhG0KolB86mPZu0WeKz1B3iP5qAUlDNBHLDV
+pQIez0mrMsVsimVguuLYHMpIgijphA9WhijCJW2x7c6aocB6IpnMIV1sqnUQTwLG
+t32AMrckxqFmaKGj/8I9M+Xj+Cy4fIa5YSOdb/tlaYZSfjH5ch41xucQ2HWFyZ/9
+hkTFodvF5ajCQ5maHeIjDkS/Bc/s9CB+/fbSkstDsPMRp/ExyQcEYjKTG5o9Ewyo
++KGGXS2dSS10Ibl0Zx/S/0ZuZx8ZAxMOIIPpugdkWqHU9thh71dR8zM4KMkEfB8C
+sWLGB1yMuztn9nRUcpiEZTECAwEAAQ==
+-----END PUBLIC KEY-----
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 44868b2d..4fae73a 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -25,6 +25,8 @@
import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
import static android.net.BpfNetMapsConstants.IIF_MATCH;
import static android.net.BpfNetMapsConstants.INGRESS_DISCARD_MAP_PATH;
+import static android.net.BpfNetMapsConstants.LOCAL_NET_ACCESS_MAP_PATH;
+import static android.net.BpfNetMapsConstants.LOCAL_NET_BLOCKED_UID_MAP_PATH;
import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH;
@@ -74,6 +76,7 @@
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.SingleWriterBpfMap;
import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Bool;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U32;
import com.android.net.module.util.Struct.U8;
@@ -81,6 +84,7 @@
import com.android.net.module.util.bpf.CookieTagMapValue;
import com.android.net.module.util.bpf.IngressDiscardKey;
import com.android.net.module.util.bpf.IngressDiscardValue;
+import com.android.net.module.util.bpf.LocalNetAccessKey;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -131,6 +135,9 @@
private static IBpfMap<S32, U8> sDataSaverEnabledMap = null;
private static IBpfMap<IngressDiscardKey, IngressDiscardValue> sIngressDiscardMap = null;
+ private static IBpfMap<LocalNetAccessKey, Bool> sLocalNetAccessMap = null;
+ private static IBpfMap<U32, Bool> sLocalNetBlockedUidMap = null;
+
private static final List<Pair<Integer, String>> PERMISSION_LIST = Arrays.asList(
Pair.create(PERMISSION_INTERNET, "PERMISSION_INTERNET"),
Pair.create(PERMISSION_UPDATE_DEVICE_STATS, "PERMISSION_UPDATE_DEVICE_STATS")
@@ -186,6 +193,25 @@
sIngressDiscardMap = ingressDiscardMap;
}
+ /**
+ * Set localNetAccessMap for test.
+ */
+ @VisibleForTesting
+ public static void setLocalNetAccessMapForTest(
+ IBpfMap<LocalNetAccessKey, Bool> localNetAccessMap) {
+ sLocalNetAccessMap = localNetAccessMap;
+ }
+
+ /**
+ * Set localNetBlockedUidMap for test.
+ */
+ @VisibleForTesting
+ public static void setLocalNetBlockedUidMapForTest(
+ IBpfMap<U32, Bool> localNetBlockedUidMap) {
+ sLocalNetBlockedUidMap = localNetBlockedUidMap;
+ }
+
+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static IBpfMap<S32, U32> getConfigurationMap() {
try {
@@ -247,6 +273,26 @@
}
}
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ private static IBpfMap<U32, Bool> getLocalNetBlockedUidMap() {
+ try {
+ return SingleWriterBpfMap.getSingleton(LOCAL_NET_BLOCKED_UID_MAP_PATH,
+ U32.class, Bool.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open local_net_blocked_uid map", e);
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ private static IBpfMap<LocalNetAccessKey, Bool> getLocalNetAccessMap() {
+ try {
+ return SingleWriterBpfMap.getSingleton(LOCAL_NET_ACCESS_MAP_PATH,
+ LocalNetAccessKey.class, Bool.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open local_net_access map", e);
+ }
+ }
+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static void initBpfMaps() {
if (sConfigurationMap == null) {
@@ -299,6 +345,27 @@
} catch (ErrnoException e) {
throw new IllegalStateException("Failed to initialize ingress discard map", e);
}
+
+ if (isAtLeast25Q2()) {
+ if (sLocalNetAccessMap == null) {
+ sLocalNetAccessMap = getLocalNetAccessMap();
+ }
+ try {
+ sLocalNetAccessMap.clear();
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Failed to initialize local_net_access map", e);
+ }
+
+ if (sLocalNetBlockedUidMap == null) {
+ sLocalNetBlockedUidMap = getLocalNetBlockedUidMap();
+ }
+ try {
+ sLocalNetBlockedUidMap.clear();
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Failed to initialize local_net_blocked_uid map",
+ e);
+ }
+ }
}
/**
@@ -387,6 +454,21 @@
}
}
+ private void throwIfPre25Q2(final String msg) {
+ if (!isAtLeast25Q2()) {
+ throw new UnsupportedOperationException(msg);
+ }
+ }
+
+ /*
+ ToDo : Remove this method when SdkLevel.isAtLeastB() is fixed, aosp is at sdk level 36 or use
+ NetworkStackUtils.isAtLeast25Q2 when it is moved to a static lib.
+ */
+ public static boolean isAtLeast25Q2() {
+ return SdkLevel.isAtLeastB() || (SdkLevel.isAtLeastV()
+ && "Baklava".equals(Build.VERSION.CODENAME));
+ }
+
private void removeRule(final int uid, final long match, final String caller) {
try {
synchronized (sUidOwnerMap) {
@@ -810,6 +892,113 @@
}
/**
+ * Add configuration to local_net_access trie map.
+ * @param lpmBitlen prefix length that will be used for longest matching
+ * @param iface interface name
+ * @param address remote address. ipv4 addresses would be mapped to v6
+ * @param protocol required for longest match in special cases
+ * @param remotePort src/dst port for ingress/egress
+ * @param isAllowed is the local network call allowed or blocked.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void addLocalNetAccess(final int lpmBitlen, final String iface,
+ final InetAddress address, final int protocol, final int remotePort,
+ final boolean isAllowed) {
+ throwIfPre25Q2("addLocalNetAccess is not available on pre-B devices");
+ final int ifIndex = mDeps.getIfIndex(iface);
+ if (ifIndex == 0) {
+ Log.e(TAG, "Failed to get if index, skip addLocalNetAccess for " + address
+ + "(" + iface + ")");
+ return;
+ }
+ LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+ address, protocol, remotePort);
+
+ try {
+ sLocalNetAccessMap.updateEntry(localNetAccessKey, new Bool(isAllowed));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to add local network access for localNetAccessKey : "
+ + localNetAccessKey + ", isAllowed : " + isAllowed);
+ }
+ }
+
+ /**
+ * False if the configuration is disallowed.
+ *
+ * @param lpmBitlen prefix length that will be used for longest matching
+ * @param iface interface name
+ * @param address remote address. ipv4 addresses would be mapped to v6
+ * @param protocol required for longest match in special cases
+ * @param remotePort src/dst port for ingress/egress
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public boolean getLocalNetAccess(final int lpmBitlen, final String iface,
+ final InetAddress address, final int protocol, final int remotePort) {
+ throwIfPre25Q2("getLocalNetAccess is not available on pre-B devices");
+ final int ifIndex = mDeps.getIfIndex(iface);
+ if (ifIndex == 0) {
+ Log.e(TAG, "Failed to get if index, returning default from getLocalNetAccess for "
+ + address + "(" + iface + ")");
+ return true;
+ }
+ LocalNetAccessKey localNetAccessKey = new LocalNetAccessKey(lpmBitlen, ifIndex,
+ address, protocol, remotePort);
+ try {
+ Bool value = sLocalNetAccessMap.getValue(localNetAccessKey);
+ return value == null ? true : value.val;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to find local network access configuration for "
+ + "localNetAccessKey : " + localNetAccessKey);
+ }
+ return true;
+ }
+
+ /**
+ * Add uid to local_net_blocked_uid map.
+ * @param uid application uid that needs to block local network calls.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void addUidToLocalNetBlockMap(final int uid) {
+ throwIfPre25Q2("addUidToLocalNetBlockMap is not available on pre-B devices");
+ try {
+ sLocalNetBlockedUidMap.updateEntry(new U32(uid), new Bool(true));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to add local network blocked for uid : " + uid);
+ }
+ }
+
+ /**
+ * True if local network calls are blocked for application.
+ * @param uid application uid that needs check if local network calls are blocked.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public boolean getUidValueFromLocalNetBlockMap(final int uid) {
+ throwIfPre25Q2("getUidValueFromLocalNetBlockMap is not available on pre-B devices");
+ try {
+ Bool value = sLocalNetBlockedUidMap.getValue(new U32(uid));
+ return value == null ? false : value.val;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to find uid(" + uid
+ + ") is present in local network blocked map");
+ }
+ return false;
+ }
+
+ /**
+ * Remove uid from local_net_blocked_uid map(if present).
+ * @param uid application uid that needs check if local network calls are blocked.
+ */
+ @RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
+ public void removeUidFromLocalNetBlockMap(final int uid) {
+ throwIfPre25Q2("removeUidFromLocalNetBlockMap is not available on pre-B devices");
+ try {
+ sLocalNetBlockedUidMap.deleteEntry(new U32(uid));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to remove uid(" + uid + ") from local network blocked map");
+ }
+ }
+
+ /**
* Get granted permissions for specified uid. If uid is not in the map, this method returns
* {@link android.net.INetd.PERMISSION_INTERNET} since this is a default permission.
* See {@link #setNetPermForUids}
@@ -1079,6 +1268,14 @@
(key, value) -> "[" + key.dstAddr + "]: "
+ value.iif1 + "(" + mDeps.getIfName(value.iif1) + "), "
+ value.iif2 + "(" + mDeps.getIfName(value.iif2) + ")");
+ if (sLocalNetBlockedUidMap != null) {
+ BpfDump.dumpMap(sLocalNetAccessMap, pw, "sLocalNetAccessMap",
+ (key, value) -> "[" + key + "]: " + value);
+ }
+ if (sLocalNetBlockedUidMap != null) {
+ BpfDump.dumpMap(sLocalNetBlockedUidMap, pw, "sLocalNetBlockedUidMap",
+ (key, value) -> "[" + key + "]: " + value);
+ }
dumpDataSaverConfig(pw);
pw.decreaseIndent();
}
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
index c5ec9ee..15c860b 100644
--- a/service/src/com/android/server/L2capNetworkProvider.java
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -30,8 +30,15 @@
import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE;
+import static android.system.OsConstants.F_GETFL;
+import static android.system.OsConstants.F_SETFL;
+import static android.system.OsConstants.O_NONBLOCK;
import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
@@ -45,23 +52,49 @@
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
-import android.util.ArrayMap;
+import android.os.ParcelFileDescriptor;
+import android.system.Os;
+import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.ServiceConnectivityJni;
+import com.android.server.net.L2capNetwork;
-import java.util.Map;
+import java.io.IOException;
+import java.util.Set;
public class L2capNetworkProvider {
private static final String TAG = L2capNetworkProvider.class.getSimpleName();
+ private static final NetworkCapabilities COMMON_CAPABILITIES =
+ // TODO: add NET_CAPABILITY_NOT_RESTRICTED and check that getRequestorUid() has
+ // BLUETOOTH_CONNECT permission.
+ NetworkCapabilities.Builder.withoutDefaultCapabilities()
+ .addTransportType(TRANSPORT_BLUETOOTH)
+ // TODO: remove NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED.
+ .addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .addCapability(NET_CAPABILITY_NOT_METERED)
+ .addCapability(NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .addCapability(NET_CAPABILITY_NOT_VPN)
+ .build();
private final Dependencies mDeps;
private final Context mContext;
private final HandlerThread mHandlerThread;
private final Handler mHandler;
private final NetworkProvider mProvider;
private final BlanketReservationOffer mBlanketOffer;
- private final Map<Integer, ReservedServerOffer> mReservedServerOffers = new ArrayMap<>();
+ private final Set<ReservedServerOffer> mReservedServerOffers = new ArraySet<>();
+ // mBluetoothManager guaranteed non-null when read on handler thread after start() is called
+ @Nullable
+ private BluetoothManager mBluetoothManager;
+
+ // Note: IFNAMSIZ is 16.
+ private static final String TUN_IFNAME = "l2cap-tun";
+ private static int sTunIndex = 0;
/**
* The blanket reservation offer is used to create an L2CAP server network, i.e. a network
@@ -71,28 +104,24 @@
* requests that do not have a NetworkSpecifier set.
*/
private class BlanketReservationOffer implements NetworkOfferCallback {
- // TODO: ensure that once the incoming request is satisfied, the blanket offer does not get
- // unneeded. This means the blanket offer must always outscore the reserved offer. This
- // might require setting the blanket offer as setTransportPrimary().
- public static final NetworkScore SCORE = new NetworkScore.Builder().build();
+ // Set as transport primary to ensure that the BlanketReservationOffer always outscores the
+ // ReservedServerOffer, because as soon as the BlanketReservationOffer receives an
+ // onNetworkUnneeded() callback, it will tear down the associated reserved offer.
+ public static final NetworkScore SCORE = new NetworkScore.Builder()
+ .setTransportPrimary(true)
+ .build();
// Note the missing NET_CAPABILITY_NOT_RESTRICTED marking the network as restricted.
public static final NetworkCapabilities CAPABILITIES;
static {
+ // Below capabilities will match any reservation request with an L2capNetworkSpecifier
+ // that specifies ROLE_SERVER or without a NetworkSpecifier.
final L2capNetworkSpecifier l2capNetworkSpecifier = new L2capNetworkSpecifier.Builder()
.setRole(ROLE_SERVER)
.build();
- NetworkCapabilities caps = NetworkCapabilities.Builder.withoutDefaultCapabilities()
- .addTransportType(TRANSPORT_BLUETOOTH)
- // TODO: consider removing NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED.
- .addCapability(NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED)
- .addCapability(NET_CAPABILITY_NOT_CONGESTED)
- .addCapability(NET_CAPABILITY_NOT_METERED)
- .addCapability(NET_CAPABILITY_NOT_ROAMING)
- .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
- .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
- .addCapability(NET_CAPABILITY_NOT_VPN)
+ NetworkCapabilities caps = new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
.setNetworkSpecifier(l2capNetworkSpecifier)
.build();
+ // TODO: add #setReservationId() to NetworkCapabilities.Builder
caps.setReservationId(RES_ID_MATCH_ALL_RESERVATIONS);
CAPABILITIES = caps;
}
@@ -127,51 +156,129 @@
return;
}
- final NetworkCapabilities reservationCaps = request.networkCapabilities;
- final ReservedServerOffer reservedOffer = new ReservedServerOffer(reservationCaps);
+ final ReservedServerOffer reservedOffer = createReservedServerOffer(request);
+ if (reservedOffer == null) {
+ // Something went wrong when creating the offer. Send onUnavailable() to the app.
+ Log.e(TAG, "Failed to create L2cap server offer");
+ mProvider.declareNetworkRequestUnfulfillable(request);
+ return;
+ }
final NetworkCapabilities reservedCaps = reservedOffer.getReservedCapabilities();
mProvider.registerNetworkOffer(SCORE, reservedCaps, mHandler::post, reservedOffer);
- mReservedServerOffers.put(request.requestId, reservedOffer);
+ mReservedServerOffers.add(reservedOffer);
+ }
+
+ @Nullable
+ private ReservedServerOffer createReservedServerOffer(NetworkRequest reservation) {
+ final BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
+ if (bluetoothAdapter == null) {
+ Log.w(TAG, "Failed to get BluetoothAdapter");
+ return null;
+ }
+ final BluetoothServerSocket serverSocket;
+ try {
+ serverSocket = bluetoothAdapter.listenUsingInsecureL2capChannel();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to open BluetoothServerSocket");
+ return null;
+ }
+
+ // Create the reserved capabilities partially from the reservation itself (non-reserved
+ // parts of the L2capNetworkSpecifier), the COMMON_CAPABILITIES, and the reserved data
+ // (BLE L2CAP PSM from the BluetoothServerSocket).
+ final NetworkCapabilities reservationNc = reservation.networkCapabilities;
+ final L2capNetworkSpecifier reservationSpec =
+ (L2capNetworkSpecifier) reservationNc.getNetworkSpecifier();
+ // Note: the RemoteAddress is unspecified for server networks.
+ final L2capNetworkSpecifier reservedSpec = new L2capNetworkSpecifier.Builder()
+ .setRole(ROLE_SERVER)
+ .setHeaderCompression(reservationSpec.getHeaderCompression())
+ .setPsm(serverSocket.getPsm())
+ .build();
+ NetworkCapabilities reservedNc =
+ new NetworkCapabilities.Builder(COMMON_CAPABILITIES)
+ .setNetworkSpecifier(reservedSpec)
+ .build();
+ reservedNc.setReservationId(reservationNc.getReservationId());
+ return new ReservedServerOffer(reservedNc, serverSocket);
+ }
+
+ @Nullable
+ private ReservedServerOffer getReservedOfferForRequest(NetworkRequest request) {
+ final int rId = request.networkCapabilities.getReservationId();
+ for (ReservedServerOffer offer : mReservedServerOffers) {
+ // Comparing by reservationId is more explicit then using canBeSatisfiedBy() or the
+ // requestId.
+ if (offer.getReservedCapabilities().getReservationId() != rId) continue;
+ return offer;
+ }
+ return null;
}
@Override
public void onNetworkUnneeded(NetworkRequest request) {
- if (!mReservedServerOffers.containsKey(request.requestId)) {
- return;
- }
+ final ReservedServerOffer reservedOffer = getReservedOfferForRequest(request);
+ if (reservedOffer == null) return;
- final ReservedServerOffer reservedOffer = mReservedServerOffers.get(request.requestId);
// Note that the reserved offer gets torn down when the reservation goes away, even if
- // there are lingering requests.
- reservedOffer.tearDown();
- mProvider.unregisterNetworkOffer(reservedOffer);
+ // there are active (non-reservation) requests for said offer.
+ destroyAndUnregisterReservedOffer(reservedOffer);
}
}
+ private void destroyAndUnregisterReservedOffer(ReservedServerOffer reservedOffer) {
+ // Ensure the offer still exists if this was posted on the handler.
+ if (!mReservedServerOffers.contains(reservedOffer)) return;
+ mReservedServerOffers.remove(reservedOffer);
+
+ reservedOffer.tearDown();
+ mProvider.unregisterNetworkOffer(reservedOffer);
+ }
+
+ @Nullable
+ private static ParcelFileDescriptor createTunInterface(String ifname) {
+ final ParcelFileDescriptor fd;
+ try {
+ fd = ParcelFileDescriptor.adoptFd(
+ ServiceConnectivityJni.createTunTap(
+ true /*isTun*/, true /*hasCarrier*/, true /*setIffMulticast*/, ifname));
+ ServiceConnectivityJni.bringUpInterface(ifname);
+ // TODO: consider adding a parameter to createTunTap() (or the Builder that should
+ // be added) to configure i/o blocking.
+ final int flags = Os.fcntlInt(fd.getFileDescriptor(), F_GETFL, 0);
+ Os.fcntlInt(fd.getFileDescriptor(), F_SETFL, flags & ~O_NONBLOCK);
+ } catch (Exception e) {
+ // Note: createTunTap currently throws an IllegalStateException on failure.
+ // TODO: native functions should throw ErrnoException.
+ Log.e(TAG, "Failed to create tun interface", e);
+ return null;
+ }
+ return fd;
+ }
+
+ @Nullable
+ private L2capNetwork createL2capNetwork(BluetoothSocket socket, NetworkCapabilities caps,
+ L2capNetwork.ICallback cb) {
+ final String ifname = TUN_IFNAME + String.valueOf(sTunIndex++);
+ final ParcelFileDescriptor tunFd = createTunInterface(ifname);
+ if (tunFd == null) {
+ return null;
+ }
+
+ return new L2capNetwork(mHandler, mContext, mProvider, ifname, socket, tunFd, caps, cb);
+ }
+
+
private class ReservedServerOffer implements NetworkOfferCallback {
- private final boolean mUseHeaderCompression;
- private final int mPsm;
private final NetworkCapabilities mReservedCapabilities;
+ private final BluetoothServerSocket mServerSocket;
- public ReservedServerOffer(NetworkCapabilities reservationCaps) {
- // getNetworkSpecifier() is guaranteed to return a non-null L2capNetworkSpecifier.
- final L2capNetworkSpecifier reservationSpec =
- (L2capNetworkSpecifier) reservationCaps.getNetworkSpecifier();
- mUseHeaderCompression =
- reservationSpec.getHeaderCompression() == HEADER_COMPRESSION_6LOWPAN;
-
- // TODO: open BluetoothServerSocket and allocate a PSM.
- mPsm = 0x80;
-
- final L2capNetworkSpecifier reservedSpec = new L2capNetworkSpecifier.Builder()
- .setRole(ROLE_SERVER)
- .setHeaderCompression(reservationSpec.getHeaderCompression())
- .setPsm(mPsm)
- .build();
- mReservedCapabilities = new NetworkCapabilities.Builder(reservationCaps)
- .setNetworkSpecifier(reservedSpec)
- .build();
+ public ReservedServerOffer(NetworkCapabilities reservedCapabilities,
+ BluetoothServerSocket serverSocket) {
+ mReservedCapabilities = reservedCapabilities;
+ // TODO: ServerSocket will be managed by an AcceptThread.
+ mServerSocket = serverSocket;
}
public NetworkCapabilities getReservedCapabilities() {
@@ -194,8 +301,11 @@
* This method can be called multiple times.
*/
public void tearDown() {
- // TODO: implement.
- // This method can be called multiple times.
+ try {
+ mServerSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close BluetoothServerSocket", e);
+ }
}
}
@@ -234,11 +344,20 @@
* Called on CS Handler thread.
*/
public void start() {
- final PackageManager pm = mContext.getPackageManager();
- if (pm.hasSystemFeature(FEATURE_BLUETOOTH_LE)) {
+ mHandler.post(() -> {
+ final PackageManager pm = mContext.getPackageManager();
+ if (!pm.hasSystemFeature(FEATURE_BLUETOOTH_LE)) {
+ return;
+ }
+ mBluetoothManager = mContext.getSystemService(BluetoothManager.class);
+ if (mBluetoothManager == null) {
+ // Can this ever happen?
+ Log.wtf(TAG, "BluetoothManager not found");
+ return;
+ }
mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(mProvider);
mProvider.registerNetworkOffer(BlanketReservationOffer.SCORE,
BlanketReservationOffer.CAPABILITIES, mHandler::post, mBlanketOffer);
- }
+ });
}
}
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 2686e4a..e762a8e 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -1128,6 +1128,7 @@
int delta = add ? +1 : -1;
switch (request.type) {
case REQUEST:
+ case RESERVATION:
mNumRequestNetworkRequests += delta;
break;
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index beaa174..737e27a 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -31,6 +31,7 @@
import static android.net.INetd.PERMISSION_UNINSTALLED;
import static android.net.INetd.PERMISSION_UPDATE_DEVICE_STATS;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.net.connectivity.ConnectivityCompatChanges.RESTRICT_LOCAL_NETWORK;
import static android.os.Process.INVALID_UID;
import static android.os.Process.SYSTEM_UID;
@@ -38,6 +39,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.compat.CompatChanges;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -70,6 +72,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.flags.Flags;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.ProcessShimImpl;
@@ -279,6 +282,13 @@
mContext = context;
mBpfNetMaps = bpfNetMaps;
mThread = thread;
+ if (BpfNetMaps.isAtLeast25Q2()) {
+ // Local net restrictions is supported as a developer opt-in starting in Android B.
+ // This listener should finish registration by the time the system has completed
+ // boot setup such that any changes to runtime permissions for local network
+ // restrictions can only occur after this registration has completed.
+ mPackageManager.addOnPermissionsChangeListener(new PermissionChangeListener());
+ }
}
private void ensureRunningOnHandlerThread() {
@@ -1311,4 +1321,20 @@
private static void loge(String s, Throwable e) {
Log.e(TAG, s, e);
}
+
+ private class PermissionChangeListener implements PackageManager.OnPermissionsChangedListener {
+ @Override
+ public void onPermissionsChanged(int uid) {
+ // RESTRICT_LOCAL_NETWORK is a compat change that is enabled when developers manually
+ // opt-in to this change, or when the app's targetSdkVersion is greater than 36.
+ // The RESTRICT_LOCAL_NETWORK compat change is used here instead of the
+ // Flags.restrictLocalNetwork() is used to offer the feature to devices, but it will
+ // only be enforced when develoeprs choose to enable it.
+ // TODO(b/394567896): Update compat change checks
+ if (CompatChanges.isChangeEnabled(RESTRICT_LOCAL_NETWORK, uid)
+ && BpfNetMaps.isAtLeast25Q2()) {
+ // TODO(b/388803658): Update network permissions and record change
+ }
+ }
+ }
}
diff --git a/service/src/com/android/server/net/L2capNetwork.java b/service/src/com/android/server/net/L2capNetwork.java
new file mode 100644
index 0000000..b9d5f13
--- /dev/null
+++ b/service/src/com/android/server/net/L2capNetwork.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2025 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.annotation.Nullable;
+import android.bluetooth.BluetoothSocket;
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkScore;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.net.ip.IpClientUtil;
+import android.net.shared.ProvisioningConfiguration;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+public class L2capNetwork {
+ private static final NetworkScore NETWORK_SCORE = new NetworkScore.Builder().build();
+ private final String mLogTag;
+ private final Handler mHandler;
+ private final String mIfname;
+ private final L2capPacketForwarder mForwarder;
+ private final NetworkCapabilities mNetworkCapabilities;
+ private final L2capIpClient mIpClient;
+ private final NetworkAgent mNetworkAgent;
+
+ /** IpClient wrapper to handle IPv6 link-local provisioning for L2CAP tun.
+ *
+ * Note that the IpClient does not need to be stopped.
+ */
+ private static class L2capIpClient extends IpClientCallbacks {
+ private final String mLogTag;
+ private final ConditionVariable mOnIpClientCreatedCv = new ConditionVariable(false);
+ private final ConditionVariable mOnProvisioningSuccessCv = new ConditionVariable(false);
+ @Nullable
+ private IpClientManager mIpClient;
+ @Nullable
+ private LinkProperties mLinkProperties;
+
+ L2capIpClient(String logTag, Context context, String ifname) {
+ mLogTag = logTag;
+ IpClientUtil.makeIpClient(context, ifname, this);
+ }
+
+ @Override
+ public void onIpClientCreated(IIpClient ipClient) {
+ mIpClient = new IpClientManager(ipClient, mLogTag);
+ mOnIpClientCreatedCv.open();
+ }
+
+ @Override
+ public void onProvisioningSuccess(LinkProperties lp) {
+ Log.d(mLogTag, "Successfully provisionined l2cap tun: " + lp);
+ mLinkProperties = lp;
+ mOnProvisioningSuccessCv.open();
+ }
+
+ public LinkProperties start() {
+ mOnIpClientCreatedCv.block();
+ // mIpClient guaranteed non-null.
+ final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+ .withoutIPv4()
+ .withIpv6LinkLocalOnly()
+ .withRandomMacAddress() // addr_gen_mode EUI64 -> random on tun.
+ .build();
+ mIpClient.startProvisioning(config);
+ // "Provisioning" is guaranteed to succeed as link-local only mode does not actually
+ // require any provisioning.
+ mOnProvisioningSuccessCv.block();
+ return mLinkProperties;
+ }
+ }
+
+ public interface ICallback {
+ /** Called when an error is encountered */
+ void onError(L2capNetwork network);
+ /** Called when CS triggers NetworkAgent#onNetworkUnwanted */
+ void onNetworkUnwanted(L2capNetwork network);
+ }
+
+ public L2capNetwork(Handler handler, Context context, NetworkProvider provider, String ifname,
+ BluetoothSocket socket, ParcelFileDescriptor tunFd,
+ NetworkCapabilities networkCapabilities, ICallback cb) {
+ // TODO: add a check that this constructor is invoked on the handler thread.
+ mLogTag = String.format("L2capNetwork[%s]", ifname);
+ mHandler = handler;
+ mIfname = ifname;
+ mForwarder = new L2capPacketForwarder(handler, tunFd, socket, () -> {
+ // TODO: add a check that this callback is invoked on the handler thread.
+ cb.onError(L2capNetwork.this);
+ });
+ mNetworkCapabilities = networkCapabilities;
+ mIpClient = new L2capIpClient(mLogTag, context, ifname);
+ final LinkProperties linkProperties = mIpClient.start();
+
+ final NetworkAgentConfig config = new NetworkAgentConfig.Builder().build();
+ mNetworkAgent = new NetworkAgent(context, mHandler.getLooper(), mLogTag,
+ networkCapabilities, linkProperties, NETWORK_SCORE, config, provider) {
+ @Override
+ public void onNetworkUnwanted() {
+ Log.i(mLogTag, mIfname + ": Network is unwanted");
+ // TODO: add a check that this callback is invoked on the handler thread.
+ cb.onNetworkUnwanted(L2capNetwork.this);
+ }
+ };
+ mNetworkAgent.register();
+ mNetworkAgent.markConnected();
+ }
+
+ /** Get the NetworkCapabilities used for this Network */
+ public NetworkCapabilities getNetworkCapabilities() {
+ return mNetworkCapabilities;
+ }
+
+ /** Tear down the network and associated resources */
+ public void tearDown() {
+ mNetworkAgent.unregister();
+ mForwarder.tearDown();
+ }
+}
diff --git a/service/src/com/android/server/net/L2capPacketForwarder.java b/service/src/com/android/server/net/L2capPacketForwarder.java
new file mode 100644
index 0000000..cef351c
--- /dev/null
+++ b/service/src/com/android/server/net/L2capPacketForwarder.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2025 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.bluetooth.BluetoothSocket;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Forwards packets from a BluetoothSocket of type L2CAP to a tun fd and vice versa.
+ *
+ * The forwarding logic operates on raw IP packets and there are no ethernet headers.
+ * Therefore, L3 MTU = L2 MTU.
+ */
+public class L2capPacketForwarder {
+ private static final String TAG = "L2capPacketForwarder";
+
+ // DCT specifies an MTU of 1500.
+ // TODO: Set /proc/sys/net/ipv6/conf/${iface}/mtu to 1280 and the link MTU to 1528 to accept
+ // slightly larger packets on ingress (i.e. packets passing through a NAT64 gateway).
+ // MTU determines the value of the read buffers, so use the larger of the two.
+ @VisibleForTesting
+ public static final int MTU = 1528;
+ private final Handler mHandler;
+ private final IReadWriteFd mTunFd;
+ private final IReadWriteFd mL2capFd;
+ private final L2capThread mIngressThread;
+ private final L2capThread mEgressThread;
+ private final ICallback mCallback;
+
+ public interface ICallback {
+ /** Called when an error is encountered; should tear down forwarding. */
+ void onError();
+ }
+
+ private interface IReadWriteFd {
+ /**
+ * Read up to len bytes into bytes[off] and return bytes actually read.
+ *
+ * bytes[] must be of size >= off + len.
+ */
+ int read(byte[] bytes, int off, int len) throws IOException;
+ /**
+ * Write len bytes starting from bytes[off]
+ *
+ * bytes[] must be of size >= off + len.
+ */
+ void write(byte[] bytes, int off, int len) throws IOException;
+ /** Disallow further receptions, shutdown(fd, SHUT_RD) */
+ void shutdownRead();
+ /** Disallow further transmissions, shutdown(fd, SHUT_WR) */
+ void shutdownWrite();
+ /** Close the fd */
+ void close();
+ }
+
+ @VisibleForTesting
+ public static class BluetoothSocketWrapper implements IReadWriteFd {
+ private final BluetoothSocket mSocket;
+ private final InputStream mInputStream;
+ private final OutputStream mOutputStream;
+
+ public BluetoothSocketWrapper(BluetoothSocket socket) {
+ // TODO: assert that MTU can fit within Bluetooth L2CAP SDU (maximum size of an L2CAP
+ // packet). The L2CAP SDU is 65535 by default, but can be less when using hardware
+ // offload.
+ mSocket = socket;
+ try {
+ mInputStream = socket.getInputStream();
+ mOutputStream = socket.getOutputStream();
+ } catch (IOException e) {
+ // Per the API docs, this should not actually be possible.
+ Log.wtf(TAG, "Failed to get Input/OutputStream", e);
+ // Fail hard.
+ throw new IllegalStateException("Failed to get Input/OutputStream");
+ }
+ }
+
+ /** Read from the BluetoothSocket. */
+ @Override
+ public int read(byte[] bytes, int off, int len) throws IOException {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ int bytesRead = mInputStream.read(bytes, off, len);
+ if (bytesRead > MTU) {
+ // Don't try to recover, just trigger network teardown. This might indicate a bug in
+ // the Bluetooth stack.
+ throw new IOException("Packet exceeds MTU");
+ }
+ return bytesRead;
+ }
+
+ /** Write to the BluetoothSocket. */
+ @Override
+ public void write(byte[] bytes, int off, int len) throws IOException {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ mOutputStream.write(bytes, off, len);
+ }
+
+ @Override
+ public void shutdownRead() {
+ // BluetoothSocket does not expose methods to shutdown read / write individually;
+ // however, BluetoothSocket#close() shuts down both read and write and is safe to be
+ // called multiple times from any thread.
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "shutdownRead: Failed to close BluetoothSocket", e);
+ }
+ }
+
+ @Override
+ public void shutdownWrite() {
+ // BluetoothSocket does not expose methods to shutdown read / write individually;
+ // however, BluetoothSocket#close() shuts down both read and write and is safe to be
+ // called multiple times from any thread.
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "shutdownWrite: Failed to close BluetoothSocket", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ // BluetoothSocket#close() is safe to be called multiple times.
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w(TAG, "close: Failed to close BluetoothSocket", e);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public static class FdWrapper implements IReadWriteFd {
+ private final ParcelFileDescriptor mFd;
+
+ public FdWrapper(ParcelFileDescriptor fd) {
+ mFd = fd;
+ }
+
+ @Override
+ public int read(byte[] bytes, int off, int len) throws IOException {
+ try {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ return Os.read(mFd.getFileDescriptor(), bytes, off, len);
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Write to BluetoothSocket.
+ */
+ @Override
+ public void write(byte[] bytes, int off, int len) throws IOException {
+ try {
+ // Note: EINTR is handled internally and automatically triggers a retry loop.
+ Os.write(mFd.getFileDescriptor(), bytes, off, len);
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public void shutdownRead() {
+ try {
+ Os.shutdown(mFd.getFileDescriptor(), OsConstants.SHUT_RD);
+ } catch (ErrnoException e) {
+ Log.w(TAG, "shutdownRead: Failed to shutdown(fd, SHUT_RD)", e);
+ }
+ }
+
+ @Override
+ public void shutdownWrite() {
+ try {
+ Os.shutdown(mFd.getFileDescriptor(), OsConstants.SHUT_WR);
+ } catch (ErrnoException e) {
+ Log.w(TAG, "shutdownWrite: Failed to shutdown(fd, SHUT_WR)", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ // Safe to call multiple times. Both Os.close(FileDescriptor) and
+ // ParcelFileDescriptor#close() offer protection against double-closing an fd.
+ mFd.close();
+ } catch (IOException e) {
+ Log.w(TAG, "close: Failed to close fd", e);
+ }
+ }
+ }
+
+ private class L2capThread extends Thread {
+ // Set mBuffer length to MTU + 1 to catch read() overflows.
+ private final byte[] mBuffer = new byte[MTU + 1];
+ private volatile boolean mIsRunning = true;
+
+ private final String mLogTag;
+ private IReadWriteFd mReadFd;
+ private IReadWriteFd mWriteFd;
+
+ L2capThread(String logTag, IReadWriteFd readFd, IReadWriteFd writeFd) {
+ mLogTag = logTag;
+ mReadFd = readFd;
+ mWriteFd = writeFd;
+ }
+
+ private void postOnError() {
+ mHandler.post(() -> {
+ // All callbacks must be called on handler thread.
+ mCallback.onError();
+ });
+ }
+
+ @Override
+ public void run() {
+ while (mIsRunning) {
+ try {
+ final int readBytes = mReadFd.read(mBuffer, 0 /*off*/, mBuffer.length);
+ // No bytes to write, continue.
+ if (readBytes <= 0) {
+ Log.w(mLogTag, "Zero-byte read encountered: " + readBytes);
+ continue;
+ }
+
+ // If the packet exceeds MTU, drop it.
+ // Note that a large read on BluetoothSocket throws an IOException to tear down
+ // the network.
+ if (readBytes > MTU) continue;
+
+ mWriteFd.write(mBuffer, 0 /*off*/, readBytes);
+ } catch (IOException e) {
+ Log.e(mLogTag, "L2capThread exception", e);
+ // Tear down the network on any error.
+ mIsRunning = false;
+ // Notify provider that forwarding has stopped.
+ postOnError();
+ }
+ }
+ }
+
+ public void tearDown() {
+ mIsRunning = false;
+ mReadFd.shutdownRead();
+ mWriteFd.shutdownWrite();
+ }
+ }
+
+ public L2capPacketForwarder(Handler handler, ParcelFileDescriptor tunFd, BluetoothSocket socket,
+ ICallback cb) {
+ this(handler, new FdWrapper(tunFd), new BluetoothSocketWrapper(socket), cb);
+ }
+
+ @VisibleForTesting
+ L2capPacketForwarder(Handler handler, IReadWriteFd tunFd, IReadWriteFd l2capFd, ICallback cb) {
+ mHandler = handler;
+ mTunFd = tunFd;
+ mL2capFd = l2capFd;
+ mCallback = cb;
+
+ mIngressThread = new L2capThread("L2capThread-Ingress", l2capFd, tunFd);
+ mEgressThread = new L2capThread("L2capThread-Egress", tunFd, l2capFd);
+
+ mIngressThread.start();
+ mEgressThread.start();
+ }
+
+ /**
+ * Tear down the L2capPacketForwarder.
+ *
+ * This operation closes both the passed tun fd and BluetoothSocket.
+ **/
+ public void tearDown() {
+ // Destroying both threads first guarantees that both read and write side of FD have been
+ // shutdown.
+ mIngressThread.tearDown();
+ mEgressThread.tearDown();
+
+ // In order to interrupt a blocking read on the BluetoothSocket, the BluetoothSocket must be
+ // closed (which triggers shutdown()). This means, the BluetoothSocket must be closed inside
+ // L2capPacketForwarder. Tear down the tun fd alongside it for consistency.
+ mTunFd.close();
+ mL2capFd.close();
+
+ try {
+ mIngressThread.join();
+ } catch (InterruptedException e) {
+ // join() interrupted in tearDown path, do nothing.
+ }
+ try {
+ mEgressThread.join();
+ } catch (InterruptedException e) {
+ // join() interrupted in tearDown path, do nothing.
+ }
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
index 4a5dd4f..1d3561a 100644
--- a/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
+++ b/staticlibs/device/com/android/net/module/util/ServiceConnectivityJni.java
@@ -17,8 +17,7 @@
package com.android.net.module.util;
import android.annotation.NonNull;
-
-import java.io.IOException;
+import android.system.ErrnoException;
/**
* Contains JNI functions for use in service-connectivity
@@ -38,17 +37,17 @@
/**
* Create a timerfd.
*
- * @throws IOException if the timerfd creation is failed.
+ * @throws ErrnoException if the timerfd creation is failed.
*/
- public static native int createTimerFd() throws IOException;
+ public static native int createTimerFd() throws ErrnoException;
/**
* Set given time to the timerfd.
*
* @param timeMs target time
- * @throws IOException if setting expiration time is failed.
+ * @throws ErrnoException if setting expiration time is failed.
*/
- public static native void setTimerFdTime(int fd, long timeMs) throws IOException;
+ public static native void setTimerFdTime(int fd, long timeMs) throws ErrnoException;
/** Create tun/tap interface */
public static native int createTunTap(boolean isTun, boolean hasCarrier,
diff --git a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
index 10bc595..cce7efd 100644
--- a/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
+++ b/staticlibs/device/com/android/net/module/util/TimerFdUtils.java
@@ -16,11 +16,9 @@
package com.android.net.module.util;
-import android.os.Process;
+import android.system.ErrnoException;
import android.util.Log;
-import java.io.IOException;
-
/**
* Contains mostly timerfd functionality.
*/
@@ -33,7 +31,7 @@
static int createTimerFileDescriptor() {
try {
return ServiceConnectivityJni.createTimerFd();
- } catch (IOException e) {
+ } catch (ErrnoException e) {
Log.e(TAG, "createTimerFd failed", e);
return -1;
}
@@ -45,7 +43,7 @@
static boolean setExpirationTime(int fd, long expirationTimeMs) {
try {
ServiceConnectivityJni.setTimerFdTime(fd, expirationTimeMs);
- } catch (IOException e) {
+ } catch (ErrnoException e) {
Log.e(TAG, "setExpirationTime failed", e);
return false;
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index 0d96fc4..2420e7a 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -497,4 +497,33 @@
return false;
}
}
+
+ /**
+ * Sends a netlink request to set MTU for given interface
+ *
+ * @param interfaceName The name of the network interface to query.
+ * @param mtu MTU value to set for the interface.
+ * @return true if the request finished successfully, otherwise false.
+ */
+ public static boolean setInterfaceMtu(@NonNull String interfaceName, int mtu) {
+ if (mtu < 68) {
+ Log.e(TAG, "Invalid mtu: " + mtu + ", mtu should be greater than 68 referring RFC791");
+ return false;
+ }
+ final RtNetlinkLinkMessage ntMsg =
+ RtNetlinkLinkMessage.createSetMtuMessage(interfaceName, /*seqNo*/ 0, mtu);
+ if (ntMsg == null) {
+ Log.e(TAG, "Failed to create message to set MTU to " + mtu
+ + "for interface " + interfaceName);
+ return false;
+ }
+ final byte[] msg = ntMsg.pack(ByteOrder.nativeOrder());
+ try {
+ NetlinkUtils.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+ return true;
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to set MTU to " + mtu + " for: " + interfaceName, e);
+ return false;
+ }
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
index f17a7ec..c19a124 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkLinkMessage.java
@@ -364,6 +364,37 @@
DEFAULT_MTU, /*hardwareAddress*/ null, /*interfaceName*/ null);
}
+ /**
+ * Creates an {@link RtNetlinkLinkMessage} instance that can be used to set the MTU of a
+ * network interface.
+ *
+ * @param interfaceName The name of the network interface to query.
+ * @param sequenceNumber The sequence number for the Netlink message.
+ * @param mtu MTU value to set for the interface.
+ * @return An `RtNetlinkLinkMessage` instance representing the request to query the interface.
+ */
+ @Nullable
+ public static RtNetlinkLinkMessage createSetMtuMessage(@NonNull String interfaceName,
+ int sequenceNumber, int mtu) {
+ return createSetMtuMessage(
+ interfaceName, sequenceNumber, mtu, new OsAccess());
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected static RtNetlinkLinkMessage createSetMtuMessage(@NonNull String interfaceName,
+ int sequenceNumber, int mtu, @NonNull OsAccess osAccess) {
+ final int interfaceIndex = osAccess.if_nametoindex(interfaceName);
+ if (interfaceIndex == OsAccess.INVALID_INTERFACE_INDEX) {
+ return null;
+ }
+ return RtNetlinkLinkMessage.build(
+ new StructNlMsgHdr(/*payloadLen*/ 0, RTM_NEWLINK, NLM_F_REQUEST_ACK , sequenceNumber),
+ new StructIfinfoMsg((short) AF_UNSPEC, /*type*/ 0, interfaceIndex,
+ /*flags*/ 0, /*change*/ 0),
+ mtu, /*hardwareAddress*/ null, /*interfaceName*/ null);
+ }
+
@Override
public String toString() {
return "RtNetlinkLinkMessage{ "
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 4878334..026e985 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -153,7 +153,8 @@
(Inet6Address) InetAddresses.parseNumericAddress("ff02::2");
public static final Inet6Address IPV6_ADDR_ALL_HOSTS_MULTICAST =
(Inet6Address) InetAddresses.parseNumericAddress("ff02::3");
-
+ public static final Inet6Address IPV6_ADDR_NODE_LOCAL_ALL_NODES_MULTICAST =
+ (Inet6Address) InetAddresses.parseNumericAddress("ff01::1");
public static final int IPPROTO_FRAGMENT = 44;
/**
diff --git a/staticlibs/native/tcutils/tcutils.cpp b/staticlibs/native/tcutils/tcutils.cpp
index 21e781c..5425d0e 100644
--- a/staticlibs/native/tcutils/tcutils.cpp
+++ b/staticlibs/native/tcutils/tcutils.cpp
@@ -361,7 +361,7 @@
const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
-int sendAndProcessNetlinkResponse(const void *req, int len) {
+int sendAndProcessNetlinkResponse(const void *req, int len, bool enoent_ok) {
// TODO: use unique_fd instead of ScopeGuard
unique_fd fd(socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE));
if (!fd.ok()) {
@@ -445,7 +445,9 @@
return -ENOMSG;
}
- if (resp.e.error) {
+ if (resp.e.error == -ENOENT) {
+ if (!enoent_ok) ALOGE("NLMSG_ERROR message returned ENOENT");
+ } else if (resp.e.error) {
ALOGE("NLMSG_ERROR message return error: %d", resp.e.error);
}
return resp.e.error; // returns 0 on success
@@ -560,7 +562,8 @@
};
#undef CLSACT
- return sendAndProcessNetlinkResponse(&req, sizeof(req));
+ const bool enoent_ok = (nlMsgType == RTM_DELQDISC);
+ return sendAndProcessNetlinkResponse(&req, sizeof(req), enoent_ok);
}
// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned
@@ -666,7 +669,7 @@
snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]",
basename(bpfProgPath));
- int error = sendAndProcessNetlinkResponse(&req, sizeof(req));
+ int error = sendAndProcessNetlinkResponse(&req, sizeof(req), false);
return error;
}
@@ -698,7 +701,8 @@
return error;
}
return sendAndProcessNetlinkResponse(filter.getRequest(),
- filter.getRequestSize());
+ filter.getRequestSize(),
+ false);
}
// tc filter del dev .. in/egress prio .. protocol ..
@@ -726,7 +730,7 @@
},
};
- return sendAndProcessNetlinkResponse(&req, sizeof(req));
+ return sendAndProcessNetlinkResponse(&req, sizeof(req), true);
}
} // namespace android
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
index b29fc73..13710b1 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkLinkMessageTest.java
@@ -328,6 +328,28 @@
}
@Test
+ public void testCreateSetInterfaceMtuMessage() {
+ final String expectedHexBytes =
+ "280000001000050068240000000000000000000008000000" // struct nlmsghdr
+ + "000000000000000008000400DC050000"; // struct ifinfomsg
+ final String interfaceName = "wlan0";
+ final int interfaceIndex = 8;
+ final int sequenceNumber = 0x2468;
+ final int mtu = 1500;
+
+ when(mOsAccess.if_nametoindex(interfaceName)).thenReturn(interfaceIndex);
+
+ final RtNetlinkLinkMessage msg = RtNetlinkLinkMessage.createSetMtuMessage(
+ interfaceName,
+ sequenceNumber,
+ mtu,
+ mOsAccess);
+ assertNotNull(msg);
+ final byte[] bytes = msg.pack(ByteOrder.LITTLE_ENDIAN); // For testing.
+ assertEquals(expectedHexBytes, HexDump.toHexString(bytes));
+ }
+
+ @Test
public void testToString() {
final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWLINK_HEX);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
index ac60b0f..a1cf968 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/TetheringTest.java
@@ -16,6 +16,8 @@
package com.android.cts.net.hostside;
+import static android.net.TetheringManager.TETHERING_WIFI;
+
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.junit.Assert.assertEquals;
@@ -71,6 +73,9 @@
mCtsTetheringUtils.startWifiTethering(mTetheringEventCallback, softApConfig);
assertNotNull(tetheringInterface);
assertEquals(softApConfig, tetheringInterface.getSoftApConfiguration());
+ assertEquals(new TetheringInterface(
+ TETHERING_WIFI, tetheringInterface.getInterface(), softApConfig),
+ tetheringInterface);
TetheringInterface tetheringInterfaceForApp2 =
mTetheringHelperClient.getTetheredWifiInterface();
assertNotNull(tetheringInterfaceForApp2);
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 9379697..ee2e7db 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -52,8 +52,8 @@
import android.os.Handler
import android.os.HandlerThread
import android.os.PowerManager
-import android.os.UserManager
import android.os.SystemProperties
+import android.os.UserManager
import android.platform.test.annotations.AppModeFull
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 00c87a3..aa7d618 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -4087,4 +4087,11 @@
// shims, and @IgnoreUpTo does not check that.
assumeTrue(TestUtils.shouldTestSApis());
}
+
+ @Test
+ public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
+ assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
+ assertThrows(UnsupportedOperationException.class, () -> mCm.tether("iface"));
+ assertThrows(UnsupportedOperationException.class, () -> mCm.untether("iface"));
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index 57bc2be..06f2075 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -605,6 +605,9 @@
}
private fun assumeNoInterfaceForTetheringAvailable() {
+ // Requesting a tethered interface will stop IpClient. Prevent it from doing so
+ // if adb is connected over ethernet.
+ assumeFalse(isAdbOverEthernet())
// Interfaces that have configured NetworkCapabilities will never be used for tethering,
// see aosp/2123900.
try {
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 06bdca6..375d604 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -16,6 +16,7 @@
package com.android.server.net.integrationtests
+import android.Manifest.permission
import android.app.usage.NetworkStatsManager
import android.content.ComponentName
import android.content.Context
@@ -66,6 +67,7 @@
import com.android.testutils.DeviceInfoUtils
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.runAsShell
import com.android.testutils.tryTest
import java.util.function.BiConsumer
import java.util.function.Consumer
@@ -208,7 +210,9 @@
networkStackClient = TestNetworkStackClient(realContext)
networkStackClient.start()
- service = TestConnectivityService(TestDependencies())
+ service = runAsShell(permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) {
+ TestConnectivityService(TestDependencies())
+ }
cm = ConnectivityManager(context, service)
context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm)
context.addMockSystemService(Context.NETWORK_STATS_SERVICE, statsManager)
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index b415382..9a77c89 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -44,7 +44,6 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
@@ -66,7 +65,6 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.net.ConnectivityManager.NetworkCallback;
-import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
@@ -671,12 +669,4 @@
// No callbacks overridden -> do not use the optimization
eq(~0));
}
-
- @Test
- public void testLegacyTetherApisThrowUnsupportedOperationExceptionAfterV() {
- assumeTrue(Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM);
- final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
- assertThrows(UnsupportedOperationException.class, () -> manager.tether("iface"));
- assertThrows(UnsupportedOperationException.class, () -> manager.untether("iface"));
- }
}
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index c1c15ca..73eb24d 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -71,6 +71,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -99,6 +100,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.Bool;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.Struct.U32;
import com.android.net.module.util.Struct.U8;
@@ -106,6 +108,7 @@
import com.android.net.module.util.bpf.CookieTagMapValue;
import com.android.net.module.util.bpf.IngressDiscardKey;
import com.android.net.module.util.bpf.IngressDiscardValue;
+import com.android.net.module.util.bpf.LocalNetAccessKey;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -171,6 +174,10 @@
private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap =
new TestBpfMap<>(S32.class, UidOwnerValue.class);
private final IBpfMap<S32, U8> mUidPermissionMap = new TestBpfMap<>(S32.class, U8.class);
+ private final IBpfMap<U32, Bool> mLocalNetBlockedUidMap =
+ new TestBpfMap<>(U32.class, Bool.class);
+ private final IBpfMap<LocalNetAccessKey, Bool> mLocalNetAccessMap =
+ new TestBpfMap<>(LocalNetAccessKey.class, Bool.class);
private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap =
spy(new TestBpfMap<>(CookieTagMapKey.class, CookieTagMapValue.class));
private final IBpfMap<S32, U8> mDataSaverEnabledMap = new TestBpfMap<>(S32.class, U8.class);
@@ -189,6 +196,8 @@
CURRENT_STATS_MAP_CONFIGURATION_KEY, new U32(STATS_SELECT_MAP_A));
BpfNetMaps.setUidOwnerMapForTest(mUidOwnerMap);
BpfNetMaps.setUidPermissionMapForTest(mUidPermissionMap);
+ BpfNetMaps.setLocalNetAccessMapForTest(mLocalNetAccessMap);
+ BpfNetMaps.setLocalNetBlockedUidMapForTest(mLocalNetBlockedUidMap);
BpfNetMaps.setCookieTagMapForTest(mCookieTagMap);
BpfNetMaps.setDataSaverEnabledMapForTest(mDataSaverEnabledMap);
mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
@@ -235,6 +244,138 @@
}
@Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.addLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0, true));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessAfterV() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+ assertNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("100.68.0.0"), 0, 0)));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddLocalNetAccessAfterVWithIncorrectInterface() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addLocalNetAccess(160, "wlan2",
+ Inet4Address.getByName("196.68.0.0"), 0, 0, true);
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testGetLocalNetAccessBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.getLocalNetAccess(0, TEST_IF_NAME, Inet6Address.ANY, 0, 0));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testGetLocalNetAccessAfterV() throws Exception {
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mLocalNetAccessMap.updateEntry(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0),
+ new Bool(false));
+
+ assertNotNull(mLocalNetAccessMap.getValue(new LocalNetAccessKey(160, TEST_IF_INDEX,
+ Inet4Address.getByName("196.68.0.0"), 0, 0)));
+
+ assertFalse(mBpfNetMaps.getLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("196.68.0.0"), 0, 0));
+ assertTrue(mBpfNetMaps.getLocalNetAccess(160, TEST_IF_NAME,
+ Inet4Address.getByName("100.68.0.0"), 0, 0));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddUidToLocalNetBlockMapBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.addUidToLocalNetBlockMap(0));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testIsUidPresentInLocalNetBlockMapBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.getUidValueFromLocalNetBlockMap(0));
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveUidFromLocalNetBlockMapBeforeV() {
+ assertThrows(UnsupportedOperationException.class, () ->
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(0));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testAddUidFromLocalNetBlockMapAfterV() throws Exception {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mBpfNetMaps.addUidToLocalNetBlockMap(uid0);
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid0)).val);
+ assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid1)));
+
+ mBpfNetMaps.addUidToLocalNetBlockMap(uid1);
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testIsUidPresentInLocalNetBlockMapAfterV() throws Exception {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid0), new Bool(true));
+ assertTrue(mBpfNetMaps.getUidValueFromLocalNetBlockMap(uid0));
+ assertFalse(mBpfNetMaps.getUidValueFromLocalNetBlockMap(uid1));
+
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid1), new Bool(true));
+ assertTrue(mBpfNetMaps.getUidValueFromLocalNetBlockMap(uid1));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public void testRemoveUidFromLocalNetBlockMapAfterV() throws Exception {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+
+ assertTrue(mLocalNetAccessMap.isEmpty());
+
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid0), new Bool(true));
+ mLocalNetBlockedUidMap.updateEntry(new U32(uid1), new Bool(true));
+
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid0)).val);
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(uid0);
+ assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid0)));
+ assertTrue(mLocalNetBlockedUidMap.getValue(new U32(uid1)).val);
+
+ mBpfNetMaps.removeUidFromLocalNetBlockMap(uid1);
+ assertNull(mLocalNetBlockedUidMap.getValue(new U32(uid1)));
+ }
+
+ @Test
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testIsChainEnabled() throws Exception {
doTestIsChainEnabled(FIREWALL_CHAIN_DOZABLE);
diff --git a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
index 5a7515e..49eb476 100644
--- a/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
+++ b/tests/unit/java/com/android/server/L2capNetworkProviderTest.kt
@@ -16,6 +16,9 @@
package com.android.server
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothManager
+import android.bluetooth.BluetoothServerSocket
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE
@@ -35,6 +38,7 @@
import android.os.HandlerThread
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.waitForIdle
import kotlin.test.assertTrue
import org.junit.After
import org.junit.Before
@@ -52,13 +56,14 @@
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-const val TAG = "L2capNetworkProviderTest"
+private const val TAG = "L2capNetworkProviderTest"
+private const val TIMEOUT_MS = 1000
-val RESERVATION_CAPS = NetworkCapabilities.Builder.withoutDefaultCapabilities()
+private val RESERVATION_CAPS = NetworkCapabilities.Builder.withoutDefaultCapabilities()
.addTransportType(TRANSPORT_BLUETOOTH)
.build()
-val RESERVATION = NetworkRequest(
+private val RESERVATION = NetworkRequest(
NetworkCapabilities(RESERVATION_CAPS),
TYPE_NONE,
42 /* rId */,
@@ -73,6 +78,9 @@
@Mock private lateinit var provider: NetworkProvider
@Mock private lateinit var cm: ConnectivityManager
@Mock private lateinit var pm: PackageManager
+ @Mock private lateinit var bm: BluetoothManager
+ @Mock private lateinit var adapter: BluetoothAdapter
+ @Mock private lateinit var serverSocket: BluetoothServerSocket
private val handlerThread = HandlerThread("$TAG handler thread").apply { start() }
private val handler = Handler(handlerThread.looper)
@@ -85,6 +93,11 @@
doReturn(cm).`when`(context).getSystemService(eq(ConnectivityManager::class.java))
doReturn(pm).`when`(context).getPackageManager()
doReturn(true).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE)
+
+ doReturn(bm).`when`(context).getSystemService(eq(BluetoothManager::class.java))
+ doReturn(adapter).`when`(bm).getAdapter()
+ doReturn(serverSocket).`when`(adapter).listenUsingInsecureL2capChannel()
+ doReturn(0x80).`when`(serverSocket).getPsm()
}
@After
@@ -96,6 +109,7 @@
@Test
fun testNetworkProvider_registeredWhenSupported() {
L2capNetworkProvider(deps, context).start()
+ handlerThread.waitForIdle(TIMEOUT_MS)
verify(cm).registerNetworkProvider(eq(provider))
verify(provider).registerNetworkOffer(any(), any(), any(), any())
}
@@ -104,12 +118,14 @@
fun testNetworkProvider_notRegisteredWhenNotSupported() {
doReturn(false).`when`(pm).hasSystemFeature(FEATURE_BLUETOOTH_LE)
L2capNetworkProvider(deps, context).start()
+ handlerThread.waitForIdle(TIMEOUT_MS)
verify(cm, never()).registerNetworkProvider(eq(provider))
}
fun doTestBlanketOfferIgnoresRequest(request: NetworkRequest) {
clearInvocations(provider)
L2capNetworkProvider(deps, context).start()
+ handlerThread.waitForIdle(TIMEOUT_MS)
val blanketOfferCaptor = ArgumentCaptor.forClass(NetworkOfferCallback::class.java)
verify(provider).registerNetworkOffer(any(), any(), any(), blanketOfferCaptor.capture())
@@ -124,6 +140,7 @@
) {
clearInvocations(provider)
L2capNetworkProvider(deps, context).start()
+ handlerThread.waitForIdle(TIMEOUT_MS)
val blanketOfferCaptor = ArgumentCaptor.forClass(NetworkOfferCallback::class.java)
verify(provider).registerNetworkOffer(any(), any(), any(), blanketOfferCaptor.capture())
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index 5bde31a..55c68b7 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -22,6 +22,7 @@
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.INTERNET;
import static android.Manifest.permission.NETWORK_STACK;
+import static android.Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS;
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_OEM;
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRODUCT;
@@ -41,6 +42,7 @@
import static android.os.Process.SYSTEM_UID;
import static com.android.server.connectivity.PermissionMonitor.isHigherNetworkPermission;
+import static com.android.testutils.TestPermissionUtil.runAsShell;
import static junit.framework.Assert.fail;
@@ -1235,8 +1237,10 @@
// Use the real context as this test must ensure the *real* system package holds the
// necessary permission.
final Context realContext = InstrumentationRegistry.getContext();
- final PermissionMonitor monitor = new PermissionMonitor(
- realContext, mNetdService, mBpfNetMaps, mHandlerThread);
+ final PermissionMonitor monitor = runAsShell(
+ OBSERVE_GRANT_REVOKE_PERMISSIONS,
+ () -> new PermissionMonitor(realContext, mNetdService, mBpfNetMaps, mHandlerThread)
+ );
final PackageManager manager = realContext.getPackageManager();
final PackageInfo systemInfo = manager.getPackageInfo(REAL_SYSTEM_PACKAGE_NAME,
GET_PERMISSIONS | MATCH_ANY_USER);
diff --git a/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
new file mode 100644
index 0000000..b3095ee
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/L2capPacketForwarderTest.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2025 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.bluetooth.BluetoothSocket
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.ParcelFileDescriptor
+import android.system.Os
+import android.system.OsConstants.AF_UNIX
+import android.system.OsConstants.SHUT_RD
+import android.system.OsConstants.SHUT_WR
+import android.system.OsConstants.SOCK_SEQPACKET
+import android.system.OsConstants.SOL_SOCKET
+import android.system.OsConstants.SO_RCVTIMEO
+import android.system.OsConstants.SO_SNDTIMEO
+import android.system.StructTimeval
+import com.android.server.net.L2capPacketForwarder.BluetoothSocketWrapper
+import com.android.server.net.L2capPacketForwarder.FdWrapper
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.nio.ByteBuffer
+import kotlin.arrayOf
+import kotlin.random.Random
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+private const val TIMEOUT = 1000L
+
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class L2capPacketForwarderTest {
+ private lateinit var forwarder: L2capPacketForwarder
+ private val tunFds = arrayOf(FileDescriptor(), FileDescriptor())
+ private val l2capFds = arrayOf(FileDescriptor(), FileDescriptor())
+ private lateinit var l2capInputStream: BluetoothL2capInputStream
+ private lateinit var l2capOutputStream: BluetoothL2capOutputStream
+ @Mock private lateinit var bluetoothSocket: BluetoothSocket
+ @Mock private lateinit var callback: L2capPacketForwarder.ICallback
+
+ private val handlerThread = HandlerThread("L2capPacketForwarderTest thread").apply { start() }
+ private val handler = Handler(handlerThread.looper)
+
+ /** Imitates the behavior of an L2CAP BluetoothSocket */
+ private class BluetoothL2capInputStream(
+ val fd: FileDescriptor,
+ ) : InputStream() {
+ val l2capBuffer = ByteBuffer.wrap(ByteArray(0xffff)).apply {
+ limit(0)
+ }
+
+ override fun read(): Int {
+ throw NotImplementedError("b/391623333: not implemented correctly for L2cap sockets")
+ }
+
+ /** See BluetoothSocket#read(buf, off, len) */
+ override fun read(b: ByteArray, off: Int, len: Int): Int {
+ // If no more bytes are remaining, read from the fd into the intermediate buffer.
+ if (l2capBuffer.remaining() == 0) {
+ // fillL2capRxBuffer()
+ // refill buffer and return - 1
+ val backingArray = l2capBuffer.array()
+ var bytesRead = 0
+ try {
+ bytesRead = Os.read(fd, backingArray, 0 /*off*/, backingArray.size)
+ } catch (e: Exception) {
+ // read failed, timed out, or was interrupted
+ // InputStream throws IOException
+ throw IOException(e)
+ }
+ l2capBuffer.rewind()
+ l2capBuffer.limit(bytesRead)
+ }
+
+ val bytesToRead = if (len > l2capBuffer.remaining()) l2capBuffer.remaining() else len
+ l2capBuffer.get(b, off, bytesToRead)
+ return bytesToRead
+ }
+
+ override fun available(): Int {
+ throw NotImplementedError("b/391623333: not implemented correctly for L2cap sockets")
+ }
+
+ override fun close() {
+ try {
+ Os.shutdown(fd, SHUT_RD)
+ } catch (e: Exception) {
+ // InputStream throws IOException
+ throw IOException(e)
+ }
+ }
+ }
+
+ /** Imitates the behavior of an L2CAP BluetoothSocket */
+ private class BluetoothL2capOutputStream(
+ val fd: FileDescriptor,
+ ) : OutputStream() {
+
+ override fun write(b: Int) {
+ throw NotImplementedError("This method does not maintain packet boundaries, do not use")
+ }
+
+ /** See BluetoothSocket#write(buf, off, len) */
+ override fun write(b: ByteArray, off: Int, len: Int) {
+ try {
+ Os.write(fd, b, off, len)
+ } catch (e: Exception) {
+ // OutputStream throws IOException
+ throw IOException(e)
+ }
+ }
+
+ override fun close() {
+ try {
+ Os.shutdown(fd, SHUT_WR)
+ } catch (e: Exception) {
+ // OutputStream throws IOException
+ throw IOException(e)
+ }
+ }
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, tunFds[0], tunFds[1])
+ Os.socketpair(AF_UNIX, SOCK_SEQPACKET, 0, l2capFds[0], l2capFds[1])
+
+ // Set socket i/o timeout for test end.
+ Os.setsockoptTimeval(tunFds[1], SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(5000))
+ Os.setsockoptTimeval(tunFds[1], SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(5000))
+ Os.setsockoptTimeval(l2capFds[1], SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(5000))
+ Os.setsockoptTimeval(l2capFds[1], SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(5000))
+
+ l2capInputStream = BluetoothL2capInputStream(l2capFds[0])
+ l2capOutputStream = BluetoothL2capOutputStream(l2capFds[0])
+ doReturn(l2capInputStream).`when`(bluetoothSocket).getInputStream()
+ doReturn(l2capOutputStream).`when`(bluetoothSocket).getOutputStream()
+ doAnswer({
+ l2capInputStream.close()
+ l2capOutputStream.close()
+ try {
+ // libcore's Linux_close properly invalidates the FileDescriptor, so it is safe to
+ // close multiple times.
+ Os.close(l2capFds[0])
+ } catch (e: Exception) {
+ // BluetoothSocket#close can be called multiple times. Catch EBADF on subsequent
+ // invocations.
+ }
+ }).`when`(bluetoothSocket).close()
+
+ forwarder = L2capPacketForwarder(
+ handler,
+ FdWrapper(ParcelFileDescriptor(tunFds[0])),
+ BluetoothSocketWrapper(bluetoothSocket),
+ callback
+ )
+ }
+
+ @After
+ fun tearDown() {
+ if (::forwarder.isInitialized) {
+ // forwarder closes tunFds[0] and l2capFds[0]
+ forwarder.tearDown()
+ } else {
+ Os.close(tunFds[0])
+ Os.close(l2capFds[0])
+ }
+ Os.close(tunFds[1])
+ Os.close(l2capFds[1])
+
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+
+ fun sendPacket(fd: FileDescriptor, size: Int = 1280): ByteArray {
+ val packet = ByteArray(size)
+ Random.nextBytes(packet)
+ Os.write(fd, packet, 0 /*off*/, packet.size)
+ return packet
+ }
+
+ fun assertPacketReceived(fd: FileDescriptor, expected: ByteArray) {
+ val readBuffer = ByteArray(expected.size)
+ Os.read(fd, readBuffer, 0 /*off*/, readBuffer.size)
+ assertThat(readBuffer).isEqualTo(expected)
+ }
+
+ @Test
+ fun testForwarding_withoutHeaderCompression() {
+ var packet = sendPacket(l2capFds[1])
+ var packet2 = sendPacket(l2capFds[1])
+ assertPacketReceived(tunFds[1], packet)
+ assertPacketReceived(tunFds[1], packet2)
+
+ packet = sendPacket(tunFds[1])
+ packet2 = sendPacket(tunFds[1])
+ assertPacketReceived(l2capFds[1], packet)
+ assertPacketReceived(l2capFds[1], packet2)
+ }
+
+ @Test
+ fun testForwarding_packetExceedsMtu() {
+ // Reading from tun drops packets that exceed MTU.
+ // drop
+ sendPacket(tunFds[1], L2capPacketForwarder.MTU + 1)
+ // drop
+ sendPacket(tunFds[1], L2capPacketForwarder.MTU + 42)
+ var packet = sendPacket(l2capFds[1], 1280)
+ assertPacketReceived(tunFds[1], packet)
+
+ // On the BluetoothSocket side, reads that exceed MTU stop forwarding.
+ sendPacket(l2capFds[1], L2capPacketForwarder.MTU + 1)
+ verify(callback, timeout(TIMEOUT)).onError()
+ }
+}