diff --git a/framework/Android.bp b/framework/Android.bp index a93a5329a6..73516483b6 100644 --- a/framework/Android.bp +++ b/framework/Android.bp @@ -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: [ @@ -334,6 +335,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 ], }, diff --git a/framework/src/android/net/CaptivePortal.java b/framework/src/android/net/CaptivePortal.java index 4a7b601642..4c534f3c4b 100644 --- a/framework/src/android/net/CaptivePortal.java +++ b/framework/src/android/net/CaptivePortal.java @@ -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 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(); + } + } } diff --git a/framework/src/android/net/ICaptivePortal.aidl b/framework/src/android/net/ICaptivePortal.aidl index e35f8d46af..5cbb4282bc 100644 --- a/framework/src/android/net/ICaptivePortal.aidl +++ b/framework/src/android/net/ICaptivePortal.aidl @@ -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); } diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index f3b97bc5c1..4c0d7b3bbc 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -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() { diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java index 94b655f5e3..2b00386420 100644 --- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java +++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java @@ -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 mInactivityTimerForRequest = new SparseArray<>(); + // Map of delegated UIDs used to bypass VPN and its captive portal app caller. + private final ArrayMap 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, diff --git a/tests/common/java/android/net/CaptivePortalTest.java b/tests/common/java/android/net/CaptivePortalTest.java index 15d3398d43..6655827c61 100644 --- a/tests/common/java/android/net/CaptivePortalTest.java +++ b/tests/common/java/android/net/CaptivePortalTest.java @@ -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;