Merge "CaptivePortal: implement setDelegateUid API." into main

This commit is contained in:
Xiao Ma 2024-12-11 22:43:58 -08:00 • committed by Gerrit Code Review
commit 69aa3c5469
6 changed files with 212 additions and 1 deletions

View file

@ -75,6 +75,7 @@ java_defaults {
// the module builds against API (the parcelable declarations exist in framework.aidl)
"frameworks/base/core/java", // For framework parcelables
"frameworks/native/aidl/binder", // For PersistableBundle.aidl
"packages/modules/Connectivity/Tethering/common/TetheringLib/src",
],
},
stub_only_libs: [
@ -335,6 +336,7 @@ droidstubs {
aidl: {
include_dirs: [
"packages/modules/Connectivity/framework/aidl-export",
"packages/modules/Connectivity/Tethering/common/TetheringLib/src",
"frameworks/native/aidl/binder", // For PersistableBundle.aidl
],
},

View file

@ -18,10 +18,19 @@ package android.net;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.TargetApi;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.OutcomeReceiver;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.system.OsConstants;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* A class allowing apps handling the {@link ConnectivityManager#ACTION_CAPTIVE_PORTAL_SIGN_IN}
@ -69,6 +78,15 @@ public class CaptivePortal implements Parcelable {
@SystemApi
public static final int APP_REQUEST_REEVALUATION_REQUIRED = APP_REQUEST_BASE + 0;
/**
* Binder object used for tracking the lifetime of the process, so CS can perform cleanup if
* the CaptivePortal app dies. This binder is not parcelled as part of this object. It is
* created in the client process and sent to the server by setDelegateUid so that the server
* can use it to register a death recipient.
*
*/
private final Binder mLifetimeBinder = new Binder();
private final IBinder mBinder;
/** @hide */
@ -167,4 +185,56 @@ public class CaptivePortal implements Parcelable {
@SystemApi
public void logEvent(int eventId, @NonNull String packageName) {
}
/**
* Sets the UID of the app that is allowed to perform network traffic for captive
* portal login.
*
* This app will be allowed to communicate directly on the captive
* portal by binding to the {@link android.net.Network} extra passed in the
* ACTION_CAPTIVE_PORTAL_SIGN_IN broadcast that contained this object.
*
* Communication will bypass network access restrictions such as VPNs and
* Private DNS settings, so the delegated UID must be trusted to ensure that only
* traffic intended for captive portal login binds to that network.
*
* By default, no UID is delegated. The delegation can be cleared by calling
* this method again with {@link android.os.Process.INVALID_UID}. Only one UID can
* be delegated at any given time.
*
* The operation is asynchronous. The uid is only guaranteed to have access when
* the provided OutcomeReceiver is called.
*
* @hide
*/
@RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
// OutcomeReceiver is not available on R, but the mainline version of this
// class is only available on S+.
@TargetApi(Build.VERSION_CODES.S)
public void setDelegateUid(int uid, @NonNull Executor executor,
@NonNull final OutcomeReceiver<Void, ServiceSpecificException> receiver) {
Objects.requireNonNull(executor);
Objects.requireNonNull(receiver);
try {
ICaptivePortal.Stub.asInterface(mBinder).setDelegateUid(
uid,
mLifetimeBinder,
new IIntResultListener.Stub() {
@Override
public void onResult(int resultCode) {
if (resultCode != 0) {
final String msg = "Fail to set the delegate UID " + uid
+ ", error: " + OsConstants.errnoName(resultCode);
executor.execute(() -> {
receiver.onError(new ServiceSpecificException(resultCode, msg));
});
} else {
executor.execute(() -> receiver.onResult(null));
}
}
});
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}

View file

@ -16,6 +16,9 @@
package android.net;
import android.net.IIntResultListener;
import android.os.IBinder;
/**
* Interface to inform NetworkMonitor of decisions of app handling captive portal.
* @hide
@ -23,4 +26,5 @@ package android.net;
oneway interface ICaptivePortal {
void appRequest(int request);
void appResponse(int response);
void setDelegateUid(int uid, IBinder binder, IIntResultListener listener);
}

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() {

View file

@ -25,6 +25,9 @@ import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.transportNamesOf;
import static android.system.OsConstants.EIO;
import static android.system.OsConstants.EEXIST;
import static android.system.OsConstants.ENOENT;
import android.annotation.NonNull;
import android.annotation.Nullable;
@ -57,9 +60,11 @@ import android.net.TcpKeepalivePacketData;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.telephony.data.EpsBearerQosSessionAttributes;
import android.telephony.data.NrQosSessionAttributes;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@ -70,6 +75,7 @@ import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.WakeupMessage;
import com.android.net.module.util.HandlerUtils;
import com.android.server.ConnectivityService;
import com.android.server.ConnectivityService.CaptivePortalImpl;
import java.io.PrintWriter;
import java.net.Inet4Address;
@ -574,6 +580,10 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable {
// For fast lookups. Indexes into mInactivityTimers by request ID.
private final SparseArray<InactivityTimer> mInactivityTimerForRequest = new SparseArray<>();
// Map of delegated UIDs used to bypass VPN and its captive portal app caller.
private final ArrayMap<CaptivePortalImpl, Integer> mCaptivePortalDelegateUids =
new ArrayMap<>();
// Inactivity expiry timer. Armed whenever mInactivityTimers is non-empty, regardless of
// whether the network is inactive or not. Always set to the expiry of the mInactivityTimers
// that expires last. When the timer fires, all inactivity state is cleared, and if the network
@ -626,6 +636,7 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable {
private final Context mContext;
private final Handler mHandler;
private final QosCallbackTracker mQosCallbackTracker;
private final INetd mNetd;
private final long mCreationTime;
@ -655,6 +666,7 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable {
mConnServiceDeps = deps;
setScore(score); // uses members connService, networkCapabilities and networkAgentConfig
clatd = new Nat464Xlat(this, netd, dnsResolver, deps);
mNetd = netd;
mContext = context;
mHandler = handler;
this.factorySerialNumber = factorySerialNumber;
@ -1549,6 +1561,52 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable {
}
}
private int allowBypassVpnOnNetwork(boolean allow, int uid, int netId) {
try {
mNetd.networkAllowBypassVpnOnNetwork(allow, uid, netId);
return 0;
} catch (RemoteException e) {
// Netd has crashed, and this process is about to crash as well.
return EIO;
} catch (ServiceSpecificException e) {
return e.errorCode;
}
}
/**
* Set the delegate UID of the app that is allowed to perform network traffic for captive
* portal login, and configure the netd bypass rule with this delegated UID.
*
* @param caller the captive portal app to that delegated UID
* @param uid the delegated UID of the captive portal app.
* @return Return 0 if set the UID and VPN bypass rule successfully or bypass rule corresponding
* to this UID already exists otherwise return errno.
*/
public int setCaptivePortalDelegateUid(@NonNull final CaptivePortalImpl caller, int uid) {
final int errorCode = allowBypassVpnOnNetwork(true /* allow */, uid, network.netId);
if (errorCode == 0 || errorCode == EEXIST) {
mCaptivePortalDelegateUids.put(caller, uid);
}
return errorCode == EEXIST ? 0 : errorCode;
}
/**
* Remove the delegate UID of the app that is allowed to perform network traffic for captive
* portal login, and remove the netd bypass rule if no other caller is delegating this UID.
*
* @param caller the captive portal app to that delegated UID.
* @return Return 0 if remove the UID and VPN bypass rule successfully or bypass rule
* corresponding to this UID doesn't exist otherwise return errno.
*/
public int removeCaptivePortalDelegateUid(@NonNull final CaptivePortalImpl caller) {
final Integer maybeDelegateUid = mCaptivePortalDelegateUids.remove(caller);
if (maybeDelegateUid == null) return 0;
if (mCaptivePortalDelegateUids.values().contains(maybeDelegateUid)) return 0;
final int errorCode =
allowBypassVpnOnNetwork(false /* allow */, maybeDelegateUid, network.netId);
return errorCode == ENOENT ? 0 : errorCode;
}
private static boolean areAllowedUidsAcceptableFromNetworkAgent(
@NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
@NonNull final ConnectivityService.Dependencies deps,

View file

@ -19,6 +19,7 @@ package android.net;
import static org.junit.Assert.assertEquals;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import androidx.test.filters.SmallTest;
@ -55,6 +56,10 @@ public class CaptivePortalTest {
mCode = request;
}
@Override
public void setDelegateUid(int uid, IBinder binder, IIntResultListener listener) {
}
// This is only @Override on R-
public void logEvent(int eventId, String packageName) throws RemoteException {
mCode = eventId;