Merge changes I0eb70c12,I6cd727bd into main
* changes: Remove unnecessary ifindex arg from destroyUdpSocket Add hidden API to register/unregister QUIC connection close payload
This commit is contained in:
commit
ebdcd9e199
11 changed files with 1112 additions and 9 deletions
|
|
@ -6856,4 +6856,68 @@ public class ConnectivityManager {
|
||||||
throw e.rethrowFromSystemServer();
|
throw e.rethrowFromSystemServer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register QUIC UDP socket and UDP payload that can close QUIC connection.
|
||||||
|
*
|
||||||
|
* When the app loses network access (e.g. due to freezer or firewall chain),
|
||||||
|
* ConnectivityService 1)destroys the registered UDP socket by sending a SOCK_DESTROY netlink
|
||||||
|
* message and 2)sends the registered UDP payload to the server.
|
||||||
|
* This prevents unnecessary modem wakeups caused by packets from the server after the apps
|
||||||
|
* lose network access.
|
||||||
|
*
|
||||||
|
* Additionally, to close QUIC connection for apps that crash or are killed, ConnectivityService
|
||||||
|
* sends the registered UDP payload when it receives a socket destroy diag message for the
|
||||||
|
* registered socket.
|
||||||
|
*
|
||||||
|
* If apps call this method, they must call {@link #unregisterQuicConnectionClosePayload} before
|
||||||
|
* they close the QUIC connection and the socket.
|
||||||
|
* Otherwise, the registered UDP payload will be sent to the server, even if the QUIC connection
|
||||||
|
* was already properly closed. This is because ConnectivityService cannot distinguish whether
|
||||||
|
* the socket was closed due to the app being killed or because the app forgets to call
|
||||||
|
* {@link #unregisterQuicConnectionClosePayload}.
|
||||||
|
*
|
||||||
|
* The registered payload will be sent using the source and destination addresses and ports of
|
||||||
|
* the connected UDP socket provided. So, the app must not reuse the same socket to reconnect
|
||||||
|
* to a different server, even if first calls unregisterQuicConnectionClosePayload. This is
|
||||||
|
* because this method operates asynchronously.
|
||||||
|
*
|
||||||
|
* If the number of registered socket and payload hits the limit, ConnectivityService silently
|
||||||
|
* ignores new registration. ConnectivityService cannot know about this synchronously, but the
|
||||||
|
* caller couldn't really do anything with this information anyway.
|
||||||
|
* TODO: Consider adding a result listener if this API becomes public.
|
||||||
|
*
|
||||||
|
* @param pfd The {@link ParcelFileDescriptor} for the connected UDP socket.
|
||||||
|
* The registered payload will be sent using the source and destination addresses and
|
||||||
|
* ports of this connected UDP socket.
|
||||||
|
* @param payload The UDP payload that can close QUIC connection.
|
||||||
|
*
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public void registerQuicConnectionClosePayload(final ParcelFileDescriptor pfd,
|
||||||
|
final byte[] payload) {
|
||||||
|
try {
|
||||||
|
mService.registerQuicConnectionClosePayload(pfd, payload);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw e.rethrowFromSystemServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister the QUIC socket previously registered by
|
||||||
|
* {@link #registerQuicConnectionClosePayload}
|
||||||
|
*
|
||||||
|
* This method is a no-op if the provided UDP socket is not registered.
|
||||||
|
*
|
||||||
|
* @param pfd The {@link ParcelFileDescriptor} for the UDP socket.
|
||||||
|
*
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
public void unregisterQuicConnectionClosePayload(final ParcelFileDescriptor pfd) {
|
||||||
|
try {
|
||||||
|
mService.unregisterQuicConnectionClosePayload(pfd);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw e.rethrowFromSystemServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -266,4 +266,7 @@ interface IConnectivityManager
|
||||||
long getEnabledConnectivityManagerFeatures();
|
long getEnabledConnectivityManagerFeatures();
|
||||||
|
|
||||||
boolean isConnectivityServiceFeatureEnabledForTesting(String featureFlag);
|
boolean isConnectivityServiceFeatureEnabledForTesting(String featureFlag);
|
||||||
|
|
||||||
|
oneway void registerQuicConnectionClosePayload(in ParcelFileDescriptor pfd, in byte[] payload);
|
||||||
|
oneway void unregisterQuicConnectionClosePayload(in ParcelFileDescriptor pfd);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,7 @@ import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPer
|
||||||
import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
|
import static com.android.net.module.util.PermissionUtils.hasAnyPermissionOf;
|
||||||
import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
|
import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
|
||||||
import static com.android.server.connectivity.ConnectivityFlags.CELLULAR_DATA_INACTIVITY_TIMEOUT;
|
import static com.android.server.connectivity.ConnectivityFlags.CELLULAR_DATA_INACTIVITY_TIMEOUT;
|
||||||
|
import static com.android.server.connectivity.ConnectivityFlags.CLOSE_QUIC_CONNECTION;
|
||||||
import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
|
import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
|
||||||
import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
|
import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
|
||||||
import static com.android.server.connectivity.ConnectivityFlags.NAMESPACE_TETHERING_BOOT;
|
import static com.android.server.connectivity.ConnectivityFlags.NAMESPACE_TETHERING_BOOT;
|
||||||
|
|
@ -389,6 +390,7 @@ import com.android.server.connectivity.PermissionMonitor;
|
||||||
import com.android.server.connectivity.ProfileNetworkPreferenceInfo;
|
import com.android.server.connectivity.ProfileNetworkPreferenceInfo;
|
||||||
import com.android.server.connectivity.ProxyTracker;
|
import com.android.server.connectivity.ProxyTracker;
|
||||||
import com.android.server.connectivity.QosCallbackTracker;
|
import com.android.server.connectivity.QosCallbackTracker;
|
||||||
|
import com.android.server.connectivity.QuicConnectionCloser;
|
||||||
import com.android.server.connectivity.SatelliteAccessController;
|
import com.android.server.connectivity.SatelliteAccessController;
|
||||||
import com.android.server.connectivity.UidRangeUtils;
|
import com.android.server.connectivity.UidRangeUtils;
|
||||||
import com.android.server.connectivity.VpnNetworkPreferenceInfo;
|
import com.android.server.connectivity.VpnNetworkPreferenceInfo;
|
||||||
|
|
@ -1104,6 +1106,13 @@ public class ConnectivityService extends IConnectivityManager.Stub {
|
||||||
// Flag to drop packets to VPN addresses ingressing via non-VPN interfaces.
|
// Flag to drop packets to VPN addresses ingressing via non-VPN interfaces.
|
||||||
private final boolean mIngressToVpnAddressFiltering;
|
private final boolean mIngressToVpnAddressFiltering;
|
||||||
|
|
||||||
|
// Flag to close QUIC connection for registered sockets when apps lose network access.
|
||||||
|
private final boolean mCloseQuicConnection;
|
||||||
|
|
||||||
|
// This is null if mCloseQuicConnection is false
|
||||||
|
@Nullable
|
||||||
|
private final QuicConnectionCloser mQuicConnectionCloser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements support for the legacy "one network per network type" model.
|
* Implements support for the legacy "one network per network type" model.
|
||||||
*
|
*
|
||||||
|
|
@ -1825,6 +1834,14 @@ public class ConnectivityService extends IConnectivityManager.Stub {
|
||||||
handler.sendMessageDelayed(
|
handler.sendMessageDelayed(
|
||||||
handler.obtainMessage(EVENT_INITIAL_EVALUATION_TIMEOUT, network), delayMs);
|
handler.obtainMessage(EVENT_INITIAL_EVALUATION_TIMEOUT, network), delayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create {@link QuicConnectionCloser}.
|
||||||
|
*/
|
||||||
|
public QuicConnectionCloser makeQuicConnectionCloser(
|
||||||
|
final SparseArray<NetworkAgentInfo> networkForNetId, final Handler handler) {
|
||||||
|
return new QuicConnectionCloser(networkForNetId, handler);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConnectivityService(Context context) {
|
public ConnectivityService(Context context) {
|
||||||
|
|
@ -2133,6 +2150,13 @@ public class ConnectivityService extends IConnectivityManager.Stub {
|
||||||
&& mDeps.isFeatureNotChickenedOut(mContext, INGRESS_TO_VPN_ADDRESS_FILTERING);
|
&& mDeps.isFeatureNotChickenedOut(mContext, INGRESS_TO_VPN_ADDRESS_FILTERING);
|
||||||
|
|
||||||
mL2capNetworkProvider = mDeps.makeL2capNetworkProvider(mContext);
|
mL2capNetworkProvider = mDeps.makeL2capNetworkProvider(mContext);
|
||||||
|
|
||||||
|
mCloseQuicConnection = mDeps.isFeatureEnabled(context, CLOSE_QUIC_CONNECTION);
|
||||||
|
if (mCloseQuicConnection) {
|
||||||
|
mQuicConnectionCloser = mDeps.makeQuicConnectionCloser(mNetworkForNetId, mHandler);
|
||||||
|
} else {
|
||||||
|
mQuicConnectionCloser = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -3687,6 +3711,11 @@ public class ConnectivityService extends IConnectivityManager.Stub {
|
||||||
} catch (SocketException | InterruptedIOException | ErrnoException e) {
|
} catch (SocketException | InterruptedIOException | ErrnoException e) {
|
||||||
loge("Failed to destroy sockets: " + e);
|
loge("Failed to destroy sockets: " + e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mCloseQuicConnection) {
|
||||||
|
mQuicConnectionCloser.closeQuicConnectionByUids(uids);
|
||||||
|
}
|
||||||
|
|
||||||
mDestroySocketPendingUids.clear();
|
mDestroySocketPendingUids.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4516,6 +4545,12 @@ public class ConnectivityService extends IConnectivityManager.Stub {
|
||||||
mNetworkActivityTracker.dump(pw);
|
mNetworkActivityTracker.dump(pw);
|
||||||
pw.decreaseIndent();
|
pw.decreaseIndent();
|
||||||
|
|
||||||
|
pw.println();
|
||||||
|
pw.println("Close QUIC connection: " + mCloseQuicConnection);
|
||||||
|
if (mCloseQuicConnection) {
|
||||||
|
mQuicConnectionCloser.dump(pw);
|
||||||
|
}
|
||||||
|
|
||||||
pw.println();
|
pw.println();
|
||||||
pw.println("Multicast routing supported: " +
|
pw.println("Multicast routing supported: " +
|
||||||
(mMulticastRoutingCoordinatorService != null));
|
(mMulticastRoutingCoordinatorService != null));
|
||||||
|
|
@ -15125,8 +15160,33 @@ public class ConnectivityService extends IConnectivityManager.Stub {
|
||||||
return mIngressToVpnAddressFiltering;
|
return mIngressToVpnAddressFiltering;
|
||||||
case QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER:
|
case QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER:
|
||||||
return mQueueNetworkAgentEventsInSystemServer;
|
return mQueueNetworkAgentEventsInSystemServer;
|
||||||
|
case CLOSE_QUIC_CONNECTION:
|
||||||
|
return mCloseQuicConnection;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unknown flag: " + featureFlag);
|
throw new IllegalArgumentException("Unknown flag: " + featureFlag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerQuicConnectionClosePayload(final ParcelFileDescriptor pfd,
|
||||||
|
final byte[] payload) {
|
||||||
|
if (!mCloseQuicConnection) {
|
||||||
|
IoUtils.closeQuietly(pfd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// pfd is closed by registerQuicConnectionClosePayload
|
||||||
|
mQuicConnectionCloser.registerQuicConnectionClosePayload(mDeps.getCallingUid(),
|
||||||
|
pfd, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unregisterQuicConnectionClosePayload(final ParcelFileDescriptor pfd) {
|
||||||
|
if (!mCloseQuicConnection) {
|
||||||
|
IoUtils.closeQuietly(pfd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// pfd is closed by unregisterQuicConnectionClosePayload
|
||||||
|
mQuicConnectionCloser.unregisterQuicConnectionClosePayload(pfd);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ public final class ConnectivityFlags {
|
||||||
public static final String QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER =
|
public static final String QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER =
|
||||||
"queue_network_agent_events_in_system_server";
|
"queue_network_agent_events_in_system_server";
|
||||||
|
|
||||||
|
public static final String CLOSE_QUIC_CONNECTION = "close_quic_connection";
|
||||||
|
|
||||||
private boolean mNoRematchAllRequestsOnRegister;
|
private boolean mNoRematchAllRequestsOnRegister;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,406 @@
|
||||||
|
/*
|
||||||
|
* 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.connectivity;
|
||||||
|
|
||||||
|
import static android.system.OsConstants.ENOENT;
|
||||||
|
import static android.system.OsConstants.SOL_SOCKET;
|
||||||
|
import static android.system.OsConstants.SO_SNDTIMEO;
|
||||||
|
|
||||||
|
import android.annotation.NonNull;
|
||||||
|
import android.net.Network;
|
||||||
|
import android.net.NetworkUtils;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.system.ErrnoException;
|
||||||
|
import android.system.Os;
|
||||||
|
import android.system.StructTimeval;
|
||||||
|
import android.util.ArrayMap;
|
||||||
|
import android.util.IndentingPrintWriter;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.SparseArray;
|
||||||
|
|
||||||
|
import com.android.internal.annotations.GuardedBy;
|
||||||
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
|
import com.android.net.module.util.HandlerUtils;
|
||||||
|
import com.android.net.module.util.SharedLog;
|
||||||
|
import com.android.net.module.util.SkDestroyListener;
|
||||||
|
import com.android.net.module.util.netlink.InetDiagMessage;
|
||||||
|
|
||||||
|
import libcore.io.IoUtils;
|
||||||
|
|
||||||
|
import java.io.FileDescriptor;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
import java.net.DatagramSocket;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class QuicConnectionCloser {
|
||||||
|
private static final String TAG = QuicConnectionCloser.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final int SOCKET_WRITE_TIMEOUT_MS = 100;
|
||||||
|
|
||||||
|
// Map from socket cookie to QUIC connection close information
|
||||||
|
private final Map<Long, QuicConnectionCloseInfo>
|
||||||
|
mRegisteredQuicConnectionCloseInfos = new ArrayMap<>();
|
||||||
|
|
||||||
|
// The maximum number of QUIC connection close information entries that can be registered
|
||||||
|
// concurrently.
|
||||||
|
private static final int MAX_REGISTERED_QUIC_CONNECTION_CLOSE_INFO = 1000;
|
||||||
|
|
||||||
|
private final Handler mHandler;
|
||||||
|
|
||||||
|
// Reference to ConnectivityService#mNetworkForNetId, must be synchronized on itself.
|
||||||
|
// mHandler in this class is associated with the ConnectivityService handler thread.
|
||||||
|
// The only code that can update mNetworkForNetId runs on that thread, so no entries can be
|
||||||
|
// updated while the code in this class is running on mHandler thread.
|
||||||
|
@GuardedBy("mNetworkForNetId")
|
||||||
|
private final SparseArray<NetworkAgentInfo> mNetworkForNetId;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Dependencies mDeps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to store the necessary information for closing a QUIC connection.
|
||||||
|
*/
|
||||||
|
private static class QuicConnectionCloseInfo {
|
||||||
|
public final int uid;
|
||||||
|
public final int netId;
|
||||||
|
public final long cookie;
|
||||||
|
public final InetSocketAddress src;
|
||||||
|
public final InetSocketAddress dst;
|
||||||
|
public final byte[] payload;
|
||||||
|
|
||||||
|
QuicConnectionCloseInfo(int uid, int netId, long cookie, InetSocketAddress src,
|
||||||
|
InetSocketAddress dst, byte[] payload) {
|
||||||
|
this.uid = uid;
|
||||||
|
this.netId = netId;
|
||||||
|
this.cookie = cookie;
|
||||||
|
this.src = src;
|
||||||
|
this.dst = dst;
|
||||||
|
this.payload = payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "QuicConnectionCloseInfo{"
|
||||||
|
+ "uid: " + uid
|
||||||
|
+ ", netId: " + netId
|
||||||
|
+ ", cookie: " + cookie
|
||||||
|
+ ", src: " + src
|
||||||
|
+ ", dst: " + dst
|
||||||
|
+ ", payload length: " + payload.length
|
||||||
|
+ "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Dependencies {
|
||||||
|
/**
|
||||||
|
* Send a UDP packet with the specified source and destination address and port over the
|
||||||
|
* specified network.
|
||||||
|
*
|
||||||
|
* @param network The {@link Network} over which the UDP packet will be sent.
|
||||||
|
* @param src The source {@link InetSocketAddress} of the UDP packet to be sent.
|
||||||
|
* @param dst The destination {@link InetSocketAddress} of the UDP packet to be sent.
|
||||||
|
* @param payload The UDP payload to be sent.
|
||||||
|
*/
|
||||||
|
public void sendQuicConnectionClosePayload(final Network network,
|
||||||
|
final InetSocketAddress src, final InetSocketAddress dst, final byte[] payload)
|
||||||
|
throws IOException, ErrnoException {
|
||||||
|
final DatagramSocket socket = new DatagramSocket(src);
|
||||||
|
network.bindSocket(socket);
|
||||||
|
socket.connect(dst);
|
||||||
|
Os.setsockoptTimeval(socket.getFileDescriptor$(), SOL_SOCKET, SO_SNDTIMEO,
|
||||||
|
StructTimeval.fromMillis(SOCKET_WRITE_TIMEOUT_MS));
|
||||||
|
Os.write(socket.getFileDescriptor$(), payload, 0 /* byteOffset */, payload.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call {@link InetDiagMessage#destroyUdpSocket}
|
||||||
|
*/
|
||||||
|
public void destroyUdpSocket(final InetSocketAddress src, final InetSocketAddress dst,
|
||||||
|
final long cookie)
|
||||||
|
throws SocketException, InterruptedIOException, ErrnoException {
|
||||||
|
InetDiagMessage.destroyUdpSocket(src, dst, cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call {@link SkDestroyListener#makeSkDestroyListener}
|
||||||
|
*/
|
||||||
|
public SkDestroyListener makeSkDestroyListener(final Consumer<InetDiagMessage> consumer,
|
||||||
|
final Handler handler) {
|
||||||
|
return SkDestroyListener.makeSkDestroyListener(consumer, false /* monitorTcpSocket */,
|
||||||
|
true /* monitorUdpSocket */, handler, new SharedLog(TAG));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call {@link NetworkUtils#getSocketCookie}
|
||||||
|
*/
|
||||||
|
public long getSocketCookie(final FileDescriptor fd) throws ErrnoException {
|
||||||
|
return NetworkUtils.getSocketCookie(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call {@link Os#getsockoptInt}
|
||||||
|
*/
|
||||||
|
public int getsockoptInt(final FileDescriptor fd, final int level, final int option)
|
||||||
|
throws ErrnoException {
|
||||||
|
return Os.getsockoptInt(fd, level, option);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call {@link Os#getsockname}}
|
||||||
|
*/
|
||||||
|
public InetSocketAddress getsockname(final FileDescriptor fd) throws ErrnoException {
|
||||||
|
return (InetSocketAddress) Os.getsockname(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call {@link Os#getpeername}
|
||||||
|
*/
|
||||||
|
public InetSocketAddress getpeername(final FileDescriptor fd) throws ErrnoException {
|
||||||
|
return (InetSocketAddress) Os.getpeername(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuicConnectionCloser(final SparseArray<NetworkAgentInfo> networkForNetId,
|
||||||
|
final Handler handler) {
|
||||||
|
this(networkForNetId, handler, new Dependencies());
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public QuicConnectionCloser(final SparseArray<NetworkAgentInfo> networkForNetId,
|
||||||
|
final Handler handler, final Dependencies deps) {
|
||||||
|
mNetworkForNetId = networkForNetId;
|
||||||
|
mHandler = handler;
|
||||||
|
mDeps = deps;
|
||||||
|
|
||||||
|
// handleUdpSocketDestroy must be posted to the thread to avoid racing with
|
||||||
|
// handleUnregisterQuicConnectionCloseInfo, even though they both run on the same thread.
|
||||||
|
// Specifically, the following can happen:
|
||||||
|
// unregisterQuicConnectionClosePayload posts handleUnregisterQuicConnectionCloseInfo to
|
||||||
|
// the handler and then closes the fd.
|
||||||
|
// The close() will cause the kernel to enqueue a netlink message to the SkDestroyHandler's
|
||||||
|
// fd.
|
||||||
|
// It's possible that the MessageQueue of the handler sees both the SkDestroyHandler's fd
|
||||||
|
// and the MessageQueue fd go active at the same time, and chooses to run the netlink
|
||||||
|
// event first.
|
||||||
|
// As a result, handleUdpSocketDestroy runs before handleUnregisterQuicConnectionCloseInfo,
|
||||||
|
// and the code incorrectly sends a close packet for a socket that the app has already
|
||||||
|
// unregistered.
|
||||||
|
// Posting handleUdpSocketDestroy to the handler ensures that it always runs after
|
||||||
|
// handleUnregisterQuicConnectionCloseInfo. It doesn't matter if the
|
||||||
|
// handleUdpSocketDestroy is delayed, because it will only send a packet if
|
||||||
|
// mRegisteredQuicConnectionCloseInfos contains the socket cookie, and socket cookies are
|
||||||
|
// never reused.
|
||||||
|
final SkDestroyListener udpSkDestroyListener = mDeps.makeSkDestroyListener(
|
||||||
|
(inetDiagMessage) -> handler.post(() -> handleUdpSocketDestroy(inetDiagMessage)),
|
||||||
|
handler);
|
||||||
|
handler.post(udpSkDestroyListener::start);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureRunningOnHandlerThread() {
|
||||||
|
HandlerUtils.ensureRunningOnHandlerThread(mHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close registered QUIC connection by uids
|
||||||
|
*
|
||||||
|
* @param uids target uids to close QUIC connections
|
||||||
|
*/
|
||||||
|
public void closeQuicConnectionByUids(final Set<Integer> uids) {
|
||||||
|
ensureRunningOnHandlerThread();
|
||||||
|
|
||||||
|
for (Iterator<Map.Entry<Long, QuicConnectionCloseInfo>> it =
|
||||||
|
mRegisteredQuicConnectionCloseInfos.entrySet().iterator(); it.hasNext();) {
|
||||||
|
final QuicConnectionCloseInfo info = it.next().getValue();
|
||||||
|
if (uids.contains(info.uid)) {
|
||||||
|
closeQuicConnection(info, true /* destroySocket */);
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NetworkAgentInfo getNetworkAgentInfoForNetId(int netId) {
|
||||||
|
synchronized (mNetworkForNetId) {
|
||||||
|
return mNetworkForNetId.get(netId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeQuicConnection(final QuicConnectionCloseInfo info,
|
||||||
|
final boolean destroySocket) {
|
||||||
|
ensureRunningOnHandlerThread();
|
||||||
|
|
||||||
|
Log.d(TAG, "Close QUIC socket for " + info + ", destroySocket=" + destroySocket);
|
||||||
|
final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(info.netId);
|
||||||
|
if (nai == null || !nai.linkProperties.getAddresses().contains(info.src.getAddress())) {
|
||||||
|
// The device should not send a packet with an unused source address.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destroySocket) {
|
||||||
|
try {
|
||||||
|
mDeps.destroyUdpSocket(info.src, info.dst, info.cookie);
|
||||||
|
} catch (ErrnoException | SocketException | InterruptedIOException e) {
|
||||||
|
if (e instanceof ErrnoException && ((ErrnoException) e).errno == ENOENT) {
|
||||||
|
// This can happen if the socket is already closed, but unregister message is
|
||||||
|
// not processed yet.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.e(TAG, "Failed to destroy QUIC socket for " + info + ": " + e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mDeps.sendQuicConnectionClosePayload(nai.network(), info.src, info.dst, info.payload);
|
||||||
|
} catch (ErrnoException | IOException e) {
|
||||||
|
Log.e(TAG, "Failed to send registered QUIC connection close payload for "
|
||||||
|
+ info + ": " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close QUIC connection if the registered socket was destroyed
|
||||||
|
*
|
||||||
|
* @param inetDiagMessage {@link InetDiagMessage} received from kernel
|
||||||
|
*/
|
||||||
|
private void handleUdpSocketDestroy(final InetDiagMessage inetDiagMessage) {
|
||||||
|
ensureRunningOnHandlerThread();
|
||||||
|
|
||||||
|
final long cookie = inetDiagMessage.inetDiagMsg.id.cookie;
|
||||||
|
final QuicConnectionCloseInfo info = mRegisteredQuicConnectionCloseInfos.remove(cookie);
|
||||||
|
if (info == null) {
|
||||||
|
// Destroyed socket is not registered or already unregistered.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// App registered a QUIC connection close payload and this socket, but the socket was
|
||||||
|
// closed before the socket was unregistered.
|
||||||
|
// This can happen if the app crashes or is killed.
|
||||||
|
closeQuicConnection(info, false /* destroySocket */);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the QUIC connection close information
|
||||||
|
*
|
||||||
|
* @param info {@link QuicConnectionCloseInfo} to register
|
||||||
|
*/
|
||||||
|
private void handleRegisterQuicConnectionCloseInfo(final QuicConnectionCloseInfo info) {
|
||||||
|
ensureRunningOnHandlerThread();
|
||||||
|
|
||||||
|
if (mRegisteredQuicConnectionCloseInfos.size()
|
||||||
|
>= MAX_REGISTERED_QUIC_CONNECTION_CLOSE_INFO) {
|
||||||
|
Log.e(TAG, "Failed to register QUIC connection close information."
|
||||||
|
+ " number of registered information exceeded "
|
||||||
|
+ MAX_REGISTERED_QUIC_CONNECTION_CLOSE_INFO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mRegisteredQuicConnectionCloseInfos.put(info.cookie, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Use OsConstants.SO_MARK once this API is available
|
||||||
|
private static final int SO_MARK = 36;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register QUIC socket and connection close payload
|
||||||
|
*
|
||||||
|
* @param uid The uid of the socket owner
|
||||||
|
* @param pfd The {@link ParcelFileDescriptor} for the connected UDP socket.
|
||||||
|
* @param payload The UDP payload that can close QUIC connection.
|
||||||
|
*/
|
||||||
|
public void registerQuicConnectionClosePayload(final int uid, final ParcelFileDescriptor pfd,
|
||||||
|
final byte[] payload) {
|
||||||
|
try {
|
||||||
|
final FileDescriptor fd = pfd.getFileDescriptor();
|
||||||
|
// See FWMARK_NET_ID_MASK in Fwmark.h
|
||||||
|
final int netId = mDeps.getsockoptInt(fd, SOL_SOCKET, SO_MARK) & 0xffff;
|
||||||
|
// The peer name could change if the socket is reconnected to a different server.
|
||||||
|
// But, it is the caller's responsibility to avoid this, as explicitly stated in the
|
||||||
|
// Javadoc of ConnectivityManager#registerQuicConnectionClosePayload.
|
||||||
|
final QuicConnectionCloseInfo info = new QuicConnectionCloseInfo(
|
||||||
|
uid,
|
||||||
|
netId,
|
||||||
|
mDeps.getSocketCookie(fd),
|
||||||
|
mDeps.getsockname(fd),
|
||||||
|
mDeps.getpeername(fd),
|
||||||
|
payload);
|
||||||
|
mHandler.post(() -> handleRegisterQuicConnectionCloseInfo(info));
|
||||||
|
} catch (ErrnoException e) {
|
||||||
|
Log.e(TAG, "Failed to register QUIC connection close information", e);
|
||||||
|
} finally {
|
||||||
|
// |pfd| must remain open until the message is posted to the handler thread, because
|
||||||
|
// doing so ensures that the socket is not closed even if the app calls close().
|
||||||
|
// Keeping it open avoids the following threading issue with SkDestroyListener :
|
||||||
|
// If the PFD was closed before posting the message, then SkDestroyListener might
|
||||||
|
// get a "socket destroyed" message from Netlink immediately and post to the handler
|
||||||
|
// thread before this method posts its own message, resulting in a leak of the
|
||||||
|
// mRegisteredQuicConnectionCloseInfo entry.
|
||||||
|
// While the PFD is held, Netlink will not send the socket destroy message to
|
||||||
|
// SkDestroyListener, and thus the message posted by SkDestroyListener as
|
||||||
|
// a reaction to it will be processed after the message posted by this method.
|
||||||
|
IoUtils.closeQuietly(pfd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister the QUIC connection close information
|
||||||
|
*
|
||||||
|
* @param cookie cookie of the socket whose connection close information should be unregistered
|
||||||
|
*/
|
||||||
|
private void handleUnregisterQuicConnectionCloseInfo(final long cookie) {
|
||||||
|
ensureRunningOnHandlerThread();
|
||||||
|
|
||||||
|
mRegisteredQuicConnectionCloseInfos.remove(cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister the QUIC socket
|
||||||
|
*
|
||||||
|
* @param pfd The {@link ParcelFileDescriptor} for the UDP socket.
|
||||||
|
*/
|
||||||
|
public void unregisterQuicConnectionClosePayload(final ParcelFileDescriptor pfd) {
|
||||||
|
try {
|
||||||
|
final long cookie = mDeps.getSocketCookie(pfd.getFileDescriptor());
|
||||||
|
mHandler.post(() -> handleUnregisterQuicConnectionCloseInfo(cookie));
|
||||||
|
} catch (ErrnoException e) {
|
||||||
|
Log.e(TAG, "Failed to unregister QUIC connection close information: " + e);
|
||||||
|
} finally {
|
||||||
|
// |pfd| must be closed after posting the message to the handler thread to avoid the
|
||||||
|
// threading issue with SkDestroyListener.
|
||||||
|
// See the comment in registerQuicConnectionClosePayload.
|
||||||
|
IoUtils.closeQuietly(pfd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dump QUIC connection closer information
|
||||||
|
*/
|
||||||
|
public void dump(final IndentingPrintWriter pw) {
|
||||||
|
pw.println("Registered QUIC connection close information: "
|
||||||
|
+ mRegisteredQuicConnectionCloseInfos.size());
|
||||||
|
pw.increaseIndent();
|
||||||
|
for (QuicConnectionCloseInfo info: mRegisteredQuicConnectionCloseInfos.values()) {
|
||||||
|
pw.println(info);
|
||||||
|
}
|
||||||
|
pw.decreaseIndent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -491,7 +491,7 @@ public class InetDiagMessage extends NetlinkMessage {
|
||||||
* Close the udp socket which can be uniquely identified with the cookie and other information.
|
* Close the udp socket which can be uniquely identified with the cookie and other information.
|
||||||
*/
|
*/
|
||||||
public static void destroyUdpSocket(final InetSocketAddress src, final InetSocketAddress dst,
|
public static void destroyUdpSocket(final InetSocketAddress src, final InetSocketAddress dst,
|
||||||
final int ifIndex, final long cookie)
|
final long cookie)
|
||||||
throws ErrnoException, SocketException, InterruptedIOException {
|
throws ErrnoException, SocketException, InterruptedIOException {
|
||||||
FileDescriptor fd = null;
|
FileDescriptor fd = null;
|
||||||
|
|
||||||
|
|
@ -502,7 +502,7 @@ public class InetDiagMessage extends NetlinkMessage {
|
||||||
final StructInetDiagSockId id = new StructInetDiagSockId(
|
final StructInetDiagSockId id = new StructInetDiagSockId(
|
||||||
src,
|
src,
|
||||||
dst,
|
dst,
|
||||||
ifIndex,
|
0 /* ifIndex */,
|
||||||
cookie
|
cookie
|
||||||
);
|
);
|
||||||
sendNetlinkDestroyRequest(fd, IPPROTO_UDP, id, (short) family, 0 /* state */);
|
sendNetlinkDestroyRequest(fd, IPPROTO_UDP, id, (short) family, 0 /* state */);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import static android.Manifest.permission.ACCESS_FINE_LOCATION;
|
||||||
import static android.Manifest.permission.ACCESS_NETWORK_STATE;
|
import static android.Manifest.permission.ACCESS_NETWORK_STATE;
|
||||||
import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
|
import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
|
||||||
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
|
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
|
||||||
|
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
|
||||||
import static android.Manifest.permission.NETWORK_FACTORY;
|
import static android.Manifest.permission.NETWORK_FACTORY;
|
||||||
import static android.Manifest.permission.NETWORK_SETTINGS;
|
import static android.Manifest.permission.NETWORK_SETTINGS;
|
||||||
import static android.Manifest.permission.NETWORK_SETUP_WIZARD;
|
import static android.Manifest.permission.NETWORK_SETUP_WIZARD;
|
||||||
|
|
@ -100,6 +101,11 @@ import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERE
|
||||||
import static android.system.OsConstants.AF_INET;
|
import static android.system.OsConstants.AF_INET;
|
||||||
import static android.system.OsConstants.AF_INET6;
|
import static android.system.OsConstants.AF_INET6;
|
||||||
import static android.system.OsConstants.AF_UNSPEC;
|
import static android.system.OsConstants.AF_UNSPEC;
|
||||||
|
import static android.system.OsConstants.ECONNABORTED;
|
||||||
|
import static android.system.OsConstants.EDESTADDRREQ;
|
||||||
|
import static android.system.OsConstants.IPPROTO_UDP;
|
||||||
|
import static android.system.OsConstants.POLLIN;
|
||||||
|
import static android.system.OsConstants.SOCK_DGRAM;
|
||||||
|
|
||||||
import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
|
import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
|
||||||
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
|
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
|
||||||
|
|
@ -182,7 +188,9 @@ import android.os.ConditionVariable;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.MessageQueue;
|
import android.os.MessageQueue;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
|
import android.os.RemoteException;
|
||||||
import android.os.ServiceManager;
|
import android.os.ServiceManager;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
|
|
@ -190,6 +198,9 @@ import android.os.VintfRuntimeInfo;
|
||||||
import android.platform.test.annotations.AppModeFull;
|
import android.platform.test.annotations.AppModeFull;
|
||||||
import android.provider.DeviceConfig;
|
import android.provider.DeviceConfig;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
import android.system.ErrnoException;
|
||||||
|
import android.system.Os;
|
||||||
|
import android.system.StructPollfd;
|
||||||
import android.telephony.SubscriptionManager;
|
import android.telephony.SubscriptionManager;
|
||||||
import android.telephony.TelephonyManager;
|
import android.telephony.TelephonyManager;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
@ -205,6 +216,10 @@ import com.android.internal.util.ArrayUtils;
|
||||||
import com.android.modules.utils.build.SdkLevel;
|
import com.android.modules.utils.build.SdkLevel;
|
||||||
import com.android.net.module.util.CollectionUtils;
|
import com.android.net.module.util.CollectionUtils;
|
||||||
import com.android.net.module.util.DnsPacket;
|
import com.android.net.module.util.DnsPacket;
|
||||||
|
import com.android.net.module.util.Struct;
|
||||||
|
import com.android.net.module.util.structs.Ipv4Header;
|
||||||
|
import com.android.net.module.util.structs.Ipv6Header;
|
||||||
|
import com.android.net.module.util.structs.UdpHeader;
|
||||||
import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
|
import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
|
||||||
import com.android.networkstack.apishim.ConstantsShim;
|
import com.android.networkstack.apishim.ConstantsShim;
|
||||||
import com.android.networkstack.apishim.NetworkInformationShimImpl;
|
import com.android.networkstack.apishim.NetworkInformationShimImpl;
|
||||||
|
|
@ -228,6 +243,7 @@ import com.android.testutils.TestableNetworkCallback;
|
||||||
|
|
||||||
import junit.framework.AssertionFailedError;
|
import junit.framework.AssertionFailedError;
|
||||||
|
|
||||||
|
import libcore.io.IoUtils;
|
||||||
import libcore.io.Streams;
|
import libcore.io.Streams;
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
|
@ -253,6 +269,7 @@ import java.net.MalformedURLException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
@ -352,6 +369,15 @@ public class ConnectivityManagerTest {
|
||||||
// Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
|
// Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
|
||||||
private static final long WIFI_CONNECT_TIMEOUT_MS = 60_000L;
|
private static final long WIFI_CONNECT_TIMEOUT_MS = 60_000L;
|
||||||
|
|
||||||
|
// Timeout for waiting the QUIC connection close information registration/unregistration
|
||||||
|
private static final long QUIC_CONNECTION_CLOSE_INFO_REGISTRATION_TIMEOUT_MS = 500L;
|
||||||
|
|
||||||
|
// Timeout for waiting the QUIC connection close packet to be sent
|
||||||
|
private static final long QUIC_CONNECTION_CLOSE_PACKET_TIMEOUT_MS = 200L;
|
||||||
|
|
||||||
|
// Name of the feature flag for closing quic connection
|
||||||
|
private static final String CLOSE_QUIC_CONNECTION = "close_quic_connection";
|
||||||
|
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
private Instrumentation mInstrumentation;
|
private Instrumentation mInstrumentation;
|
||||||
private ConnectivityManager mCm;
|
private ConnectivityManager mCm;
|
||||||
|
|
@ -2389,8 +2415,8 @@ public class ConnectivityManagerTest {
|
||||||
waitForAvailable(cb);
|
waitForAvailable(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void waitForAvailable(@NonNull final TestableNetworkCallback cb) {
|
private CallbackEntry.Available waitForAvailable(@NonNull final TestableNetworkCallback cb) {
|
||||||
cb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
|
return cb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
|
||||||
c -> c instanceof CallbackEntry.Available);
|
c -> c instanceof CallbackEntry.Available);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4131,4 +4157,302 @@ public class ConnectivityManagerTest {
|
||||||
assertThrows(UnsupportedOperationException.class, () -> mCm.tether("iface"));
|
assertThrows(UnsupportedOperationException.class, () -> mCm.tether("iface"));
|
||||||
assertThrows(UnsupportedOperationException.class, () -> mCm.untether("iface"));
|
assertThrows(UnsupportedOperationException.class, () -> mCm.untether("iface"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ParcelFileDescriptor setupTestNetworkAndGetFd() {
|
||||||
|
return runWithShellPermissionIdentity(() -> {
|
||||||
|
final TestNetworkManager tnm = mContext.getSystemService(TestNetworkManager.class);
|
||||||
|
final List<LinkAddress> linkAddresses = List.of(new LinkAddress("192.0.2.2/24"),
|
||||||
|
new LinkAddress("2001:db8:1:2::2/64"));
|
||||||
|
final TestNetworkInterface iface = tnm.createTunInterface(linkAddresses);
|
||||||
|
tnm.setupTestNetwork(iface.getInterfaceName(), new Binder());
|
||||||
|
return iface.getFileDescriptor();
|
||||||
|
}, MANAGE_TEST_NETWORKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Network getTestNetwork() {
|
||||||
|
final TestableNetworkCallback callback = networkCallbackRule.requestNetwork(
|
||||||
|
new NetworkRequest.Builder()
|
||||||
|
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
|
.removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
|
||||||
|
.addTransportType(TRANSPORT_TEST)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
return waitForAvailable(callback).getNetwork();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitForQuicConnectionCloseInfoRegistration(final int myUid,
|
||||||
|
final boolean expectRegistered)
|
||||||
|
throws ErrnoException, RemoteException, InterruptedException {
|
||||||
|
final long timeout = SystemClock.elapsedRealtime()
|
||||||
|
+ QUIC_CONNECTION_CLOSE_INFO_REGISTRATION_TIMEOUT_MS;
|
||||||
|
while (timeout > SystemClock.elapsedRealtime()) {
|
||||||
|
if (DumpTestUtils.dumpServiceWithShellPermission(
|
||||||
|
Context.CONNECTIVITY_SERVICE, "--short")
|
||||||
|
.contains("QuicConnectionCloseInfo{uid: " + myUid) == expectRegistered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Thread.sleep(50);
|
||||||
|
}
|
||||||
|
fail("Failed to register/unregister QUIC connection close information in "
|
||||||
|
+ QUIC_CONNECTION_CLOSE_INFO_REGISTRATION_TIMEOUT_MS
|
||||||
|
+ "ms, expectRegistered=" + expectRegistered);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doTestRegisterQuicConnectionClosePayload(final boolean isV6,
|
||||||
|
final boolean unregister, final boolean closeSocket, final boolean blockNetwork,
|
||||||
|
final boolean expectPacketSent) throws Exception {
|
||||||
|
final InetAddress dstAddress = isV6 ? InetAddresses.parseNumericAddress("2001:db8:1:2::3")
|
||||||
|
: InetAddresses.parseNumericAddress("192.0.2.3");
|
||||||
|
final InetSocketAddress dstSockAddress = new InetSocketAddress(dstAddress, 443);
|
||||||
|
final int myUid = Process.myUid();
|
||||||
|
|
||||||
|
final ParcelFileDescriptor tunFd = setupTestNetworkAndGetFd();
|
||||||
|
final Network testNetwork = getTestNetwork();
|
||||||
|
|
||||||
|
// Firewall chain status will be restored after the test.
|
||||||
|
final boolean wasChainEnabled = runWithShellPermissionIdentity(() ->
|
||||||
|
mCm.getFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND), NETWORK_SETTINGS);
|
||||||
|
final int previousUidFirewallRule = runWithShellPermissionIdentity(() ->
|
||||||
|
mCm.getUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, myUid), NETWORK_SETTINGS);
|
||||||
|
runWithShellPermissionIdentity(() -> {
|
||||||
|
mCm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true /* enable */);
|
||||||
|
mCm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, myUid, FIREWALL_RULE_ALLOW);
|
||||||
|
}, NETWORK_SETTINGS);
|
||||||
|
|
||||||
|
final FileDescriptor sock = Os.socket(isV6 ? AF_INET6 : AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||||
|
final ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(sock.getInt$());
|
||||||
|
testNetwork.bindSocket(sock);
|
||||||
|
Os.connect(sock, dstSockAddress);
|
||||||
|
final InetSocketAddress srcSockAddress = (InetSocketAddress) Os.getsockname(sock);
|
||||||
|
|
||||||
|
testAndCleanup(() -> {
|
||||||
|
final Random random = new Random();
|
||||||
|
final byte[] payload = new byte[100];
|
||||||
|
random.nextBytes(payload);
|
||||||
|
|
||||||
|
// register/unregisterQuicConnectionClosePayload are oneway binder calls,
|
||||||
|
// while setUidFirewallRule is a two-way binder call. So if
|
||||||
|
// setUidFirewallRule is called immediately after them, it's possible that
|
||||||
|
// setUidFirewallRule could be processed before them.
|
||||||
|
// To ensure that uid networking is blocked after the connection close information
|
||||||
|
// is registered/unregistered, wait for the registration/unregistration.
|
||||||
|
mCm.registerQuicConnectionClosePayload(pfd, payload);
|
||||||
|
waitForQuicConnectionCloseInfoRegistration(myUid, true /* expectRegistered */);
|
||||||
|
if (unregister) {
|
||||||
|
mCm.unregisterQuicConnectionClosePayload(pfd);
|
||||||
|
waitForQuicConnectionCloseInfoRegistration(myUid, false /* expectRegistered */);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeSocket) {
|
||||||
|
Os.close(sock);
|
||||||
|
pfd.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockNetwork) {
|
||||||
|
runWithShellPermissionIdentity(() ->
|
||||||
|
mCm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, myUid,
|
||||||
|
FIREWALL_RULE_DENY), NETWORK_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
final long timeout = SystemClock.elapsedRealtime()
|
||||||
|
+ QUIC_CONNECTION_CLOSE_PACKET_TIMEOUT_MS;
|
||||||
|
long remainingTimeMs;
|
||||||
|
while ((remainingTimeMs = timeout - SystemClock.elapsedRealtime()) > 0) {
|
||||||
|
final StructPollfd pollfd = new StructPollfd();
|
||||||
|
pollfd.events = (short) POLLIN;
|
||||||
|
pollfd.fd = tunFd.getFileDescriptor();
|
||||||
|
final int ret = Os.poll(new StructPollfd[] { pollfd }, (int) remainingTimeMs);
|
||||||
|
if (ret == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] recvData = new byte[200];
|
||||||
|
final int readSize = Os.read(tunFd.getFileDescriptor(), recvData,
|
||||||
|
0 /* byteOffset */, recvData.length);
|
||||||
|
|
||||||
|
if (readSize < payload.length
|
||||||
|
|| !Arrays.equals(payload, 0, payload.length,
|
||||||
|
recvData, readSize - payload.length, readSize)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// If the control comes here then the payload was received, otherwise poll would
|
||||||
|
// have returned 0 or the test above would have matched it and gone to continue.
|
||||||
|
if (!expectPacketSent) {
|
||||||
|
fail("Unexpectedly received the QUIC connection close packet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final ByteBuffer buf = ByteBuffer.wrap(recvData, 0 /* offset */, readSize);
|
||||||
|
if (isV6) {
|
||||||
|
final Ipv6Header header = Struct.parse(Ipv6Header.class, buf);
|
||||||
|
assertEquals(srcSockAddress.getAddress(), header.srcIp);
|
||||||
|
assertEquals(dstSockAddress.getAddress(), header.dstIp);
|
||||||
|
} else {
|
||||||
|
final Ipv4Header header = Struct.parse(Ipv4Header.class, buf);
|
||||||
|
assertEquals(srcSockAddress.getAddress(), header.srcIp);
|
||||||
|
assertEquals(dstSockAddress.getAddress(), header.dstIp);
|
||||||
|
}
|
||||||
|
final UdpHeader udpHeader = Struct.parse(UdpHeader.class, buf);
|
||||||
|
assertEquals(srcSockAddress.getPort(), udpHeader.srcPort);
|
||||||
|
assertEquals(dstSockAddress.getPort(), udpHeader.dstPort);
|
||||||
|
assertEquals(payload.length + Struct.getSize(UdpHeader.class), udpHeader.length);
|
||||||
|
|
||||||
|
if (!closeSocket) {
|
||||||
|
// After the socket is destroyed and QUIC connection is closed by
|
||||||
|
// ConnectivityService, writing to the UDP socket should throw.
|
||||||
|
try {
|
||||||
|
Os.write(sock, payload, 0 /* byteOffset */, payload.length);
|
||||||
|
fail("Write to the destroyed socket must throw.");
|
||||||
|
} catch (ErrnoException e) {
|
||||||
|
assertEquals(EDESTADDRREQ, e.errno);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Os.sendto(sock, payload, 0 /* byteOffset */,
|
||||||
|
payload.length, 0 /* flags */, dstSockAddress);
|
||||||
|
fail("Sendto with the destroyed socket must throw.");
|
||||||
|
} catch (ErrnoException e) {
|
||||||
|
assertEquals(ECONNABORTED, e.errno);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (expectPacketSent) {
|
||||||
|
fail("Did not receive the QUIC connection close packet.");
|
||||||
|
}
|
||||||
|
}, /* cleanup */ () -> {
|
||||||
|
runWithShellPermissionIdentity(() -> {
|
||||||
|
mContext.getSystemService(TestNetworkManager.class).teardownTestNetwork(
|
||||||
|
testNetwork);
|
||||||
|
}, MANAGE_TEST_NETWORKS);
|
||||||
|
IoUtils.closeQuietly(tunFd);
|
||||||
|
IoUtils.closeQuietly(sock);
|
||||||
|
IoUtils.closeQuietly(pfd);
|
||||||
|
}, /* cleanup */ () -> {
|
||||||
|
// Restore firewall chain global status
|
||||||
|
runWithShellPermissionIdentity(() -> {
|
||||||
|
mCm.setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, wasChainEnabled);
|
||||||
|
}, NETWORK_SETTINGS);
|
||||||
|
}, /* cleanup */ () -> {
|
||||||
|
// Restore firewall chain status for myUid
|
||||||
|
runWithShellPermissionIdentity(() -> {
|
||||||
|
try {
|
||||||
|
mCm.setUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, myUid,
|
||||||
|
previousUidFirewallRule);
|
||||||
|
} catch (IllegalStateException ignored) {
|
||||||
|
// Removing match causes an exception when the rule entry for the uid does
|
||||||
|
// not exist. But this is fine and can be ignored.
|
||||||
|
}
|
||||||
|
}, NETWORK_SETTINGS);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@IgnoreUpTo(Build.VERSION_CODES.R)
|
||||||
|
@ConnectivityModuleTest
|
||||||
|
@AppModeFull(reason = "Cannot create test network in instant app mode")
|
||||||
|
public void testRegisterQuicConnectionClosePayload_blockNetwork() throws Exception {
|
||||||
|
assumeTrue(mCm.isConnectivityServiceFeatureEnabledForTesting(CLOSE_QUIC_CONNECTION));
|
||||||
|
|
||||||
|
// Network is blocked while the connection close payload is registered.
|
||||||
|
// Packet should be sent.
|
||||||
|
doTestRegisterQuicConnectionClosePayload(
|
||||||
|
false /* isV6 */, false /* unregister */, false /* closeSocket */,
|
||||||
|
true /* blockNetwork */, true /* expectPacketSent */);
|
||||||
|
doTestRegisterQuicConnectionClosePayload(
|
||||||
|
true /* isV6 */, false /* unregister */, false /* closeSocket */,
|
||||||
|
true /* blockNetwork */, true /* expectPacketSent */);
|
||||||
|
|
||||||
|
// Network is blocked after the connection close payload is unregistered.
|
||||||
|
// Packet should not be sent.
|
||||||
|
doTestRegisterQuicConnectionClosePayload(
|
||||||
|
false /* isV6 */, true /* unregister */, false /* closeSocket */,
|
||||||
|
true /* blockNetwork */, false /* expectPacketSent */);
|
||||||
|
doTestRegisterQuicConnectionClosePayload(
|
||||||
|
true /* isV6 */, true /* unregister */, false /* closeSocket */,
|
||||||
|
true /* blockNetwork */, false /* expectPacketSent */);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@IgnoreUpTo(Build.VERSION_CODES.R)
|
||||||
|
@ConnectivityModuleTest
|
||||||
|
@AppModeFull(reason = "Cannot create test network in instant app mode")
|
||||||
|
public void testRegisterQuicConnectionClosePayload_closeSocket() throws Exception {
|
||||||
|
assumeTrue(mCm.isConnectivityServiceFeatureEnabledForTesting(CLOSE_QUIC_CONNECTION));
|
||||||
|
|
||||||
|
// Registered socket is closed while the connection close payload is registered.
|
||||||
|
// Packet should be sent. This simulates that apps crash or are killed.
|
||||||
|
doTestRegisterQuicConnectionClosePayload(
|
||||||
|
false /* isV6 */, false /* unregister */, true /* closeSocket */,
|
||||||
|
false /* blockNetwork */, true /* expectPacketSent */);
|
||||||
|
doTestRegisterQuicConnectionClosePayload(
|
||||||
|
true /* isV6 */, false /* unregister */, true /* closeSocket */,
|
||||||
|
false /* blockNetwork */, true /* expectPacketSent */);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repeat register a connection close payload, unregister it, and then close the socket.
|
||||||
|
* The registered payload must not be sent because it is unregistered before the socket
|
||||||
|
* is closed.
|
||||||
|
* This test tries to detect threading issues between
|
||||||
|
* register/unregisterQuicConnectionClosePayload and socket destroy message handling.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@IgnoreUpTo(Build.VERSION_CODES.R)
|
||||||
|
@ConnectivityModuleTest
|
||||||
|
@AppModeFull(reason = "Cannot create test network in instant app mode")
|
||||||
|
public void testRegisterQuicConnectionClosePayload_closeSocketAfterUnregister()
|
||||||
|
throws Exception {
|
||||||
|
assumeTrue(mCm.isConnectivityServiceFeatureEnabledForTesting(CLOSE_QUIC_CONNECTION));
|
||||||
|
|
||||||
|
final ParcelFileDescriptor tunFd = setupTestNetworkAndGetFd();
|
||||||
|
final Network testNetwork = getTestNetwork();
|
||||||
|
|
||||||
|
final Random random = new Random();
|
||||||
|
final byte[] payload = new byte[100];
|
||||||
|
random.nextBytes(payload);
|
||||||
|
|
||||||
|
final InetSocketAddress dstSockAddress =
|
||||||
|
new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8:1:2::3"), 443);
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
final FileDescriptor sock = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
|
||||||
|
testNetwork.bindSocket(sock);
|
||||||
|
Os.connect(sock, dstSockAddress);
|
||||||
|
|
||||||
|
final ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(sock.getInt$());
|
||||||
|
mCm.registerQuicConnectionClosePayload(pfd, payload);
|
||||||
|
mCm.unregisterQuicConnectionClosePayload(pfd);
|
||||||
|
Os.close(sock);
|
||||||
|
pfd.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
testAndCleanup(() -> {
|
||||||
|
final long timeout = SystemClock.elapsedRealtime()
|
||||||
|
+ QUIC_CONNECTION_CLOSE_PACKET_TIMEOUT_MS;
|
||||||
|
long remainingTimeMs;
|
||||||
|
while ((remainingTimeMs = timeout - SystemClock.elapsedRealtime()) > 0) {
|
||||||
|
StructPollfd pollfd = new StructPollfd();
|
||||||
|
pollfd.events = (short) POLLIN;
|
||||||
|
pollfd.fd = tunFd.getFileDescriptor();
|
||||||
|
final int ret = Os.poll(new StructPollfd[]{pollfd}, (int) remainingTimeMs);
|
||||||
|
if (ret == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] recvData = new byte[200];
|
||||||
|
final int readSize = Os.read(tunFd.getFileDescriptor(), recvData,
|
||||||
|
0 /* byteOffset */, recvData.length);
|
||||||
|
if (readSize < payload.length
|
||||||
|
|| !Arrays.equals(payload, 0, payload.length,
|
||||||
|
recvData, readSize - payload.length, readSize)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fail("Unexpectedly received the QUIC connection close packet.");
|
||||||
|
}
|
||||||
|
}, /* cleanup */ () -> {
|
||||||
|
runWithShellPermissionIdentity(() -> {
|
||||||
|
mContext.getSystemService(TestNetworkManager.class).teardownTestNetwork(
|
||||||
|
testNetwork);
|
||||||
|
}, MANAGE_TEST_NETWORKS);
|
||||||
|
IoUtils.closeQuietly(tunFd);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2188,6 +2188,7 @@ public class ConnectivityServiceTest {
|
||||||
case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
|
case ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS:
|
||||||
case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
|
case ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS:
|
||||||
case ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN:
|
case ConnectivityFlags.BACKGROUND_FIREWALL_CHAIN:
|
||||||
|
case ConnectivityFlags.CLOSE_QUIC_CONNECTION:
|
||||||
case ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION:
|
case ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
/*
|
||||||
|
* 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.connectivity
|
||||||
|
|
||||||
|
import android.net.InetAddresses
|
||||||
|
import android.net.LinkAddress
|
||||||
|
import android.net.LinkProperties
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.TEST_IFACE
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.system.OsConstants.SOL_SOCKET
|
||||||
|
import android.util.SparseArray
|
||||||
|
import com.android.net.module.util.SkDestroyListener
|
||||||
|
import com.android.net.module.util.netlink.InetDiagMessage
|
||||||
|
import com.android.net.module.util.netlink.StructInetDiagSockId
|
||||||
|
import com.android.net.module.util.netlink.StructNlMsgHdr
|
||||||
|
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
|
||||||
|
import com.android.testutils.DevSdkIgnoreRunner
|
||||||
|
import com.android.testutils.visibleOnHandlerThread
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.util.function.Consumer
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.ArgumentCaptor
|
||||||
|
import org.mockito.InOrder
|
||||||
|
import org.mockito.Mockito.any
|
||||||
|
import org.mockito.Mockito.anyLong
|
||||||
|
import org.mockito.Mockito.doReturn
|
||||||
|
import org.mockito.Mockito.eq
|
||||||
|
import org.mockito.Mockito.inOrder
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
|
import org.mockito.Mockito.never
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
|
|
||||||
|
private const val TEST_UID = 1234
|
||||||
|
private const val TEST_NETID = 789
|
||||||
|
private const val TEST_SOCKET_COOKIE = 12321L
|
||||||
|
|
||||||
|
// TODO: Use OsConstants.SO_MARK once this API is available
|
||||||
|
private const val SO_MARK = 36
|
||||||
|
|
||||||
|
private val TEST_SRC_ADDRESS = InetAddresses.parseNumericAddress("2001:db8:1:2::2")
|
||||||
|
private val TEST_SRC_SOCKET_ADDRESS = InetSocketAddress(
|
||||||
|
TEST_SRC_ADDRESS,
|
||||||
|
1234
|
||||||
|
)
|
||||||
|
private val TEST_DST_SOCKET_ADDRESS = InetSocketAddress(
|
||||||
|
InetAddresses.parseNumericAddress("2001:db8:1:2::3"),
|
||||||
|
443
|
||||||
|
)
|
||||||
|
private val TEST_LP = LinkProperties().apply {
|
||||||
|
interfaceName = TEST_IFACE
|
||||||
|
addLinkAddress(LinkAddress(TEST_SRC_ADDRESS, 64))
|
||||||
|
}
|
||||||
|
private val TEST_NETWORK = Network(TEST_NETID)
|
||||||
|
private val TEST_PAYLOAD = byteArrayOf(0, 1, 2, 3, 4, 5)
|
||||||
|
|
||||||
|
@RunWith(DevSdkIgnoreRunner::class)
|
||||||
|
@IgnoreUpTo(Build.VERSION_CODES.R)
|
||||||
|
class QuicConnectionCloserTest {
|
||||||
|
private val pfd = mock(ParcelFileDescriptor::class.java)
|
||||||
|
private val skDestroyListener = mock(SkDestroyListener::class.java)
|
||||||
|
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||||
|
|
||||||
|
private val mDeps = mock(QuicConnectionCloser.Dependencies::class.java).also {
|
||||||
|
doReturn(TEST_NETID).`when`(it).getsockoptInt(any(), eq(SOL_SOCKET), eq(SO_MARK))
|
||||||
|
doReturn(TEST_SOCKET_COOKIE).`when`(it).getSocketCookie(any())
|
||||||
|
doReturn(TEST_SRC_SOCKET_ADDRESS).`when`(it).getsockname(any())
|
||||||
|
doReturn(TEST_DST_SOCKET_ADDRESS).`when`(it).getpeername(any())
|
||||||
|
doReturn(skDestroyListener).`when`(it).makeSkDestroyListener(any(), any())
|
||||||
|
}
|
||||||
|
|
||||||
|
private val nai = mock(NetworkAgentInfo::class.java).also {
|
||||||
|
it.linkProperties = TEST_LP
|
||||||
|
doReturn(TEST_NETWORK).`when`(it).network()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val networkForNetId = SparseArray<NetworkAgentInfo>().apply {
|
||||||
|
put(TEST_NETID, nai)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mQuicConnectionCloser = QuicConnectionCloser(networkForNetId, handler, mDeps)
|
||||||
|
|
||||||
|
private fun InOrder.expectDestroyUdpSocket() = verify(mDeps).destroyUdpSocket(
|
||||||
|
TEST_SRC_SOCKET_ADDRESS,
|
||||||
|
TEST_DST_SOCKET_ADDRESS,
|
||||||
|
TEST_SOCKET_COOKIE
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun InOrder.assertNoDestroyUdpSocket() = verify(mDeps, never()).destroyUdpSocket(
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
anyLong()
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun InOrder.expectSendQuicConnectionClosePayload() =
|
||||||
|
verify(mDeps).sendQuicConnectionClosePayload(
|
||||||
|
TEST_NETWORK,
|
||||||
|
TEST_SRC_SOCKET_ADDRESS,
|
||||||
|
TEST_DST_SOCKET_ADDRESS,
|
||||||
|
TEST_PAYLOAD
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun InOrder.assertNoSendQuicConnectionClosePayload() =
|
||||||
|
verify(mDeps, never()).sendQuicConnectionClosePayload(
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
any(),
|
||||||
|
any()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCloseQuicConnectionByUids() {
|
||||||
|
mQuicConnectionCloser.registerQuicConnectionClosePayload(TEST_UID, pfd, TEST_PAYLOAD)
|
||||||
|
visibleOnHandlerThread(handler) {
|
||||||
|
mQuicConnectionCloser.closeQuicConnectionByUids(setOf(TEST_UID))
|
||||||
|
}
|
||||||
|
|
||||||
|
val inOrder = inOrder(mDeps)
|
||||||
|
inOrder.expectDestroyUdpSocket()
|
||||||
|
inOrder.expectSendQuicConnectionClosePayload()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCloseQuicConnectionByUids_unregisterQuicConnectionCloseInfo() {
|
||||||
|
mQuicConnectionCloser.registerQuicConnectionClosePayload(TEST_UID, pfd, TEST_PAYLOAD)
|
||||||
|
mQuicConnectionCloser.unregisterQuicConnectionClosePayload(pfd)
|
||||||
|
visibleOnHandlerThread(handler) {
|
||||||
|
mQuicConnectionCloser.closeQuicConnectionByUids(setOf(TEST_UID))
|
||||||
|
}
|
||||||
|
|
||||||
|
val inOrder = inOrder(mDeps)
|
||||||
|
inOrder.assertNoDestroyUdpSocket()
|
||||||
|
inOrder.assertNoSendQuicConnectionClosePayload()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCloseQuicConnectionByUids_networkDisconnected() {
|
||||||
|
mQuicConnectionCloser.registerQuicConnectionClosePayload(TEST_UID, pfd, TEST_PAYLOAD)
|
||||||
|
// closeQuicConnectionByUids determines that the network is disconnected by
|
||||||
|
// checking if it's absent from the networkForNetId set.
|
||||||
|
synchronized (networkForNetId) {
|
||||||
|
networkForNetId.clear()
|
||||||
|
}
|
||||||
|
visibleOnHandlerThread(handler) {
|
||||||
|
mQuicConnectionCloser.closeQuicConnectionByUids(setOf(TEST_UID))
|
||||||
|
}
|
||||||
|
|
||||||
|
val inOrder = inOrder(mDeps)
|
||||||
|
inOrder.assertNoDestroyUdpSocket()
|
||||||
|
inOrder.assertNoSendQuicConnectionClosePayload()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCloseQuicConnectionByUids_networkAddressChange() {
|
||||||
|
mQuicConnectionCloser.registerQuicConnectionClosePayload(TEST_UID, pfd, TEST_PAYLOAD)
|
||||||
|
// Update address to different address from TEST_SRC_SOCKET_ADDRESS
|
||||||
|
nai.linkProperties = LinkProperties().apply {
|
||||||
|
interfaceName = TEST_IFACE
|
||||||
|
addLinkAddress(LinkAddress(InetAddresses.parseNumericAddress("2001:db8:1:3::2"), 64))
|
||||||
|
}
|
||||||
|
visibleOnHandlerThread(handler) {
|
||||||
|
mQuicConnectionCloser.closeQuicConnectionByUids(setOf(TEST_UID))
|
||||||
|
}
|
||||||
|
|
||||||
|
val inOrder = inOrder(mDeps)
|
||||||
|
inOrder.assertNoDestroyUdpSocket()
|
||||||
|
inOrder.assertNoSendQuicConnectionClosePayload()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSkDestroyListenerCallback(): Consumer<InetDiagMessage> {
|
||||||
|
val captor = ArgumentCaptor.forClass(Consumer::class.java)
|
||||||
|
as ArgumentCaptor<Consumer<InetDiagMessage>>
|
||||||
|
verify(mDeps).makeSkDestroyListener(captor.capture(), any())
|
||||||
|
return captor.value
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHandleUdpSocketDestroy() {
|
||||||
|
mQuicConnectionCloser.registerQuicConnectionClosePayload(TEST_UID, pfd, TEST_PAYLOAD)
|
||||||
|
|
||||||
|
val inetDiagMessage = InetDiagMessage(StructNlMsgHdr())
|
||||||
|
inetDiagMessage.inetDiagMsg.id = StructInetDiagSockId(
|
||||||
|
TEST_SRC_SOCKET_ADDRESS,
|
||||||
|
TEST_DST_SOCKET_ADDRESS,
|
||||||
|
0 /* ifindex */,
|
||||||
|
TEST_SOCKET_COOKIE
|
||||||
|
)
|
||||||
|
getSkDestroyListenerCallback().accept(inetDiagMessage)
|
||||||
|
|
||||||
|
val inOrder = inOrder(mDeps)
|
||||||
|
// DestroyUdpSocket is not called since the socket is already closed
|
||||||
|
inOrder.assertNoDestroyUdpSocket()
|
||||||
|
inOrder.expectSendQuicConnectionClosePayload()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHandleUdpSocketDestroy_unregisterBeforeSocketClose() {
|
||||||
|
mQuicConnectionCloser.registerQuicConnectionClosePayload(TEST_UID, pfd, TEST_PAYLOAD)
|
||||||
|
mQuicConnectionCloser.unregisterQuicConnectionClosePayload(pfd)
|
||||||
|
|
||||||
|
val inetDiagMessage = InetDiagMessage(StructNlMsgHdr())
|
||||||
|
inetDiagMessage.inetDiagMsg.id = StructInetDiagSockId(
|
||||||
|
TEST_SRC_SOCKET_ADDRESS,
|
||||||
|
TEST_DST_SOCKET_ADDRESS,
|
||||||
|
0 /* ifindex */,
|
||||||
|
TEST_SOCKET_COOKIE
|
||||||
|
)
|
||||||
|
getSkDestroyListenerCallback().accept(inetDiagMessage)
|
||||||
|
|
||||||
|
val inOrder = inOrder(mDeps)
|
||||||
|
inOrder.assertNoDestroyUdpSocket()
|
||||||
|
inOrder.assertNoSendQuicConnectionClosePayload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -155,7 +155,6 @@ class CSDestroySocketTest : CSTest() {
|
||||||
@Test
|
@Test
|
||||||
fun testReplaceFirewallChain() {
|
fun testReplaceFirewallChain() {
|
||||||
val netdEventListener = getRegisteredNetdUnsolicitedEventListener()
|
val netdEventListener = getRegisteredNetdUnsolicitedEventListener()
|
||||||
val inOrder = inOrder(destroySocketsWrapper)
|
|
||||||
|
|
||||||
val cellAgent = Agent(nc = cellNc(), lp = cellLp())
|
val cellAgent = Agent(nc = cellNc(), lp = cellLp())
|
||||||
cellAgent.connect()
|
cellAgent.connect()
|
||||||
|
|
@ -193,8 +192,8 @@ class CSDestroySocketTest : CSTest() {
|
||||||
.`when`(bpfNetMaps).getUidNetworkingBlockedReasons(TEST_UID2)
|
.`when`(bpfNetMaps).getUidNetworkingBlockedReasons(TEST_UID2)
|
||||||
cm.replaceFirewallChain(FIREWALL_CHAIN_BACKGROUND, intArrayOf(TEST_UID2))
|
cm.replaceFirewallChain(FIREWALL_CHAIN_BACKGROUND, intArrayOf(TEST_UID2))
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
inOrder.verify(destroySocketsWrapper, never())
|
verify(destroySocketsWrapper, never()).destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
|
||||||
.destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
|
verify(quicConnectionCloser, never()).closeQuicConnectionByUids(setOf(TEST_UID))
|
||||||
|
|
||||||
netdEventListener.onInterfaceClassActivityChanged(
|
netdEventListener.onInterfaceClassActivityChanged(
|
||||||
true, // isActive
|
true, // isActive
|
||||||
|
|
@ -203,8 +202,8 @@ class CSDestroySocketTest : CSTest() {
|
||||||
TEST_UID
|
TEST_UID
|
||||||
)
|
)
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
inOrder.verify(destroySocketsWrapper)
|
verify(destroySocketsWrapper).destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
|
||||||
.destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
|
verify(quicConnectionCloser).closeQuicConnectionByUids(setOf(TEST_UID))
|
||||||
|
|
||||||
cellAgent.disconnect()
|
cellAgent.disconnect()
|
||||||
}
|
}
|
||||||
|
|
@ -277,8 +276,10 @@ class CSDestroySocketTest : CSTest() {
|
||||||
|
|
||||||
if (expectDestroySockets) {
|
if (expectDestroySockets) {
|
||||||
verify(destroySocketsWrapper).destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
|
verify(destroySocketsWrapper).destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
|
||||||
|
verify(quicConnectionCloser).closeQuicConnectionByUids(setOf(TEST_UID))
|
||||||
} else {
|
} else {
|
||||||
verify(destroySocketsWrapper, never()).destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
|
verify(destroySocketsWrapper, never()).destroyLiveTcpSocketsByOwnerUids(setOf(TEST_UID))
|
||||||
|
verify(quicConnectionCloser, never()).closeQuicConnectionByUids(setOf(TEST_UID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ import android.permission.PermissionManager.PermissionResult
|
||||||
import android.telephony.SubscriptionManager
|
import android.telephony.SubscriptionManager
|
||||||
import android.telephony.TelephonyManager
|
import android.telephony.TelephonyManager
|
||||||
import android.testing.TestableContext
|
import android.testing.TestableContext
|
||||||
|
import android.util.SparseArray
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.android.internal.app.IBatteryStats
|
import com.android.internal.app.IBatteryStats
|
||||||
import com.android.internal.util.test.BroadcastInterceptingContext
|
import com.android.internal.util.test.BroadcastInterceptingContext
|
||||||
|
|
@ -72,9 +73,11 @@ import com.android.server.connectivity.InterfaceTracker
|
||||||
import com.android.server.connectivity.MulticastRoutingCoordinatorService
|
import com.android.server.connectivity.MulticastRoutingCoordinatorService
|
||||||
import com.android.server.connectivity.MultinetworkPolicyTracker
|
import com.android.server.connectivity.MultinetworkPolicyTracker
|
||||||
import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
|
import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
|
||||||
|
import com.android.server.connectivity.NetworkAgentInfo
|
||||||
import com.android.server.connectivity.NetworkRequestStateStatsMetrics
|
import com.android.server.connectivity.NetworkRequestStateStatsMetrics
|
||||||
import com.android.server.connectivity.PermissionMonitor
|
import com.android.server.connectivity.PermissionMonitor
|
||||||
import com.android.server.connectivity.ProxyTracker
|
import com.android.server.connectivity.ProxyTracker
|
||||||
|
import com.android.server.connectivity.QuicConnectionCloser
|
||||||
import com.android.server.connectivity.SatelliteAccessController
|
import com.android.server.connectivity.SatelliteAccessController
|
||||||
import com.android.testutils.visibleOnHandlerThread
|
import com.android.testutils.visibleOnHandlerThread
|
||||||
import com.android.testutils.waitForIdle
|
import com.android.testutils.waitForIdle
|
||||||
|
|
@ -171,6 +174,7 @@ open class CSTest {
|
||||||
it[ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS] = true
|
it[ConnectivityFlags.USE_DECLARED_METHODS_FOR_CALLBACKS] = true
|
||||||
it[ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS] = true
|
it[ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS] = true
|
||||||
it[ConnectivityFlags.QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER] = true
|
it[ConnectivityFlags.QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER] = true
|
||||||
|
it[ConnectivityFlags.CLOSE_QUIC_CONNECTION] = true
|
||||||
}
|
}
|
||||||
fun setFeatureEnabled(flag: String, enabled: Boolean) = enabledFeatures.set(flag, enabled)
|
fun setFeatureEnabled(flag: String, enabled: Boolean) = enabledFeatures.set(flag, enabled)
|
||||||
|
|
||||||
|
|
@ -219,6 +223,7 @@ open class CSTest {
|
||||||
|
|
||||||
val multicastRoutingCoordinatorService = mock<MulticastRoutingCoordinatorService>()
|
val multicastRoutingCoordinatorService = mock<MulticastRoutingCoordinatorService>()
|
||||||
val satelliteAccessController = mock<SatelliteAccessController>()
|
val satelliteAccessController = mock<SatelliteAccessController>()
|
||||||
|
val quicConnectionCloser = mock<QuicConnectionCloser>()
|
||||||
val destroySocketsWrapper = mock<DestroySocketsWrapper>()
|
val destroySocketsWrapper = mock<DestroySocketsWrapper>()
|
||||||
|
|
||||||
val deps = CSDeps()
|
val deps = CSDeps()
|
||||||
|
|
@ -412,6 +417,11 @@ open class CSTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun makeL2capNetworkProvider(context: Context) = null
|
override fun makeL2capNetworkProvider(context: Context) = null
|
||||||
|
|
||||||
|
override fun makeQuicConnectionCloser(
|
||||||
|
networkForNetId: SparseArray<NetworkAgentInfo>,
|
||||||
|
handler: Handler
|
||||||
|
): QuicConnectionCloser = quicConnectionCloser
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class PermDeps : PermissionMonitor.Dependencies() {
|
inner class PermDeps : PermissionMonitor.Dependencies() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue