diff options
author | 2020-02-13 09:16:19 +0900 | |
---|---|---|
committer | 2020-02-19 17:05:00 +0800 | |
commit | db5594d7bdf71e51d032a98cc6afce10bbff13d0 (patch) | |
tree | 612196d46fcac6f8f50683f29544e608af66bfb1 | |
parent | 2d14a4b3e0cf5b170aca0d5c76be07125b385a47 (diff) |
Add tethering client callbacks
The callbacks are fired when the list of connected clients or their IP
addresses / hostname change.
Test: flashed, connected 2 devices, verified callbacks
Test: atest TetheringTests
Bug: 135411507
Change-Id: I96291038cf7b39a67547a5f74fcd7cbedc1ca002
Merged-In: I96291038cf7b39a67547a5f74fcd7cbedc1ca002
12 files changed, 563 insertions, 17 deletions
diff --git a/packages/Tethering/Android.bp b/packages/Tethering/Android.bp index fa3926c6a56e..3111ab701191 100644 --- a/packages/Tethering/Android.bp +++ b/packages/Tethering/Android.bp @@ -17,6 +17,7 @@ java_defaults { name: "TetheringAndroidLibraryDefaults", // TODO (b/146757305): change to module API once available + // TODO (b/148190005): change to module-libs-api-stubs-current once it is ready. sdk_version: "core_platform", srcs: [ "src/**/*.java", @@ -34,7 +35,12 @@ java_defaults { "net-utils-framework-common", ], libs: [ + // Order matters: framework-tethering needs to be before the system stubs, otherwise + // hidden fields in the framework-tethering classes (which are also used to generate stubs) + // will not be found. "framework-tethering", + "android_system_stubs_current", + "framework-res", "unsupportedappusage", "android_system_stubs_current", "framework-res", @@ -86,6 +92,7 @@ cc_library { java_defaults { name: "TetheringAppDefaults", // TODO (b/146757305): change to module API once available + // TODO (b/148190005): change to module-libs-api-stubs-current once it is ready. sdk_version: "core_platform", privileged: true, // Build system doesn't track transitive dependeicies for jni_libs, list all the dependencies @@ -99,6 +106,9 @@ java_defaults { "res", ], libs: [ + // Order matters: framework-tethering needs to be before the system stubs, otherwise + // hidden fields in the framework-tethering classes (which are also used to generate stubs) + // will not be found. "framework-tethering", "android_system_stubs_current", "framework-res", diff --git a/packages/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl b/packages/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl index 5febe73288bf..8be79645bde3 100644 --- a/packages/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl +++ b/packages/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl @@ -1,16 +1,16 @@ -/** - * Copyright (c) 2019, The Android Open Source Project +/* + * Copyright (C) 2020 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 + * 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 perNmissions and + * See the License for the specific language governing permissions and * limitations under the License. */ package android.net; diff --git a/packages/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl b/packages/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl index 28a810dbfac3..a55419383380 100644 --- a/packages/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl +++ b/packages/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl @@ -17,6 +17,7 @@ package android.net; import android.net.Network; +import android.net.TetheredClient; import android.net.TetheringConfigurationParcel; import android.net.TetheringCallbackStartedParcel; import android.net.TetherStatesParcel; @@ -33,4 +34,5 @@ oneway interface ITetheringEventCallback void onUpstreamChanged(in Network network); void onConfigurationChanged(in TetheringConfigurationParcel config); void onTetherStatesChanged(in TetherStatesParcel states); + void onTetherClientsChanged(in List<TetheredClient> clients); } diff --git a/packages/Tethering/common/TetheringLib/src/android/net/TetheredClient.java b/packages/Tethering/common/TetheringLib/src/android/net/TetheredClient.java index 779aa3b6e965..8b8b9e57a500 100644 --- a/packages/Tethering/common/TetheringLib/src/android/net/TetheredClient.java +++ b/packages/Tethering/common/TetheringLib/src/android/net/TetheredClient.java @@ -191,6 +191,15 @@ public final class TetheredClient implements Parcelable { return new AddressInfo[size]; } }; + + @NonNull + @Override + public String toString() { + return "AddressInfo {" + + mAddress + + (mHostname != null ? ", hostname " + mHostname : "") + + "}"; + } } @Override @@ -212,4 +221,13 @@ public final class TetheredClient implements Parcelable { return new TetheredClient[size]; } }; + + @NonNull + @Override + public String toString() { + return "TetheredClient {hwAddr " + mMacAddress + + ", addresses " + mAddresses + + ", tetheringType " + mTetheringType + + "}"; + } } diff --git a/packages/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl b/packages/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl index 14ee2d3e5d38..c064aa4d9a61 100644 --- a/packages/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl +++ b/packages/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl @@ -17,6 +17,7 @@ package android.net; import android.net.Network; +import android.net.TetheredClient; import android.net.TetheringConfigurationParcel; import android.net.TetherStatesParcel; @@ -29,4 +30,5 @@ parcelable TetheringCallbackStartedParcel { Network upstreamNetwork; TetheringConfigurationParcel config; TetherStatesParcel states; + List<TetheredClient> tetheredClients; }
\ No newline at end of file diff --git a/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java index 6a9f010449c4..bfa962a18c9a 100644 --- a/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java +++ b/packages/Tethering/common/TetheringLib/src/android/net/TetheringManager.java @@ -375,6 +375,9 @@ public class TetheringManager { mTetherStatesParcel = states; } + @Override + public void onTetherClientsChanged(List<TetheredClient> clients) { } + public void waitForStarted() { mWaitForCallback.block(DEFAULT_TIMEOUT_MS); throwIfPermissionFailure(mError); @@ -921,6 +924,7 @@ public class TetheringManager { sendRegexpsChanged(parcel.config); maybeSendTetherableIfacesChangedCallback(parcel.states); maybeSendTetheredIfacesChangedCallback(parcel.states); + callback.onClientsChanged(parcel.tetheredClients); }); } @@ -951,6 +955,11 @@ public class TetheringManager { maybeSendTetheredIfacesChangedCallback(states); }); } + + @Override + public void onTetherClientsChanged(final List<TetheredClient> clients) { + executor.execute(() -> callback.onClientsChanged(clients)); + } }; getConnector(c -> c.registerTetheringEventCallback(remoteCallback, callerPkg)); mTetheringEventCallbacks.put(callback, remoteCallback); diff --git a/packages/Tethering/src/android/net/ip/IpServer.java b/packages/Tethering/src/android/net/ip/IpServer.java index 2653b6d23ac9..b4d49c02b085 100644 --- a/packages/Tethering/src/android/net/ip/IpServer.java +++ b/packages/Tethering/src/android/net/ip/IpServer.java @@ -19,6 +19,7 @@ package android.net.ip; import static android.net.InetAddresses.parseNumericAddress; import static android.net.RouteInfo.RTN_UNICAST; import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; +import static android.net.shared.Inet4AddressUtils.intToInet4AddressHTH; import static android.net.util.NetworkConstants.FF; import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH; import static android.net.util.NetworkConstants.asByte; @@ -29,11 +30,15 @@ import android.net.INetworkStackStatusCallback; import android.net.IpPrefix; import android.net.LinkAddress; import android.net.LinkProperties; +import android.net.MacAddress; import android.net.RouteInfo; +import android.net.TetheredClient; import android.net.TetheringManager; +import android.net.dhcp.DhcpLeaseParcelable; import android.net.dhcp.DhcpServerCallbacks; import android.net.dhcp.DhcpServingParamsParcel; import android.net.dhcp.DhcpServingParamsParcelExt; +import android.net.dhcp.IDhcpLeaseCallbacks; import android.net.dhcp.IDhcpServer; import android.net.ip.RouterAdvertisementDaemon.RaParams; import android.net.shared.NetdUtils; @@ -48,6 +53,8 @@ import android.os.ServiceSpecificException; import android.util.Log; import android.util.SparseArray; +import androidx.annotation.NonNull; + import com.android.internal.util.MessageUtils; import com.android.internal.util.State; import com.android.internal.util.StateMachine; @@ -57,7 +64,10 @@ import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Random; import java.util.Set; @@ -130,6 +140,11 @@ public class IpServer extends StateMachine { * @param newLp the new LinkProperties to report */ public void updateLinkProperties(IpServer who, LinkProperties newLp) { } + + /** + * Notify that the DHCP leases changed in one of the IpServers. + */ + public void dhcpLeasesChanged() { } } /** Capture IpServer dependencies, for injection. */ @@ -205,6 +220,8 @@ public class IpServer extends StateMachine { private IDhcpServer mDhcpServer; private RaParams mLastRaParams; private LinkAddress mIpv4Address; + @NonNull + private List<TetheredClient> mDhcpLeases = Collections.emptyList(); public IpServer( String ifaceName, Looper looper, int interfaceType, SharedLog log, @@ -262,6 +279,14 @@ public class IpServer extends StateMachine { return new LinkProperties(mLinkProperties); } + /** + * Get the latest list of DHCP leases that was reported. Must be called on the IpServer looper + * thread. + */ + public List<TetheredClient> getAllLeases() { + return Collections.unmodifiableList(mDhcpLeases); + } + /** Stop this IpServer. After this is called this IpServer should not be used any more. */ public void stop() { sendMessage(CMD_INTERFACE_DOWN); @@ -334,7 +359,7 @@ public class IpServer extends StateMachine { mDhcpServer = server; try { - mDhcpServer.start(new OnHandlerStatusCallback() { + mDhcpServer.startWithCallbacks(new OnHandlerStatusCallback() { @Override public void callback(int startStatusCode) { if (startStatusCode != STATUS_SUCCESS) { @@ -342,7 +367,7 @@ public class IpServer extends StateMachine { handleError(); } } - }); + }, new DhcpLeaseCallback()); } catch (RemoteException e) { throw new IllegalStateException(e); } @@ -355,6 +380,48 @@ public class IpServer extends StateMachine { } } + private class DhcpLeaseCallback extends IDhcpLeaseCallbacks.Stub { + @Override + public void onLeasesChanged(List<DhcpLeaseParcelable> leaseParcelables) { + final ArrayList<TetheredClient> leases = new ArrayList<>(); + for (DhcpLeaseParcelable lease : leaseParcelables) { + final LinkAddress address = new LinkAddress( + intToInet4AddressHTH(lease.netAddr), lease.prefixLength); + + final MacAddress macAddress; + try { + macAddress = MacAddress.fromBytes(lease.hwAddr); + } catch (IllegalArgumentException e) { + Log.wtf(TAG, "Invalid address received from DhcpServer: " + + Arrays.toString(lease.hwAddr)); + return; + } + + final TetheredClient.AddressInfo addressInfo = new TetheredClient.AddressInfo( + address, lease.hostname, lease.expTime); + leases.add(new TetheredClient( + macAddress, + Collections.singletonList(addressInfo), + mInterfaceType)); + } + + getHandler().post(() -> { + mDhcpLeases = leases; + mCallback.dhcpLeasesChanged(); + }); + } + + @Override + public int getInterfaceVersion() { + return this.VERSION; + } + + @Override + public String getInterfaceHash() throws RemoteException { + return this.HASH; + } + } + private boolean startDhcp(Inet4Address addr, int prefixLen) { if (mUsingLegacyDhcp) { return true; @@ -388,6 +455,8 @@ public class IpServer extends StateMachine { mLastError = TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR; // Not much more we can do here } + mDhcpLeases.clear(); + getHandler().post(mCallback::dhcpLeasesChanged); } }); mDhcpServer = null; diff --git a/packages/Tethering/src/com/android/server/connectivity/tethering/ConnectedClientsTracker.java b/packages/Tethering/src/com/android/server/connectivity/tethering/ConnectedClientsTracker.java new file mode 100644 index 000000000000..cdd1a5d97823 --- /dev/null +++ b/packages/Tethering/src/com/android/server/connectivity/tethering/ConnectedClientsTracker.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2020 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.tethering; + +import static android.net.TetheringManager.TETHERING_WIFI; + +import android.net.MacAddress; +import android.net.TetheredClient; +import android.net.TetheredClient.AddressInfo; +import android.net.ip.IpServer; +import android.net.wifi.WifiClient; +import android.os.SystemClock; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Tracker for clients connected to downstreams. + * + * <p>This class is not thread safe, it is intended to be used only from the tethering handler + * thread. + */ +public class ConnectedClientsTracker { + private final Clock mClock; + + @NonNull + private List<WifiClient> mLastWifiClients = Collections.emptyList(); + @NonNull + private List<TetheredClient> mLastTetheredClients = Collections.emptyList(); + + @VisibleForTesting + static class Clock { + public long elapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + } + + public ConnectedClientsTracker() { + this(new Clock()); + } + + @VisibleForTesting + ConnectedClientsTracker(Clock clock) { + mClock = clock; + } + + /** + * Update the tracker with new connected clients. + * + * <p>The new list can be obtained through {@link #getLastTetheredClients()}. + * @param ipServers The IpServers used to assign addresses to clients. + * @param wifiClients The list of L2-connected WiFi clients. Null for no change since last + * update. + * @return True if the list of clients changed since the last calculation. + */ + public boolean updateConnectedClients( + Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients) { + final long now = mClock.elapsedRealtime(); + + if (wifiClients != null) { + mLastWifiClients = wifiClients; + } + final Set<MacAddress> wifiClientMacs = getClientMacs(mLastWifiClients); + + // Build the list of non-expired leases from all IpServers, grouped by mac address + final Map<MacAddress, TetheredClient> clientsMap = new HashMap<>(); + for (IpServer server : ipServers) { + for (TetheredClient client : server.getAllLeases()) { + if (client.getTetheringType() == TETHERING_WIFI + && !wifiClientMacs.contains(client.getMacAddress())) { + // Skip leases of WiFi clients that are not (or no longer) L2-connected + continue; + } + final TetheredClient prunedClient = pruneExpired(client, now); + if (prunedClient == null) continue; // All addresses expired + + addLease(clientsMap, prunedClient); + } + } + + // TODO: add IPv6 addresses from netlink + + // Add connected WiFi clients that do not have any known address + for (MacAddress client : wifiClientMacs) { + if (clientsMap.containsKey(client)) continue; + clientsMap.put(client, new TetheredClient( + client, Collections.emptyList() /* addresses */, TETHERING_WIFI)); + } + + final HashSet<TetheredClient> clients = new HashSet<>(clientsMap.values()); + final boolean clientsChanged = clients.size() != mLastTetheredClients.size() + || !clients.containsAll(mLastTetheredClients); + mLastTetheredClients = Collections.unmodifiableList(new ArrayList<>(clients)); + return clientsChanged; + } + + private static void addLease(Map<MacAddress, TetheredClient> clientsMap, TetheredClient lease) { + final TetheredClient aggregateClient = clientsMap.getOrDefault( + lease.getMacAddress(), lease); + if (aggregateClient == lease) { + // This is the first lease with this mac address + clientsMap.put(lease.getMacAddress(), lease); + return; + } + + // Only add the address info; this assumes that the tethering type is the same when the mac + // address is the same. If a client is connected through different tethering types with the + // same mac address, connected clients callbacks will report all of its addresses under only + // one of these tethering types. This keeps the API simple considering that such a scenario + // would really be a rare edge case. + clientsMap.put(lease.getMacAddress(), aggregateClient.addAddresses(lease)); + } + + /** + * Get the last list of tethered clients, as calculated in {@link #updateConnectedClients}. + * + * <p>The returned list is immutable. + */ + @NonNull + public List<TetheredClient> getLastTetheredClients() { + return mLastTetheredClients; + } + + private static boolean hasExpiredAddress(List<AddressInfo> addresses, long now) { + for (AddressInfo info : addresses) { + if (info.getExpirationTime() <= now) { + return true; + } + } + return false; + } + + @Nullable + private static TetheredClient pruneExpired(TetheredClient client, long now) { + final List<AddressInfo> addresses = client.getAddresses(); + if (addresses.size() == 0) return null; + if (!hasExpiredAddress(addresses, now)) return client; + + final ArrayList<AddressInfo> newAddrs = new ArrayList<>(addresses.size() - 1); + for (AddressInfo info : addresses) { + if (info.getExpirationTime() > now) { + newAddrs.add(info); + } + } + + if (newAddrs.size() == 0) { + return null; + } + return new TetheredClient(client.getMacAddress(), newAddrs, client.getTetheringType()); + } + + @NonNull + private static Set<MacAddress> getClientMacs(@NonNull List<WifiClient> clients) { + final Set<MacAddress> macs = new HashSet<>(clients.size()); + for (WifiClient c : clients) { + macs.add(c.getMacAddress()); + } + return macs; + } +} diff --git a/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java b/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java index 1edbbf8af1ff..e462d36cc7a8 100644 --- a/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java +++ b/packages/Tethering/src/com/android/server/connectivity/tethering/Tethering.java @@ -24,6 +24,7 @@ import static android.hardware.usb.UsbManager.USB_FUNCTION_RNDIS; import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED; import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO; +import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED; import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY; import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER; @@ -79,6 +80,7 @@ import android.net.LinkProperties; import android.net.Network; import android.net.NetworkInfo; import android.net.TetherStatesParcel; +import android.net.TetheredClient; import android.net.TetheringCallbackStartedParcel; import android.net.TetheringConfigurationParcel; import android.net.TetheringRequestParcel; @@ -89,6 +91,7 @@ import android.net.util.InterfaceSet; import android.net.util.PrefixUtils; import android.net.util.SharedLog; import android.net.util.VersionedBroadcastListener; +import android.net.wifi.WifiClient; import android.net.wifi.WifiManager; import android.net.wifi.p2p.WifiP2pGroup; import android.net.wifi.p2p.WifiP2pInfo; @@ -128,8 +131,10 @@ import java.net.InetAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; +import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; @@ -145,6 +150,10 @@ public class Tethering { private static final boolean DBG = false; private static final boolean VDBG = false; + // TODO: add the below permissions to @SystemApi + private static final String PERMISSION_NETWORK_SETTINGS = "android.permission.NETWORK_SETTINGS"; + private static final String PERMISSION_NETWORK_STACK = "android.permission.NETWORK_STACK"; + private static final Class[] sMessageClasses = { Tethering.class, TetherMasterSM.class, IpServer.class }; @@ -176,6 +185,17 @@ public class Tethering { } } + /** + * Cookie added when registering {@link android.net.TetheringManager.TetheringEventCallback}. + */ + private static class CallbackCookie { + public final boolean hasListClientsPermission; + + private CallbackCookie(boolean hasListClientsPermission) { + this.hasListClientsPermission = hasListClientsPermission; + } + } + private final SharedLog mLog = new SharedLog(TAG); private final RemoteCallbackList<ITetheringEventCallback> mTetheringEventCallbacks = new RemoteCallbackList<>(); @@ -191,7 +211,8 @@ public class Tethering { private final UpstreamNetworkMonitor mUpstreamNetworkMonitor; // TODO: Figure out how to merge this and other downstream-tracking objects // into a single coherent structure. - private final HashSet<IpServer> mForwardedDownstreams; + // Use LinkedHashSet for predictable ordering order for ConnectedClientsTracker. + private final LinkedHashSet<IpServer> mForwardedDownstreams; private final VersionedBroadcastListener mCarrierConfigChange; private final TetheringDependencies mDeps; private final EntitlementManager mEntitlementMgr; @@ -200,6 +221,7 @@ public class Tethering { private final NetdCallback mNetdCallback; private final UserRestrictionActionListener mTetheringRestriction; private final ActiveDataSubIdListener mActiveDataSubIdListener; + private final ConnectedClientsTracker mConnectedClientsTracker; private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID; // All the usage of mTetheringEventCallback should run in the same thread. private ITetheringEventCallback mTetheringEventCallback = null; @@ -234,6 +256,7 @@ public class Tethering { mPublicSync = new Object(); mTetherStates = new ArrayMap<>(); + mConnectedClientsTracker = new ConnectedClientsTracker(); mTetherMasterSM = new TetherMasterSM("TetherMaster", mLooper, deps); mTetherMasterSM.start(); @@ -246,7 +269,7 @@ public class Tethering { statsManager, mLog); mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMasterSM, mLog, TetherMasterSM.EVENT_UPSTREAM_CALLBACK); - mForwardedDownstreams = new HashSet<>(); + mForwardedDownstreams = new LinkedHashSet<>(); IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_CARRIER_CONFIG_CHANGED); @@ -291,6 +314,9 @@ public class Tethering { startStateMachineUpdaters(mHandler); startTrackDefaultNetwork(); + getWifiManager().registerSoftApCallback( + mHandler::post /* executor */, + new TetheringSoftApCallback()); } private void startStateMachineUpdaters(Handler handler) { @@ -385,6 +411,24 @@ public class Tethering { } } + private class TetheringSoftApCallback implements WifiManager.SoftApCallback { + // TODO: Remove onStateChanged override when this method has default on + // WifiManager#SoftApCallback interface. + // Wifi listener for state change of the soft AP + @Override + public void onStateChanged(final int state, final int failureReason) { + // Nothing + } + + // Called by wifi when the number of soft AP clients changed. + @Override + public void onConnectedClientsChanged(final List<WifiClient> clients) { + if (mConnectedClientsTracker.updateConnectedClients(mForwardedDownstreams, clients)) { + reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients()); + } + } + } + void interfaceStatusChanged(String iface, boolean up) { // Never called directly: only called from interfaceLinkStateChanged. // See NetlinkHandler.cpp: notifyInterfaceChanged. @@ -1938,14 +1982,21 @@ public class Tethering { /** Register tethering event callback */ void registerTetheringEventCallback(ITetheringEventCallback callback) { + final boolean hasListPermission = + hasCallingPermission(PERMISSION_NETWORK_SETTINGS) + || hasCallingPermission(PERMISSION_MAINLINE_NETWORK_STACK) + || hasCallingPermission(PERMISSION_NETWORK_STACK); mHandler.post(() -> { - mTetheringEventCallbacks.register(callback); + mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission)); final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel(); parcel.tetheringSupported = mDeps.isTetheringSupported(); parcel.upstreamNetwork = mTetherUpstream; parcel.config = mConfig.toStableParcelable(); parcel.states = mTetherStatesParcel != null ? mTetherStatesParcel : emptyTetherStatesParcel(); + parcel.tetheredClients = hasListPermission + ? mConnectedClientsTracker.getLastTetheredClients() + : Collections.emptyList(); try { callback.onCallbackStarted(parcel); } catch (RemoteException e) { @@ -1965,6 +2016,10 @@ public class Tethering { return parcel; } + private boolean hasCallingPermission(@NonNull String permission) { + return mContext.checkCallingPermission(permission) == PERMISSION_GRANTED; + } + /** Unregister tethering event callback */ void unregisterTetheringEventCallback(ITetheringEventCallback callback) { mHandler.post(() -> { @@ -2018,6 +2073,24 @@ public class Tethering { } } + private void reportTetherClientsChanged(List<TetheredClient> clients) { + final int length = mTetheringEventCallbacks.beginBroadcast(); + try { + for (int i = 0; i < length; i++) { + try { + final CallbackCookie cookie = + (CallbackCookie) mTetheringEventCallbacks.getBroadcastCookie(i); + if (!cookie.hasListClientsPermission) continue; + mTetheringEventCallbacks.getBroadcastItem(i).onTetherClientsChanged(clients); + } catch (RemoteException e) { + // Not really very much to do here. + } + } + } finally { + mTetheringEventCallbacks.finishBroadcast(); + } + } + void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) { // Binder.java closes the resource for us. @SuppressWarnings("resource") @@ -2109,6 +2182,14 @@ public class Tethering { public void updateLinkProperties(IpServer who, LinkProperties newLp) { notifyLinkPropertiesChanged(who, newLp); } + + @Override + public void dhcpLeasesChanged() { + if (mConnectedClientsTracker.updateConnectedClients( + mForwardedDownstreams, null /* wifiClients */)) { + reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients()); + } + } }; } diff --git a/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java index f29ad780b92c..acedfab3b7ab 100644 --- a/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java +++ b/packages/Tethering/tests/unit/src/android/net/ip/IpServerTest.java @@ -469,7 +469,8 @@ public class IpServerTest { private void assertDhcpStarted(IpPrefix expectedPrefix) throws Exception { verify(mDependencies, times(1)).makeDhcpServer(eq(IFACE_NAME), any(), any()); - verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).start(any()); + verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); final DhcpServingParamsParcel params = mDhcpParamsCaptor.getValue(); // Last address byte is random assertTrue(expectedPrefix.contains(intToInet4AddressHTH(params.serverAddr))); diff --git a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/ConnectedClientsTrackerTest.kt b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/ConnectedClientsTrackerTest.kt new file mode 100644 index 000000000000..56f3e21cbffe --- /dev/null +++ b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/ConnectedClientsTrackerTest.kt @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2020 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.tethering + +import android.net.LinkAddress +import android.net.MacAddress +import android.net.TetheredClient +import android.net.TetheredClient.AddressInfo +import android.net.TetheringManager.TETHERING_USB +import android.net.TetheringManager.TETHERING_WIFI +import android.net.ip.IpServer +import android.net.wifi.WifiClient +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +@SmallTest +class ConnectedClientsTrackerTest { + + private val server1 = mock(IpServer::class.java) + private val server2 = mock(IpServer::class.java) + private val servers = listOf(server1, server2) + + private val clock = TestClock(1324L) + + private val client1Addr = MacAddress.fromString("01:23:45:67:89:0A") + private val client1 = TetheredClient(client1Addr, listOf( + AddressInfo(LinkAddress("192.168.43.44/32"), null /* hostname */, clock.time + 20)), + TETHERING_WIFI) + private val wifiClient1 = makeWifiClient(client1Addr) + private val client2Addr = MacAddress.fromString("02:34:56:78:90:AB") + private val client2Exp30AddrInfo = AddressInfo( + LinkAddress("192.168.43.45/32"), "my_hostname", clock.time + 30) + private val client2 = TetheredClient(client2Addr, listOf( + client2Exp30AddrInfo, + AddressInfo(LinkAddress("2001:db8:12::34/72"), "other_hostname", clock.time + 10)), + TETHERING_WIFI) + private val wifiClient2 = makeWifiClient(client2Addr) + private val client3Addr = MacAddress.fromString("03:45:67:89:0A:BC") + private val client3 = TetheredClient(client3Addr, + listOf(AddressInfo(LinkAddress("2001:db8:34::34/72"), "other_other_hostname", + clock.time + 10)), + TETHERING_USB) + + @Test + fun testUpdateConnectedClients() { + doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases + doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases + + val tracker = ConnectedClientsTracker(clock) + assertFalse(tracker.updateConnectedClients(servers, null)) + + // Obtain a lease for client 1 + doReturn(listOf(client1)).`when`(server1).allLeases + assertSameClients(listOf(client1), assertNewClients(tracker, servers, listOf(wifiClient1))) + + // Client 2 L2-connected, no lease yet + val client2WithoutAddr = TetheredClient(client2Addr, emptyList(), TETHERING_WIFI) + assertSameClients(listOf(client1, client2WithoutAddr), + assertNewClients(tracker, servers, listOf(wifiClient1, wifiClient2))) + + // Client 2 lease obtained + doReturn(listOf(client1, client2)).`when`(server1).allLeases + assertSameClients(listOf(client1, client2), assertNewClients(tracker, servers, null)) + + // Client 3 lease obtained + doReturn(listOf(client3)).`when`(server2).allLeases + assertSameClients(listOf(client1, client2, client3), + assertNewClients(tracker, servers, null)) + + // Client 2 L2-disconnected + assertSameClients(listOf(client1, client3), + assertNewClients(tracker, servers, listOf(wifiClient1))) + + // Client 1 L2-disconnected + assertSameClients(listOf(client3), assertNewClients(tracker, servers, emptyList())) + + // Client 1 comes back + assertSameClients(listOf(client1, client3), + assertNewClients(tracker, servers, listOf(wifiClient1))) + + // Leases lost, client 1 still L2-connected + doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases + doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases + assertSameClients(listOf(TetheredClient(client1Addr, emptyList(), TETHERING_WIFI)), + assertNewClients(tracker, servers, null)) + } + + @Test + fun testUpdateConnectedClients_LeaseExpiration() { + val tracker = ConnectedClientsTracker(clock) + doReturn(listOf(client1, client2)).`when`(server1).allLeases + doReturn(listOf(client3)).`when`(server2).allLeases + assertSameClients(listOf(client1, client2, client3), assertNewClients( + tracker, servers, listOf(wifiClient1, wifiClient2))) + + clock.time += 20 + // Client 3 has no remaining lease: removed + val expectedClients = listOf( + // Client 1 has no remaining lease but is L2-connected + TetheredClient(client1Addr, emptyList(), TETHERING_WIFI), + // Client 2 has some expired leases + TetheredClient( + client2Addr, + // Only the "t + 30" address is left, the "t + 10" address expired + listOf(client2Exp30AddrInfo), + TETHERING_WIFI)) + assertSameClients(expectedClients, assertNewClients(tracker, servers, null)) + } + + private fun assertNewClients( + tracker: ConnectedClientsTracker, + ipServers: Iterable<IpServer>, + wifiClients: List<WifiClient>? + ): List<TetheredClient> { + assertTrue(tracker.updateConnectedClients(ipServers, wifiClients)) + return tracker.lastTetheredClients + } + + private fun assertSameClients(expected: List<TetheredClient>, actual: List<TetheredClient>) { + val expectedSet = HashSet(expected) + assertEquals(expected.size, expectedSet.size) + assertEquals(expectedSet, HashSet(actual)) + } + + private fun makeWifiClient(macAddr: MacAddress): WifiClient { + // Use a mock WifiClient as the constructor is not part of the WiFi module exported API. + return mock(WifiClient::class.java).apply { doReturn(macAddr).`when`(this).macAddress } + } + + private class TestClock(var time: Long) : ConnectedClientsTracker.Clock() { + override fun elapsedRealtime(): Long { + return time + } + } +}
\ No newline at end of file diff --git a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java index a9178494806c..150b76ed2ab3 100644 --- a/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java +++ b/packages/Tethering/tests/unit/src/com/android/server/connectivity/tethering/TetheringTest.java @@ -88,6 +88,7 @@ import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.RouteInfo; import android.net.TetherStatesParcel; +import android.net.TetheredClient; import android.net.TetheringCallbackStartedParcel; import android.net.TetheringConfigurationParcel; import android.net.TetheringRequestParcel; @@ -142,6 +143,7 @@ import java.net.Inet6Address; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Vector; @RunWith(AndroidJUnit4.class) @@ -470,6 +472,7 @@ public class TetheringTest { ArgumentCaptor.forClass(PhoneStateListener.class); verify(mTelephonyManager).listen(phoneListenerCaptor.capture(), eq(PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE)); + verify(mWifiManager).registerSoftApCallback(any(), any()); mPhoneStateListener = phoneListenerCaptor.getValue(); } @@ -728,7 +731,8 @@ public class TetheringTest { sendIPv6TetherUpdates(upstreamState); verify(mRouterAdvertisementDaemon, never()).buildNewRa(any(), notNull()); - verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any()); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); } @Test @@ -764,7 +768,8 @@ public class TetheringTest { verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); verify(mRouterAdvertisementDaemon, times(1)).start(); - verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any()); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); sendIPv6TetherUpdates(upstreamState); verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull()); @@ -778,7 +783,8 @@ public class TetheringTest { verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME); verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); - verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any()); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME); verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); @@ -794,7 +800,8 @@ public class TetheringTest { runUsbTethering(upstreamState); verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); - verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any()); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); // Then 464xlat comes up @@ -817,7 +824,8 @@ public class TetheringTest { verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); // DHCP not restarted on downstream (still times(1)) - verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any()); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); } @Test @@ -847,7 +855,8 @@ public class TetheringTest { public void workingNcmTethering() throws Exception { runNcmTethering(); - verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).start(any()); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); } @Test @@ -1171,6 +1180,11 @@ public class TetheringTest { } @Override + public void onTetherClientsChanged(List<TetheredClient> clients) { + // TODO: check this + } + + @Override public void onCallbackStarted(TetheringCallbackStartedParcel parcel) { mActualUpstreams.add(parcel.upstreamNetwork); mTetheringConfigs.add(parcel.config); |