CaptivePortal: implement setDelegateUid API.

This API is designed to set the UID of the app that is allowed to
perform network traffic for captive protal login, communication will
bypass network access restrictions such as VPNs and private DNS settings
when a portal page is launched by Android Custom Tabs and bind the
upcoming traffic to that portal network.

Bug: 373773542
Test: atest ConnectivityCoverageTests
Change-Id: I9992671634b8e88f5a647455811a6157fb15a44b
This commit is contained in:
Patrick Rohr 2024-12-05 04:06:53 +00:00 • committed by Xiao Ma
commit 7d6dfd52b2

View file

@ -121,6 +121,9 @@ import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_SELF_CER
import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
import static android.os.Process.INVALID_UID;
import static android.os.Process.VPN_UID;
import static android.system.OsConstants.ENOENT;
import static android.system.OsConstants.ENOTCONN;
import static android.system.OsConstants.EOPNOTSUPP;
import static android.system.OsConstants.ETH_P_ALL;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
@ -195,6 +198,7 @@ import android.net.ICaptivePortal;
import android.net.IConnectivityDiagnosticsCallback;
import android.net.IConnectivityManager;
import android.net.IDnsResolver;
import android.net.IIntResultListener;
import android.net.INetd;
import android.net.INetworkActivityListener;
import android.net.INetworkAgent;
@ -6340,8 +6344,20 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
}
private class CaptivePortalImpl extends ICaptivePortal.Stub {
public class CaptivePortalImpl extends ICaptivePortal.Stub implements IBinder.DeathRecipient {
private final Network mNetwork;
// Binder object to track the lifetime of the setDelegateUid caller for cleanup purposes.
//
// Note that in theory it can happen that there are multiple callers for a given
// object. For example, the app that receives the CaptivePortal object from the Intent
// fired by startCaptivePortalAppInternal could send the object to another process, or
// clone it. Only the first of these objects that calls setDelegateUid will properly
// register a death recipient. Calls from the other objects will work, but only the
// first object's death will cause the death recipient to fire.
// TODO: track all callers by callerBinder instead of CaptivePortalImpl, store callerBinder
// in a Set. When the death recipient fires, we can remove the callingBinder from the set,
// and when the set is empty, we can clear the delegated UID.
private IBinder mDelegateUidCaller;
private CaptivePortalImpl(Network network) {
mNetwork = network;
@ -6381,6 +6397,55 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
}
private int handleSetDelegateUid(int uid, @NonNull final IBinder callerBinder) {
if (mDelegateUidCaller == null) {
mDelegateUidCaller = callerBinder;
try {
// While technically unnecessary, it is safe to register a DeathRecipient for
// a cleanup operation (where uid = INVALID_UID).
mDelegateUidCaller.linkToDeath(this, 0);
} catch (RemoteException e) {
// remote has died, return early.
return ENOTCONN;
}
}
final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(mNetwork);
if (nai == null) return ENOENT; // network does not exist anymore.
if (nai.isDestroyed()) return ENOENT; // network has already been destroyed.
// TODO: consider allowing the uid to bypass VPN on all networks before V.
if (!mDeps.isAtLeastV()) return EOPNOTSUPP;
// Check whether there has already been a delegate UID configured, if so, perform
// cleanup and disallow bypassing VPN for that UID if no other caller is delegating
// this UID.
// TODO: consider using exceptions instead of errnos.
final int errno = nai.removeCaptivePortalDelegateUid(this);
if (errno != 0) return errno;
// If uid == INVALID_UID, we are done.
if (uid == INVALID_UID) return 0;
return nai.setCaptivePortalDelegateUid(this, uid);
}
@Override
public void setDelegateUid(int uid, @NonNull final IBinder callerBinder,
@NonNull final IIntResultListener listener) {
Objects.requireNonNull(callerBinder);
Objects.requireNonNull(listener);
enforceAnyPermissionOf(mContext, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
mHandler.post(() -> {
final int errno = handleSetDelegateUid(uid, callerBinder);
try {
listener.onResult(errno);
} catch (RemoteException e) {
// remote has died, nothing to do.
}
});
}
@Nullable
private NetworkMonitorManager getNetworkMonitorManager(final Network network) {
// getNetworkAgentInfoForNetwork is thread-safe
@ -6390,6 +6455,13 @@ public class ConnectivityService extends IConnectivityManager.Stub
// nai.networkMonitor() is thread-safe
return nai.networkMonitor();
}
@Override
public void binderDied() {
// Cleanup invalid UID and restore the VPN bypass rule. Because mDelegateUidCaller is
// never reset, it cannot be null in this context.
mHandler.post(() -> handleSetDelegateUid(INVALID_UID, mDelegateUidCaller));
}
}
public boolean avoidBadWifi() {