Change tethering file structure to respect its package name

Bug: 145099347
Test: atest TetheringTests
      atest CtsTetheringTest
Change-Id: I7401c550fbafd17a5ed2d925b5d0e17e358af467
This commit is contained in:
markchien 2020-04-12 21:41:29 +08:00 • committed by Mark Chien
commit d995566700
28 changed files with 41 additions and 48 deletions

View file

@ -43,7 +43,7 @@
android:process="com.android.networkstack.process"
android:extractNativeLibs="false"
android:persistent="true">
<service android:name="com.android.server.connectivity.tethering.TetheringService"
<service android:name="com.android.networkstack.tethering.TetheringService"
android:permission="android.permission.MAINLINE_NETWORK_STACK"
android:exported="true">
<intent-filter>

View file

@ -22,7 +22,7 @@
android:process="system">
<uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
<application>
<service android:name="com.android.server.connectivity.tethering.TetheringService"
<service android:name="com.android.networkstack.tethering.TetheringService"
android:process="system"
android:permission="android.permission.MAINLINE_NETWORK_STACK"
android:exported="true">

View file

@ -1,5 +1,5 @@
# Keep class's integer static field for MessageUtils to parsing their name.
-keep class com.android.server.connectivity.tethering.Tethering$TetherMasterSM {
-keep class com.android.networkstack.tethering.Tethering$TetherMasterSM {
static final int CMD_*;
static final int EVENT_*;
}

View file

@ -87,7 +87,7 @@
TYPE_MOBILE_HIPRI is appended.
For other changes applied to this list, now and in the future, see
com.android.server.connectivity.tethering.TetheringConfiguration.
com.android.networkstack.tethering.TetheringConfiguration.
Note also: the order of this is important. The first upstream type
for which a satisfying network exists is used.

View file

@ -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.networkstack.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;
}
}

View file

@ -0,0 +1,674 @@
/*
* Copyright (C) 2018 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.networkstack.tethering;
import static android.net.TetheringConstants.EXTRA_ADD_TETHER_TYPE;
import static android.net.TetheringConstants.EXTRA_PROVISION_CALLBACK;
import static android.net.TetheringConstants.EXTRA_RUN_PROVISION;
import static android.net.TetheringManager.TETHERING_BLUETOOTH;
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringManager.TETHERING_INVALID;
import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.util.SharedLog;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
import android.os.PersistableBundle;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.provider.Settings;
import android.telephony.CarrierConfigManager;
import android.util.ArraySet;
import android.util.SparseIntArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.StateMachine;
import java.io.PrintWriter;
/**
* Re-check tethering provisioning for enabled downstream tether types.
* Reference TetheringManager.TETHERING_{@code *} for each tether type.
*
* All methods of this class must be accessed from the thread of tethering
* state machine.
* @hide
*/
public class EntitlementManager {
private static final String TAG = EntitlementManager.class.getSimpleName();
private static final boolean DBG = false;
@VisibleForTesting
protected static final String DISABLE_PROVISIONING_SYSPROP_KEY = "net.tethering.noprovisioning";
private static final String ACTION_PROVISIONING_ALARM =
"com.android.networkstack.tethering.PROVISIONING_RECHECK_ALARM";
private static final String EXTRA_SUBID = "subId";
private final ComponentName mSilentProvisioningService;
private static final int MS_PER_HOUR = 60 * 60 * 1000;
private static final int EVENT_START_PROVISIONING = 0;
private static final int EVENT_STOP_PROVISIONING = 1;
private static final int EVENT_UPSTREAM_CHANGED = 2;
private static final int EVENT_MAYBE_RUN_PROVISIONING = 3;
private static final int EVENT_GET_ENTITLEMENT_VALUE = 4;
// The ArraySet contains enabled downstream types, ex:
// {@link TetheringManager.TETHERING_WIFI}
// {@link TetheringManager.TETHERING_USB}
// {@link TetheringManager.TETHERING_BLUETOOTH}
private final ArraySet<Integer> mCurrentTethers;
private final Context mContext;
private final int mPermissionChangeMessageCode;
private final SharedLog mLog;
private final SparseIntArray mEntitlementCacheValue;
private final EntitlementHandler mHandler;
private final StateMachine mTetherMasterSM;
// Key: TetheringManager.TETHERING_*(downstream).
// Value: TetheringManager.TETHER_ERROR_{NO_ERROR or PROVISION_FAILED}(provisioning result).
private final SparseIntArray mCellularPermitted;
private PendingIntent mProvisioningRecheckAlarm;
private boolean mCellularUpstreamPermitted = true;
private boolean mUsingCellularAsUpstream = false;
private boolean mNeedReRunProvisioningUi = false;
private OnUiEntitlementFailedListener mListener;
private TetheringConfigurationFetcher mFetcher;
public EntitlementManager(Context ctx, StateMachine tetherMasterSM, SharedLog log,
int permissionChangeMessageCode) {
mContext = ctx;
mLog = log.forSubComponent(TAG);
mCurrentTethers = new ArraySet<Integer>();
mCellularPermitted = new SparseIntArray();
mEntitlementCacheValue = new SparseIntArray();
mTetherMasterSM = tetherMasterSM;
mPermissionChangeMessageCode = permissionChangeMessageCode;
final Handler masterHandler = tetherMasterSM.getHandler();
// Create entitlement's own handler which is associated with TetherMaster thread
// let all entitlement processes run in the same thread.
mHandler = new EntitlementHandler(masterHandler.getLooper());
mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_PROVISIONING_ALARM),
null, mHandler);
mSilentProvisioningService = ComponentName.unflattenFromString(
mContext.getResources().getString(R.string.config_wifi_tether_enable));
}
public void setOnUiEntitlementFailedListener(final OnUiEntitlementFailedListener listener) {
mListener = listener;
}
/** Callback fired when UI entitlement failed. */
public interface OnUiEntitlementFailedListener {
/**
* Ui entitlement check fails in |downstream|.
*
* @param downstream tethering type from TetheringManager.TETHERING_{@code *}.
*/
void onUiEntitlementFailed(int downstream);
}
public void setTetheringConfigurationFetcher(final TetheringConfigurationFetcher fetcher) {
mFetcher = fetcher;
}
/** Interface to fetch TetheringConfiguration. */
public interface TetheringConfigurationFetcher {
/**
* Fetch current tethering configuration. This will be called to ensure whether entitlement
* check is needed.
* @return TetheringConfiguration instance.
*/
TetheringConfiguration fetchTetheringConfiguration();
}
/**
* Check if cellular upstream is permitted.
*/
public boolean isCellularUpstreamPermitted() {
// If provisioning is required and EntitlementManager don't know any downstream,
// cellular upstream should not be allowed.
final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
if (mCurrentTethers.size() == 0 && isTetherProvisioningRequired(config)) {
return false;
}
return mCellularUpstreamPermitted;
}
/**
* This is called when tethering starts.
* Launch provisioning app if upstream is cellular.
*
* @param downstreamType tethering type from TetheringManager.TETHERING_{@code *}
* @param showProvisioningUi a boolean indicating whether to show the
* provisioning app UI if there is one.
*/
public void startProvisioningIfNeeded(int downstreamType, boolean showProvisioningUi) {
mHandler.sendMessage(mHandler.obtainMessage(EVENT_START_PROVISIONING,
downstreamType, encodeBool(showProvisioningUi)));
}
private void handleStartProvisioningIfNeeded(int type, boolean showProvisioningUi) {
if (!isValidDownstreamType(type)) return;
if (!mCurrentTethers.contains(type)) mCurrentTethers.add(type);
final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
if (isTetherProvisioningRequired(config)) {
// If provisioning is required and the result is not available yet,
// cellular upstream should not be allowed.
if (mCellularPermitted.size() == 0) {
mCellularUpstreamPermitted = false;
}
// If upstream is not cellular, provisioning app would not be launched
// till upstream change to cellular.
if (mUsingCellularAsUpstream) {
if (showProvisioningUi) {
runUiTetherProvisioning(type, config.activeDataSubId);
} else {
runSilentTetherProvisioning(type, config.activeDataSubId);
}
mNeedReRunProvisioningUi = false;
} else {
mNeedReRunProvisioningUi |= showProvisioningUi;
}
} else {
mCellularUpstreamPermitted = true;
}
}
/**
* Tell EntitlementManager that a given type of tethering has been disabled
*
* @param type tethering type from TetheringManager.TETHERING_{@code *}
*/
public void stopProvisioningIfNeeded(int type) {
mHandler.sendMessage(mHandler.obtainMessage(EVENT_STOP_PROVISIONING, type, 0));
}
private void handleStopProvisioningIfNeeded(int type) {
if (!isValidDownstreamType(type)) return;
mCurrentTethers.remove(type);
// There are lurking bugs where the notion of "provisioning required" or
// "tethering supported" may change without without tethering being notified properly.
// Remove the mapping all the time no matter provisioning is required or not.
removeDownstreamMapping(type);
}
/**
* Notify EntitlementManager if upstream is cellular or not.
*
* @param isCellular whether tethering upstream is cellular.
*/
public void notifyUpstream(boolean isCellular) {
mHandler.sendMessage(mHandler.obtainMessage(
EVENT_UPSTREAM_CHANGED, encodeBool(isCellular), 0));
}
private void handleNotifyUpstream(boolean isCellular) {
if (DBG) {
mLog.i("notifyUpstream: " + isCellular
+ ", mCellularUpstreamPermitted: " + mCellularUpstreamPermitted
+ ", mNeedReRunProvisioningUi: " + mNeedReRunProvisioningUi);
}
mUsingCellularAsUpstream = isCellular;
if (mUsingCellularAsUpstream) {
final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
handleMaybeRunProvisioning(config);
}
}
/** Run provisioning if needed */
public void maybeRunProvisioning() {
mHandler.sendMessage(mHandler.obtainMessage(EVENT_MAYBE_RUN_PROVISIONING));
}
private void handleMaybeRunProvisioning(final TetheringConfiguration config) {
if (mCurrentTethers.size() == 0 || !isTetherProvisioningRequired(config)) {
return;
}
// Whenever any entitlement value changes, all downstreams will re-evaluate whether they
// are allowed. Therefore even if the silent check here ends in a failure and the UI later
// yields success, then the downstream that got a failure will re-evaluate as a result of
// the change and get the new correct value.
for (Integer downstream : mCurrentTethers) {
if (mCellularPermitted.indexOfKey(downstream) < 0) {
if (mNeedReRunProvisioningUi) {
mNeedReRunProvisioningUi = false;
runUiTetherProvisioning(downstream, config.activeDataSubId);
} else {
runSilentTetherProvisioning(downstream, config.activeDataSubId);
}
}
}
}
/**
* Check if the device requires a provisioning check in order to enable tethering.
*
* @param config an object that encapsulates the various tethering configuration elements.
* @return a boolean - {@code true} indicating tether provisioning is required by the carrier.
*/
@VisibleForTesting
protected boolean isTetherProvisioningRequired(final TetheringConfiguration config) {
if (SystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
|| config.provisioningApp.length == 0) {
return false;
}
if (carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
return false;
}
return (config.provisioningApp.length == 2);
}
/**
* Re-check tethering provisioning for all enabled tether types.
* Reference TetheringManager.TETHERING_{@code *} for each tether type.
*
* @param config an object that encapsulates the various tethering configuration elements.
* Note: this method is only called from TetherMaster on the handler thread.
* If there are new callers from different threads, the logic should move to
* masterHandler to avoid race conditions.
*/
public void reevaluateSimCardProvisioning(final TetheringConfiguration config) {
if (DBG) mLog.i("reevaluateSimCardProvisioning");
if (!mHandler.getLooper().isCurrentThread()) {
// Except for test, this log should not appear in normal flow.
mLog.log("reevaluateSimCardProvisioning() don't run in TetherMaster thread");
}
mEntitlementCacheValue.clear();
mCellularPermitted.clear();
// TODO: refine provisioning check to isTetherProvisioningRequired() ??
if (!config.hasMobileHotspotProvisionApp()
|| carrierConfigAffirmsEntitlementCheckNotRequired(config)) {
evaluateCellularPermission(config);
return;
}
if (mUsingCellularAsUpstream) {
handleMaybeRunProvisioning(config);
}
}
/**
* Get carrier configuration bundle.
* @param config an object that encapsulates the various tethering configuration elements.
* */
public PersistableBundle getCarrierConfig(final TetheringConfiguration config) {
final CarrierConfigManager configManager = (CarrierConfigManager) mContext
.getSystemService(Context.CARRIER_CONFIG_SERVICE);
if (configManager == null) return null;
final PersistableBundle carrierConfig = configManager.getConfigForSubId(
config.activeDataSubId);
if (CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) {
return carrierConfig;
}
return null;
}
// The logic here is aimed solely at confirming that a CarrierConfig exists
// and affirms that entitlement checks are not required.
//
// TODO: find a better way to express this, or alter the checking process
// entirely so that this is more intuitive.
private boolean carrierConfigAffirmsEntitlementCheckNotRequired(
final TetheringConfiguration config) {
// Check carrier config for entitlement checks
final PersistableBundle carrierConfig = getCarrierConfig(config);
if (carrierConfig == null) return false;
// A CarrierConfigManager was found and it has a config.
final boolean isEntitlementCheckRequired = carrierConfig.getBoolean(
CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL);
return !isEntitlementCheckRequired;
}
/**
* Run no UI tethering provisioning check.
* @param type tethering type from TetheringManager.TETHERING_{@code *}
* @param subId default data subscription ID.
*/
@VisibleForTesting
protected void runSilentTetherProvisioning(int type, int subId) {
if (DBG) mLog.i("runSilentTetherProvisioning: " + type);
// For silent provisioning, settings would stop tethering when entitlement fail.
ResultReceiver receiver = buildProxyReceiver(type, false/* notifyFail */, null);
Intent intent = new Intent();
intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
intent.putExtra(EXTRA_RUN_PROVISION, true);
intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
intent.putExtra(EXTRA_SUBID, subId);
intent.setComponent(mSilentProvisioningService);
// Only admin user can change tethering and SilentTetherProvisioning don't need to
// show UI, it is fine to always start setting's background service as system user.
mContext.startService(intent);
}
private void runUiTetherProvisioning(int type, int subId) {
ResultReceiver receiver = buildProxyReceiver(type, true/* notifyFail */, null);
runUiTetherProvisioning(type, subId, receiver);
}
/**
* Run the UI-enabled tethering provisioning check.
* @param type tethering type from TetheringManager.TETHERING_{@code *}
* @param subId default data subscription ID.
* @param receiver to receive entitlement check result.
*/
@VisibleForTesting
protected void runUiTetherProvisioning(int type, int subId, ResultReceiver receiver) {
if (DBG) mLog.i("runUiTetherProvisioning: " + type);
Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING_UI);
intent.putExtra(EXTRA_ADD_TETHER_TYPE, type);
intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
intent.putExtra(EXTRA_SUBID, subId);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// Only launch entitlement UI for system user. Entitlement UI should not appear for other
// user because only admin user is allowed to change tethering.
mContext.startActivity(intent);
}
// Not needed to check if this don't run on the handler thread because it's private.
private void scheduleProvisioningRechecks(final TetheringConfiguration config) {
if (mProvisioningRecheckAlarm == null) {
final int period = config.provisioningCheckPeriod;
if (period <= 0) return;
Intent intent = new Intent(ACTION_PROVISIONING_ALARM);
mProvisioningRecheckAlarm = PendingIntent.getBroadcast(mContext, 0, intent, 0);
AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
Context.ALARM_SERVICE);
long periodMs = period * MS_PER_HOUR;
long firstAlarmTime = SystemClock.elapsedRealtime() + periodMs;
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME, firstAlarmTime, periodMs,
mProvisioningRecheckAlarm);
}
}
private void cancelTetherProvisioningRechecks() {
if (mProvisioningRecheckAlarm != null) {
AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
Context.ALARM_SERVICE);
alarmManager.cancel(mProvisioningRecheckAlarm);
mProvisioningRecheckAlarm = null;
}
}
private void evaluateCellularPermission(final TetheringConfiguration config) {
final boolean oldPermitted = mCellularUpstreamPermitted;
mCellularUpstreamPermitted = (!isTetherProvisioningRequired(config)
|| mCellularPermitted.indexOfValue(TETHER_ERROR_NO_ERROR) > -1);
if (DBG) {
mLog.i("Cellular permission change from " + oldPermitted
+ " to " + mCellularUpstreamPermitted);
}
if (mCellularUpstreamPermitted != oldPermitted) {
mLog.log("Cellular permission change: " + mCellularUpstreamPermitted);
mTetherMasterSM.sendMessage(mPermissionChangeMessageCode);
}
// Only schedule periodic re-check when tether is provisioned
// and the result is ok.
if (mCellularUpstreamPermitted && mCellularPermitted.size() > 0) {
scheduleProvisioningRechecks(config);
} else {
cancelTetherProvisioningRechecks();
}
}
/**
* Add the mapping between provisioning result and tethering type.
* Notify UpstreamNetworkMonitor if Cellular permission changes.
*
* @param type tethering type from TetheringManager.TETHERING_{@code *}
* @param resultCode Provisioning result
*/
protected void addDownstreamMapping(int type, int resultCode) {
mLog.i("addDownstreamMapping: " + type + ", result: " + resultCode
+ " ,TetherTypeRequested: " + mCurrentTethers.contains(type));
if (!mCurrentTethers.contains(type)) return;
mCellularPermitted.put(type, resultCode);
final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
evaluateCellularPermission(config);
}
/**
* Remove the mapping for input tethering type.
* @param type tethering type from TetheringManager.TETHERING_{@code *}
*/
protected void removeDownstreamMapping(int type) {
mLog.i("removeDownstreamMapping: " + type);
mCellularPermitted.delete(type);
final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
evaluateCellularPermission(config);
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_PROVISIONING_ALARM.equals(intent.getAction())) {
mLog.log("Received provisioning alarm");
final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
reevaluateSimCardProvisioning(config);
}
}
};
private class EntitlementHandler extends Handler {
EntitlementHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_START_PROVISIONING:
handleStartProvisioningIfNeeded(msg.arg1, toBool(msg.arg2));
break;
case EVENT_STOP_PROVISIONING:
handleStopProvisioningIfNeeded(msg.arg1);
break;
case EVENT_UPSTREAM_CHANGED:
handleNotifyUpstream(toBool(msg.arg1));
break;
case EVENT_MAYBE_RUN_PROVISIONING:
final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
handleMaybeRunProvisioning(config);
break;
case EVENT_GET_ENTITLEMENT_VALUE:
handleRequestLatestTetheringEntitlementValue(msg.arg1,
(ResultReceiver) msg.obj, toBool(msg.arg2));
break;
default:
mLog.log("Unknown event: " + msg.what);
break;
}
}
}
private static boolean toBool(int encodedBoolean) {
return encodedBoolean != 0;
}
private static int encodeBool(boolean b) {
return b ? 1 : 0;
}
private static boolean isValidDownstreamType(int type) {
switch (type) {
case TETHERING_BLUETOOTH:
case TETHERING_ETHERNET:
case TETHERING_USB:
case TETHERING_WIFI:
return true;
default:
return false;
}
}
/**
* Dump the infromation of EntitlementManager.
* @param pw {@link PrintWriter} is used to print formatted
*/
public void dump(PrintWriter pw) {
pw.print("mCellularUpstreamPermitted: ");
pw.println(mCellularUpstreamPermitted);
for (Integer type : mCurrentTethers) {
pw.print("Type: ");
pw.print(typeString(type));
if (mCellularPermitted.indexOfKey(type) > -1) {
pw.print(", Value: ");
pw.println(errorString(mCellularPermitted.get(type)));
} else {
pw.println(", Value: empty");
}
}
}
private static String typeString(int type) {
switch (type) {
case TETHERING_BLUETOOTH: return "TETHERING_BLUETOOTH";
case TETHERING_INVALID: return "TETHERING_INVALID";
case TETHERING_USB: return "TETHERING_USB";
case TETHERING_WIFI: return "TETHERING_WIFI";
default:
return String.format("TETHERING UNKNOWN TYPE (%d)", type);
}
}
private static String errorString(int value) {
switch (value) {
case TETHER_ERROR_ENTITLEMENT_UNKNOWN: return "TETHER_ERROR_ENTITLEMENT_UNKONWN";
case TETHER_ERROR_NO_ERROR: return "TETHER_ERROR_NO_ERROR";
case TETHER_ERROR_PROVISIONING_FAILED: return "TETHER_ERROR_PROVISIONING_FAILED";
default:
return String.format("UNKNOWN ERROR (%d)", value);
}
}
private ResultReceiver buildProxyReceiver(int type, boolean notifyFail,
final ResultReceiver receiver) {
ResultReceiver rr = new ResultReceiver(mHandler) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
int updatedCacheValue = updateEntitlementCacheValue(type, resultCode);
addDownstreamMapping(type, updatedCacheValue);
if (updatedCacheValue == TETHER_ERROR_PROVISIONING_FAILED && notifyFail) {
mListener.onUiEntitlementFailed(type);
}
if (receiver != null) receiver.send(updatedCacheValue, null);
}
};
return writeToParcel(rr);
}
// Instances of ResultReceiver need to be public classes for remote processes to be able
// to load them (otherwise, ClassNotFoundException). For private classes, this method
// performs a trick : round-trip parceling any instance of ResultReceiver will return a
// vanilla instance of ResultReceiver sharing the binder token with the original receiver.
// The binder token has a reference to the original instance of the private class and will
// still call its methods, and can be sent over. However it cannot be used for anything
// else than sending over a Binder call.
// While round-trip parceling is not great, there is currently no other way of generating
// a vanilla instance of ResultReceiver because all its fields are private.
private ResultReceiver writeToParcel(final ResultReceiver receiver) {
Parcel parcel = Parcel.obtain();
receiver.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel);
parcel.recycle();
return receiverForSending;
}
/**
* Update the last entitlement value to internal cache
*
* @param type tethering type from TetheringManager.TETHERING_{@code *}
* @param resultCode last entitlement value
* @return the last updated entitlement value
*/
private int updateEntitlementCacheValue(int type, int resultCode) {
if (DBG) {
mLog.i("updateEntitlementCacheValue: " + type + ", result: " + resultCode);
}
if (resultCode == TETHER_ERROR_NO_ERROR) {
mEntitlementCacheValue.put(type, resultCode);
return resultCode;
} else {
mEntitlementCacheValue.put(type, TETHER_ERROR_PROVISIONING_FAILED);
return TETHER_ERROR_PROVISIONING_FAILED;
}
}
/** Get the last value of the tethering entitlement check. */
public void requestLatestTetheringEntitlementResult(int downstream, ResultReceiver receiver,
boolean showEntitlementUi) {
mHandler.sendMessage(mHandler.obtainMessage(EVENT_GET_ENTITLEMENT_VALUE,
downstream, encodeBool(showEntitlementUi), receiver));
}
private void handleRequestLatestTetheringEntitlementValue(int downstream,
ResultReceiver receiver, boolean showEntitlementUi) {
if (!isValidDownstreamType(downstream)) {
receiver.send(TETHER_ERROR_ENTITLEMENT_UNKNOWN, null);
return;
}
final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration();
if (!isTetherProvisioningRequired(config)) {
receiver.send(TETHER_ERROR_NO_ERROR, null);
return;
}
final int cacheValue = mEntitlementCacheValue.get(
downstream, TETHER_ERROR_ENTITLEMENT_UNKNOWN);
if (cacheValue == TETHER_ERROR_NO_ERROR || !showEntitlementUi) {
receiver.send(cacheValue, null);
} else {
ResultReceiver proxy = buildProxyReceiver(downstream, false/* notifyFail */, receiver);
runUiTetherProvisioning(downstream, config.activeDataSubId, proxy);
}
}
}

View file

@ -0,0 +1,311 @@
/*
* Copyright (C) 2016 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.networkstack.tethering;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.RouteInfo;
import android.net.ip.IpServer;
import android.net.util.NetworkConstants;
import android.net.util.SharedLog;
import android.util.Log;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Random;
/**
* IPv6 tethering is rather different from IPv4 owing to the absence of NAT.
* This coordinator is responsible for evaluating the dedicated prefixes
* assigned to the device and deciding how to divvy them up among downstream
* interfaces.
*
* @hide
*/
public class IPv6TetheringCoordinator {
private static final String TAG = IPv6TetheringCoordinator.class.getSimpleName();
private static final boolean DBG = false;
private static final boolean VDBG = false;
private static class Downstream {
public final IpServer ipServer;
public final int mode; // IpServer.STATE_*
// Used to append to a ULA /48, constructing a ULA /64 for local use.
public final short subnetId;
Downstream(IpServer ipServer, int mode, short subnetId) {
this.ipServer = ipServer;
this.mode = mode;
this.subnetId = subnetId;
}
}
private final ArrayList<IpServer> mNotifyList;
private final SharedLog mLog;
// NOTE: mActiveDownstreams is a list and not a hash data structure because
// we keep active downstreams in arrival order. This is done so /64s can
// be parceled out on a "first come, first served" basis and a /64 used by
// a downstream that is no longer active can be redistributed to any next
// waiting active downstream (again, in arrival order).
private final LinkedList<Downstream> mActiveDownstreams;
private final byte[] mUniqueLocalPrefix;
private short mNextSubnetId;
private UpstreamNetworkState mUpstreamNetworkState;
public IPv6TetheringCoordinator(ArrayList<IpServer> notifyList, SharedLog log) {
mNotifyList = notifyList;
mLog = log.forSubComponent(TAG);
mActiveDownstreams = new LinkedList<>();
mUniqueLocalPrefix = generateUniqueLocalPrefix();
mNextSubnetId = 0;
}
/** Add active downstream to ipv6 tethering candidate list. */
public void addActiveDownstream(IpServer downstream, int mode) {
if (findDownstream(downstream) == null) {
// Adding a new downstream appends it to the list. Adding a
// downstream a second time without first removing it has no effect.
// We never change the mode of a downstream except by first removing
// it and then re-adding it (with its new mode specified);
if (mActiveDownstreams.offer(new Downstream(downstream, mode, mNextSubnetId))) {
// Make sure subnet IDs are always positive. They are appended
// to a ULA /48 to make a ULA /64 for local use.
mNextSubnetId = (short) Math.max(0, mNextSubnetId + 1);
}
updateIPv6TetheringInterfaces();
}
}
/** Remove downstream from ipv6 tethering candidate list. */
public void removeActiveDownstream(IpServer downstream) {
stopIPv6TetheringOn(downstream);
if (mActiveDownstreams.remove(findDownstream(downstream))) {
updateIPv6TetheringInterfaces();
}
// When tethering is stopping we can reset the subnet counter.
if (mNotifyList.isEmpty()) {
if (!mActiveDownstreams.isEmpty()) {
Log.wtf(TAG, "Tethering notify list empty, IPv6 downstreams non-empty.");
}
mNextSubnetId = 0;
}
}
/**
* Call when UpstreamNetworkState may be changed.
* If upstream has ipv6 for tethering, update this new UpstreamNetworkState
* to IpServer. Otherwise stop ipv6 tethering on downstream interfaces.
*/
public void updateUpstreamNetworkState(UpstreamNetworkState ns) {
if (VDBG) {
Log.d(TAG, "updateUpstreamNetworkState: " + toDebugString(ns));
}
if (TetheringInterfaceUtils.getIPv6Interface(ns) == null) {
stopIPv6TetheringOnAllInterfaces();
setUpstreamNetworkState(null);
return;
}
if (mUpstreamNetworkState != null
&& !ns.network.equals(mUpstreamNetworkState.network)) {
stopIPv6TetheringOnAllInterfaces();
}
setUpstreamNetworkState(ns);
updateIPv6TetheringInterfaces();
}
private void stopIPv6TetheringOnAllInterfaces() {
for (IpServer ipServer : mNotifyList) {
stopIPv6TetheringOn(ipServer);
}
}
private void setUpstreamNetworkState(UpstreamNetworkState ns) {
if (ns == null) {
mUpstreamNetworkState = null;
} else {
// Make a deep copy of the parts we need.
mUpstreamNetworkState = new UpstreamNetworkState(
new LinkProperties(ns.linkProperties),
new NetworkCapabilities(ns.networkCapabilities),
new Network(ns.network));
}
mLog.log("setUpstreamNetworkState: " + toDebugString(mUpstreamNetworkState));
}
private void updateIPv6TetheringInterfaces() {
for (IpServer ipServer : mNotifyList) {
final LinkProperties lp = getInterfaceIPv6LinkProperties(ipServer);
ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, lp);
break;
}
}
private LinkProperties getInterfaceIPv6LinkProperties(IpServer ipServer) {
final Downstream ds = findDownstream(ipServer);
if (ds == null) return null;
if (ds.mode == IpServer.STATE_LOCAL_ONLY) {
// Build a Unique Locally-assigned Prefix configuration.
return getUniqueLocalConfig(mUniqueLocalPrefix, ds.subnetId);
}
// This downstream is in IpServer.STATE_TETHERED mode.
if (mUpstreamNetworkState == null || mUpstreamNetworkState.linkProperties == null) {
return null;
}
// NOTE: Here, in future, we would have policies to decide how to divvy
// up the available dedicated prefixes among downstream interfaces.
// At this time we have no such mechanism--we only support tethering
// IPv6 toward the oldest (first requested) active downstream.
final Downstream currentActive = mActiveDownstreams.peek();
if (currentActive != null && currentActive.ipServer == ipServer) {
final LinkProperties lp = getIPv6OnlyLinkProperties(
mUpstreamNetworkState.linkProperties);
if (lp.hasIpv6DefaultRoute() && lp.hasGlobalIpv6Address()) {
return lp;
}
}
return null;
}
Downstream findDownstream(IpServer ipServer) {
for (Downstream ds : mActiveDownstreams) {
if (ds.ipServer == ipServer) return ds;
}
return null;
}
private static LinkProperties getIPv6OnlyLinkProperties(LinkProperties lp) {
final LinkProperties v6only = new LinkProperties();
if (lp == null) {
return v6only;
}
// NOTE: At this time we don't copy over any information about any
// stacked links. No current stacked link configuration has IPv6.
v6only.setInterfaceName(lp.getInterfaceName());
v6only.setMtu(lp.getMtu());
for (LinkAddress linkAddr : lp.getLinkAddresses()) {
if (linkAddr.isGlobalPreferred() && linkAddr.getPrefixLength() == 64) {
v6only.addLinkAddress(linkAddr);
}
}
for (RouteInfo routeInfo : lp.getRoutes()) {
final IpPrefix destination = routeInfo.getDestination();
if ((destination.getAddress() instanceof Inet6Address)
&& (destination.getPrefixLength() <= 64)) {
v6only.addRoute(routeInfo);
}
}
for (InetAddress dnsServer : lp.getDnsServers()) {
if (isIPv6GlobalAddress(dnsServer)) {
// For now we include ULAs.
v6only.addDnsServer(dnsServer);
}
}
v6only.setDomains(lp.getDomains());
return v6only;
}
// TODO: Delete this and switch to LinkAddress#isGlobalPreferred once we
// announce our own IPv6 address as DNS server.
private static boolean isIPv6GlobalAddress(InetAddress ip) {
return (ip instanceof Inet6Address)
&& !ip.isAnyLocalAddress()
&& !ip.isLoopbackAddress()
&& !ip.isLinkLocalAddress()
&& !ip.isSiteLocalAddress()
&& !ip.isMulticastAddress();
}
private static LinkProperties getUniqueLocalConfig(byte[] ulp, short subnetId) {
final LinkProperties lp = new LinkProperties();
final IpPrefix local48 = makeUniqueLocalPrefix(ulp, (short) 0, 48);
lp.addRoute(new RouteInfo(local48, null, null, RouteInfo.RTN_UNICAST));
final IpPrefix local64 = makeUniqueLocalPrefix(ulp, subnetId, 64);
// Because this is a locally-generated ULA, we don't have an upstream
// address. But because the downstream IP address management code gets
// its prefix from the upstream's IP address, we create a fake one here.
lp.addLinkAddress(new LinkAddress(local64.getAddress(), 64));
lp.setMtu(NetworkConstants.ETHER_MTU);
return lp;
}
private static IpPrefix makeUniqueLocalPrefix(byte[] in6addr, short subnetId, int prefixlen) {
final byte[] bytes = Arrays.copyOf(in6addr, in6addr.length);
bytes[7] = (byte) (subnetId >> 8);
bytes[8] = (byte) subnetId;
final InetAddress addr;
try {
addr = InetAddress.getByAddress(bytes);
} catch (UnknownHostException e) {
throw new IllegalStateException("Invalid address length: " + bytes.length, e);
}
return new IpPrefix(addr, prefixlen);
}
// Generates a Unique Locally-assigned Prefix:
//
// https://tools.ietf.org/html/rfc4193#section-3.1
//
// The result is a /48 that can be used for local-only communications.
private static byte[] generateUniqueLocalPrefix() {
final byte[] ulp = new byte[6]; // 6 = 48bits / 8bits/byte
(new Random()).nextBytes(ulp);
final byte[] in6addr = Arrays.copyOf(ulp, NetworkConstants.IPV6_ADDR_LEN);
in6addr[0] = (byte) 0xfd; // fc00::/7 and L=1
return in6addr;
}
private static String toDebugString(UpstreamNetworkState ns) {
if (ns == null) {
return "UpstreamNetworkState{null}";
}
return ns.toString();
}
private static void stopIPv6TetheringOn(IpServer ipServer) {
ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null);
}
}

View file

@ -0,0 +1,721 @@
/*
* Copyright (C) 2017 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.networkstack.tethering;
import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
import static android.net.NetworkStats.METERED_NO;
import static android.net.NetworkStats.ROAMING_NO;
import static android.net.NetworkStats.SET_DEFAULT;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static android.net.NetworkStats.UID_TETHERING;
import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.usage.NetworkStatsManager;
import android.content.ContentResolver;
import android.net.InetAddresses;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.NetworkStats;
import android.net.NetworkStats.Entry;
import android.net.RouteInfo;
import android.net.netlink.ConntrackMessage;
import android.net.netlink.NetlinkConstants;
import android.net.netlink.NetlinkSocket;
import android.net.netstats.provider.NetworkStatsProvider;
import android.net.util.SharedLog;
import android.os.Handler;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.OsConstants;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.networkstack.tethering.OffloadHardwareInterface.ForwardedStats;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
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.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* A class to encapsulate the business logic of programming the tethering
* hardware offload interface.
*
* @hide
*/
public class OffloadController {
private static final String TAG = OffloadController.class.getSimpleName();
private static final boolean DBG = false;
private static final String ANYIP = "0.0.0.0";
private static final ForwardedStats EMPTY_STATS = new ForwardedStats();
@VisibleForTesting
enum StatsType {
STATS_PER_IFACE,
STATS_PER_UID,
}
private enum UpdateType { IF_NEEDED, FORCE };
private final Handler mHandler;
private final OffloadHardwareInterface mHwInterface;
private final ContentResolver mContentResolver;
@Nullable
private final OffloadTetheringStatsProvider mStatsProvider;
private final SharedLog mLog;
private final HashMap<String, LinkProperties> mDownstreams;
private boolean mConfigInitialized;
private boolean mControlInitialized;
private LinkProperties mUpstreamLinkProperties;
// The complete set of offload-exempt prefixes passed in via Tethering from
// all upstream and downstream sources.
private Set<IpPrefix> mExemptPrefixes;
// A strictly "smaller" set of prefixes, wherein offload-approved prefixes
// (e.g. downstream on-link prefixes) have been removed and replaced with
// prefixes representing only the locally-assigned IP addresses.
private Set<String> mLastLocalPrefixStrs;
// Maps upstream interface names to offloaded traffic statistics.
// Always contains the latest value received from the hardware for each interface, regardless of
// whether offload is currently running on that interface.
private ConcurrentHashMap<String, ForwardedStats> mForwardedStats =
new ConcurrentHashMap<>(16, 0.75F, 1);
// Maps upstream interface names to interface quotas.
// Always contains the latest value received from the framework for each interface, regardless
// of whether offload is currently running (or is even supported) on that interface. Only
// includes upstream interfaces that have a quota set.
private HashMap<String, Long> mInterfaceQuotas = new HashMap<>();
private int mNatUpdateCallbacksReceived;
private int mNatUpdateNetlinkErrors;
public OffloadController(Handler h, OffloadHardwareInterface hwi,
ContentResolver contentResolver, NetworkStatsManager nsm, SharedLog log) {
mHandler = h;
mHwInterface = hwi;
mContentResolver = contentResolver;
mLog = log.forSubComponent(TAG);
mDownstreams = new HashMap<>();
mExemptPrefixes = new HashSet<>();
mLastLocalPrefixStrs = new HashSet<>();
OffloadTetheringStatsProvider provider = new OffloadTetheringStatsProvider();
try {
nsm.registerNetworkStatsProvider(getClass().getSimpleName(), provider);
} catch (RuntimeException e) {
Log.wtf(TAG, "Cannot register offload stats provider: " + e);
provider = null;
}
mStatsProvider = provider;
}
/** Start hardware offload. */
public boolean start() {
if (started()) return true;
if (isOffloadDisabled()) {
mLog.i("tethering offload disabled");
return false;
}
if (!mConfigInitialized) {
mConfigInitialized = mHwInterface.initOffloadConfig();
if (!mConfigInitialized) {
mLog.i("tethering offload config not supported");
stop();
return false;
}
}
mControlInitialized = mHwInterface.initOffloadControl(
// OffloadHardwareInterface guarantees that these callback
// methods are called on the handler passed to it, which is the
// same as mHandler, as coordinated by the setup in Tethering.
new OffloadHardwareInterface.ControlCallback() {
@Override
public void onStarted() {
if (!started()) return;
mLog.log("onStarted");
}
@Override
public void onStoppedError() {
if (!started()) return;
mLog.log("onStoppedError");
}
@Override
public void onStoppedUnsupported() {
if (!started()) return;
mLog.log("onStoppedUnsupported");
// Poll for statistics and trigger a sweep of tethering
// stats by observers. This might not succeed, but it's
// worth trying anyway. We need to do this because from
// this point on we continue with software forwarding,
// and we need to synchronize stats and limits between
// software and hardware forwarding.
updateStatsForAllUpstreams();
if (mStatsProvider != null) mStatsProvider.pushTetherStats();
}
@Override
public void onSupportAvailable() {
if (!started()) return;
mLog.log("onSupportAvailable");
// [1] Poll for statistics and trigger a sweep of stats
// by observers. We need to do this to ensure that any
// limits set take into account any software tethering
// traffic that has been happening in the meantime.
updateStatsForAllUpstreams();
if (mStatsProvider != null) mStatsProvider.pushTetherStats();
// [2] (Re)Push all state.
computeAndPushLocalPrefixes(UpdateType.FORCE);
pushAllDownstreamState();
pushUpstreamParameters(null);
}
@Override
public void onStoppedLimitReached() {
if (!started()) return;
mLog.log("onStoppedLimitReached");
// We cannot reliably determine on which interface the limit was reached,
// because the HAL interface does not specify it. We cannot just use the
// current upstream, because that might have changed since the time that
// the HAL queued the callback.
// TODO: rev the HAL so that it provides an interface name.
updateStatsForCurrentUpstream();
if (mStatsProvider != null) {
mStatsProvider.pushTetherStats();
// Push stats to service does not cause the service react to it
// immediately. Inform the service about limit reached.
mStatsProvider.notifyLimitReached();
}
}
@Override
public void onNatTimeoutUpdate(int proto,
String srcAddr, int srcPort,
String dstAddr, int dstPort) {
if (!started()) return;
updateNatTimeout(proto, srcAddr, srcPort, dstAddr, dstPort);
}
});
final boolean isStarted = started();
if (!isStarted) {
mLog.i("tethering offload control not supported");
stop();
} else {
mLog.log("tethering offload started");
mNatUpdateCallbacksReceived = 0;
mNatUpdateNetlinkErrors = 0;
}
return isStarted;
}
/** Stop hardware offload. */
public void stop() {
// Completely stops tethering offload. After this method is called, it is no longer safe to
// call any HAL method, no callbacks from the hardware will be delivered, and any in-flight
// callbacks must be ignored. Offload may be started again by calling start().
final boolean wasStarted = started();
updateStatsForCurrentUpstream();
mUpstreamLinkProperties = null;
mHwInterface.stopOffloadControl();
mControlInitialized = false;
mConfigInitialized = false;
if (wasStarted) mLog.log("tethering offload stopped");
}
private boolean started() {
return mConfigInitialized && mControlInitialized;
}
@VisibleForTesting
class OffloadTetheringStatsProvider extends NetworkStatsProvider {
// These stats must only ever be touched on the handler thread.
@NonNull
private NetworkStats mIfaceStats = new NetworkStats(0L, 0);
@NonNull
private NetworkStats mUidStats = new NetworkStats(0L, 0);
/**
* A helper function that collect tether stats from local hashmap. Note that this does not
* invoke binder call.
*/
@VisibleForTesting
@NonNull
NetworkStats getTetherStats(@NonNull StatsType how) {
NetworkStats stats = new NetworkStats(0L, 0);
final int uid = (how == StatsType.STATS_PER_UID) ? UID_TETHERING : UID_ALL;
for (final Map.Entry<String, ForwardedStats> kv : mForwardedStats.entrySet()) {
final ForwardedStats value = kv.getValue();
final Entry entry = new Entry(kv.getKey(), uid, SET_DEFAULT, TAG_NONE, METERED_NO,
ROAMING_NO, DEFAULT_NETWORK_NO, value.rxBytes, 0L, value.txBytes, 0L, 0L);
stats = stats.addEntry(entry);
}
return stats;
}
@Override
public void onSetLimit(String iface, long quotaBytes) {
// Listen for all iface is necessary since upstream might be changed after limit
// is set.
mHandler.post(() -> {
final Long curIfaceQuota = mInterfaceQuotas.get(iface);
// If the quota is set to unlimited, the value set to HAL is Long.MAX_VALUE,
// which is ~8.4 x 10^6 TiB, no one can actually reach it. Thus, it is not
// useful to set it multiple times.
// Otherwise, the quota needs to be updated to tell HAL to re-count from now even
// if the quota is the same as the existing one.
if (null == curIfaceQuota && QUOTA_UNLIMITED == quotaBytes) return;
if (quotaBytes == QUOTA_UNLIMITED) {
mInterfaceQuotas.remove(iface);
} else {
mInterfaceQuotas.put(iface, quotaBytes);
}
maybeUpdateDataLimit(iface);
});
}
/**
* Push stats to service, but does not cause a force polling. Note that this can only be
* called on the handler thread.
*/
public void pushTetherStats() {
// TODO: remove the accumulated stats and report the diff from HAL directly.
final NetworkStats ifaceDiff =
getTetherStats(StatsType.STATS_PER_IFACE).subtract(mIfaceStats);
final NetworkStats uidDiff =
getTetherStats(StatsType.STATS_PER_UID).subtract(mUidStats);
try {
notifyStatsUpdated(0 /* token */, ifaceDiff, uidDiff);
mIfaceStats = mIfaceStats.add(ifaceDiff);
mUidStats = mUidStats.add(uidDiff);
} catch (RuntimeException e) {
mLog.e("Cannot report network stats: ", e);
}
}
@Override
public void onRequestStatsUpdate(int token) {
// Do not attempt to update stats by querying the offload HAL
// synchronously from a different thread than the Handler thread. http://b/64771555.
mHandler.post(() -> {
updateStatsForCurrentUpstream();
pushTetherStats();
});
}
@Override
public void onSetAlert(long quotaBytes) {
// TODO: Ask offload HAL to notify alert without stopping traffic.
}
}
private String currentUpstreamInterface() {
return (mUpstreamLinkProperties != null)
? mUpstreamLinkProperties.getInterfaceName() : null;
}
private void maybeUpdateStats(String iface) {
if (TextUtils.isEmpty(iface)) {
return;
}
// Always called on the handler thread.
//
// Use get()/put() instead of updating ForwardedStats in place because we can be called
// concurrently with getTetherStats. In combination with the guarantees provided by
// ConcurrentHashMap, this ensures that getTetherStats always gets the most recent copy of
// the stats for each interface, and does not observe partial writes where rxBytes is
// updated and txBytes is not.
ForwardedStats diff = mHwInterface.getForwardedStats(iface);
ForwardedStats base = mForwardedStats.get(iface);
if (base != null) {
diff.add(base);
}
mForwardedStats.put(iface, diff);
// diff is a new object, just created by getForwardedStats(). Therefore, anyone reading from
// mForwardedStats (i.e., any caller of getTetherStats) will see the new stats immediately.
}
private boolean maybeUpdateDataLimit(String iface) {
// setDataLimit may only be called while offload is occurring on this upstream.
if (!started() || !TextUtils.equals(iface, currentUpstreamInterface())) {
return true;
}
Long limit = mInterfaceQuotas.get(iface);
if (limit == null) {
limit = Long.MAX_VALUE;
}
return mHwInterface.setDataLimit(iface, limit);
}
private void updateStatsForCurrentUpstream() {
maybeUpdateStats(currentUpstreamInterface());
}
private void updateStatsForAllUpstreams() {
// In practice, there should only ever be a single digit number of
// upstream interfaces over the lifetime of an active tethering session.
// Roughly speaking, imagine a very ambitious one or two of each of the
// following interface types: [ "rmnet_data", "wlan", "eth", "rndis" ].
for (Map.Entry<String, ForwardedStats> kv : mForwardedStats.entrySet()) {
maybeUpdateStats(kv.getKey());
}
}
/** Set current tethering upstream LinkProperties. */
public void setUpstreamLinkProperties(LinkProperties lp) {
if (!started() || Objects.equals(mUpstreamLinkProperties, lp)) return;
final String prevUpstream = currentUpstreamInterface();
mUpstreamLinkProperties = (lp != null) ? new LinkProperties(lp) : null;
// Make sure we record this interface in the ForwardedStats map.
final String iface = currentUpstreamInterface();
if (!TextUtils.isEmpty(iface)) mForwardedStats.putIfAbsent(iface, EMPTY_STATS);
// TODO: examine return code and decide what to do if programming
// upstream parameters fails (probably just wait for a subsequent
// onOffloadEvent() callback to tell us offload is available again and
// then reapply all state).
computeAndPushLocalPrefixes(UpdateType.IF_NEEDED);
pushUpstreamParameters(prevUpstream);
}
/** Set local prefixes. */
public void setLocalPrefixes(Set<IpPrefix> localPrefixes) {
mExemptPrefixes = localPrefixes;
if (!started()) return;
computeAndPushLocalPrefixes(UpdateType.IF_NEEDED);
}
/** Update current downstream LinkProperties. */
public void notifyDownstreamLinkProperties(LinkProperties lp) {
final String ifname = lp.getInterfaceName();
final LinkProperties oldLp = mDownstreams.put(ifname, new LinkProperties(lp));
if (Objects.equals(oldLp, lp)) return;
if (!started()) return;
pushDownstreamState(oldLp, lp);
}
private void pushDownstreamState(LinkProperties oldLp, LinkProperties newLp) {
final String ifname = newLp.getInterfaceName();
final List<RouteInfo> oldRoutes =
(oldLp != null) ? oldLp.getRoutes() : Collections.EMPTY_LIST;
final List<RouteInfo> newRoutes = newLp.getRoutes();
// For each old route, if not in new routes: remove.
for (RouteInfo ri : oldRoutes) {
if (shouldIgnoreDownstreamRoute(ri)) continue;
if (!newRoutes.contains(ri)) {
mHwInterface.removeDownstreamPrefix(ifname, ri.getDestination().toString());
}
}
// For each new route, if not in old routes: add.
for (RouteInfo ri : newRoutes) {
if (shouldIgnoreDownstreamRoute(ri)) continue;
if (!oldRoutes.contains(ri)) {
mHwInterface.addDownstreamPrefix(ifname, ri.getDestination().toString());
}
}
}
private void pushAllDownstreamState() {
for (LinkProperties lp : mDownstreams.values()) {
pushDownstreamState(null, lp);
}
}
/** Remove downstream interface from offload hardware. */
public void removeDownstreamInterface(String ifname) {
final LinkProperties lp = mDownstreams.remove(ifname);
if (lp == null) return;
if (!started()) return;
for (RouteInfo route : lp.getRoutes()) {
if (shouldIgnoreDownstreamRoute(route)) continue;
mHwInterface.removeDownstreamPrefix(ifname, route.getDestination().toString());
}
}
private boolean isOffloadDisabled() {
final int defaultDisposition = mHwInterface.getDefaultTetherOffloadDisabled();
return (Settings.Global.getInt(
mContentResolver, TETHER_OFFLOAD_DISABLED, defaultDisposition) != 0);
}
private boolean pushUpstreamParameters(String prevUpstream) {
final String iface = currentUpstreamInterface();
if (TextUtils.isEmpty(iface)) {
final boolean rval = mHwInterface.setUpstreamParameters("", ANYIP, ANYIP, null);
// Update stats after we've told the hardware to stop forwarding so
// we don't miss packets.
maybeUpdateStats(prevUpstream);
return rval;
}
// A stacked interface cannot be an upstream for hardware offload.
// Consequently, we examine only the primary interface name, look at
// getAddresses() rather than getAllAddresses(), and check getRoutes()
// rather than getAllRoutes().
final ArrayList<String> v6gateways = new ArrayList<>();
String v4addr = null;
String v4gateway = null;
for (InetAddress ip : mUpstreamLinkProperties.getAddresses()) {
if (ip instanceof Inet4Address) {
v4addr = ip.getHostAddress();
break;
}
}
// Find the gateway addresses of all default routes of either address family.
for (RouteInfo ri : mUpstreamLinkProperties.getRoutes()) {
if (!ri.hasGateway()) continue;
final String gateway = ri.getGateway().getHostAddress();
final InetAddress address = ri.getDestination().getAddress();
if (ri.isDefaultRoute() && address instanceof Inet4Address) {
v4gateway = gateway;
} else if (ri.isDefaultRoute() && address instanceof Inet6Address) {
v6gateways.add(gateway);
}
}
boolean success = mHwInterface.setUpstreamParameters(
iface, v4addr, v4gateway, (v6gateways.isEmpty() ? null : v6gateways));
if (!success) {
return success;
}
// Update stats after we've told the hardware to change routing so we don't miss packets.
maybeUpdateStats(prevUpstream);
// Data limits can only be set once offload is running on the upstream.
success = maybeUpdateDataLimit(iface);
if (!success) {
// If we failed to set a data limit, don't use this upstream, because we don't want to
// blow through the data limit that we were told to apply.
mLog.log("Setting data limit for " + iface + " failed, disabling offload.");
stop();
}
return success;
}
private boolean computeAndPushLocalPrefixes(UpdateType how) {
final boolean force = (how == UpdateType.FORCE);
final Set<String> localPrefixStrs = computeLocalPrefixStrings(
mExemptPrefixes, mUpstreamLinkProperties);
if (!force && mLastLocalPrefixStrs.equals(localPrefixStrs)) return true;
mLastLocalPrefixStrs = localPrefixStrs;
return mHwInterface.setLocalPrefixes(new ArrayList<>(localPrefixStrs));
}
// TODO: Factor in downstream LinkProperties once that information is available.
private static Set<String> computeLocalPrefixStrings(
Set<IpPrefix> localPrefixes, LinkProperties upstreamLinkProperties) {
// Create an editable copy.
final Set<IpPrefix> prefixSet = new HashSet<>(localPrefixes);
// TODO: If a downstream interface (not currently passed in) is reusing
// the /64 of the upstream (64share) then:
//
// [a] remove that /64 from the local prefixes
// [b] add in /128s for IP addresses on the downstream interface
// [c] add in /128s for IP addresses on the upstream interface
//
// Until downstream information is available here, simply add /128s from
// the upstream network; they'll just be redundant with their /64.
if (upstreamLinkProperties != null) {
for (LinkAddress linkAddr : upstreamLinkProperties.getLinkAddresses()) {
if (!linkAddr.isGlobalPreferred()) continue;
final InetAddress ip = linkAddr.getAddress();
if (!(ip instanceof Inet6Address)) continue;
prefixSet.add(new IpPrefix(ip, 128));
}
}
final HashSet<String> localPrefixStrs = new HashSet<>();
for (IpPrefix pfx : prefixSet) localPrefixStrs.add(pfx.toString());
return localPrefixStrs;
}
private static boolean shouldIgnoreDownstreamRoute(RouteInfo route) {
// Ignore any link-local routes.
final IpPrefix destination = route.getDestination();
final LinkAddress linkAddr = new LinkAddress(destination.getAddress(),
destination.getPrefixLength());
if (!linkAddr.isGlobalPreferred()) return true;
return false;
}
/** Dump information. */
public void dump(IndentingPrintWriter pw) {
if (isOffloadDisabled()) {
pw.println("Offload disabled");
return;
}
final boolean isStarted = started();
pw.println("Offload HALs " + (isStarted ? "started" : "not started"));
LinkProperties lp = mUpstreamLinkProperties;
String upstream = (lp != null) ? lp.getInterfaceName() : null;
pw.println("Current upstream: " + upstream);
pw.println("Exempt prefixes: " + mLastLocalPrefixStrs);
pw.println("NAT timeout update callbacks received during the "
+ (isStarted ? "current" : "last")
+ " offload session: "
+ mNatUpdateCallbacksReceived);
pw.println("NAT timeout update netlink errors during the "
+ (isStarted ? "current" : "last")
+ " offload session: "
+ mNatUpdateNetlinkErrors);
}
private void updateNatTimeout(
int proto, String srcAddr, int srcPort, String dstAddr, int dstPort) {
final String protoName = protoNameFor(proto);
if (protoName == null) {
mLog.e("Unknown NAT update callback protocol: " + proto);
return;
}
final Inet4Address src = parseIPv4Address(srcAddr);
if (src == null) {
mLog.e("Failed to parse IPv4 address: " + srcAddr);
return;
}
if (!isValidUdpOrTcpPort(srcPort)) {
mLog.e("Invalid src port: " + srcPort);
return;
}
final Inet4Address dst = parseIPv4Address(dstAddr);
if (dst == null) {
mLog.e("Failed to parse IPv4 address: " + dstAddr);
return;
}
if (!isValidUdpOrTcpPort(dstPort)) {
mLog.e("Invalid dst port: " + dstPort);
return;
}
mNatUpdateCallbacksReceived++;
final String natDescription = String.format("%s (%s, %s) -> (%s, %s)",
protoName, srcAddr, srcPort, dstAddr, dstPort);
if (DBG) {
mLog.log("NAT timeout update: " + natDescription);
}
final int timeoutSec = connectionTimeoutUpdateSecondsFor(proto);
final byte[] msg = ConntrackMessage.newIPv4TimeoutUpdateRequest(
proto, src, srcPort, dst, dstPort, timeoutSec);
try {
NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_NETFILTER, msg);
} catch (ErrnoException e) {
mNatUpdateNetlinkErrors++;
mLog.e("Error updating NAT conntrack entry >" + natDescription + "<: " + e
+ ", msg: " + NetlinkConstants.hexify(msg));
mLog.log("NAT timeout update callbacks received: " + mNatUpdateCallbacksReceived);
mLog.log("NAT timeout update netlink errors: " + mNatUpdateNetlinkErrors);
}
}
private static Inet4Address parseIPv4Address(String addrString) {
try {
final InetAddress ip = InetAddresses.parseNumericAddress(addrString);
// TODO: Consider other sanitization steps here, including perhaps:
// not eql to 0.0.0.0
// not within 169.254.0.0/16
// not within ::ffff:0.0.0.0/96
// not within ::/96
// et cetera.
if (ip instanceof Inet4Address) {
return (Inet4Address) ip;
}
} catch (IllegalArgumentException iae) { }
return null;
}
private static String protoNameFor(int proto) {
// OsConstants values are not constant expressions; no switch statement.
if (proto == OsConstants.IPPROTO_UDP) {
return "UDP";
} else if (proto == OsConstants.IPPROTO_TCP) {
return "TCP";
}
return null;
}
private static int connectionTimeoutUpdateSecondsFor(int proto) {
// TODO: Replace this with more thoughtful work, perhaps reading from
// and maybe writing to any required
//
// /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_*
// /proc/sys/net/netfilter/nf_conntrack_udp_timeout{,_stream}
//
// entries. TBD.
if (proto == OsConstants.IPPROTO_TCP) {
// Cf. /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established
return 432000;
} else {
// Cf. /proc/sys/net/netfilter/nf_conntrack_udp_timeout_stream
return 180;
}
}
private static boolean isValidUdpOrTcpPort(int port) {
return port > 0 && port < 65536;
}
}

View file

@ -0,0 +1,509 @@
/*
* Copyright (C) 2017 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.networkstack.tethering;
import static android.net.util.TetheringUtils.uint16;
import android.hardware.tetheroffload.config.V1_0.IOffloadConfig;
import android.hardware.tetheroffload.control.V1_0.IOffloadControl;
import android.hardware.tetheroffload.control.V1_0.ITetheringOffloadCallback;
import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate;
import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent;
import android.net.netlink.NetlinkSocket;
import android.net.util.SharedLog;
import android.net.util.SocketUtils;
import android.os.Handler;
import android.os.NativeHandle;
import android.os.RemoteException;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import com.android.internal.annotations.VisibleForTesting;
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.SocketAddress;
import java.net.SocketException;
import java.util.ArrayList;
/**
* Capture tethering dependencies, for injection.
*
* @hide
*/
public class OffloadHardwareInterface {
private static final String TAG = OffloadHardwareInterface.class.getSimpleName();
private static final String YIELDS = " -> ";
// Change this value to control whether tether offload is enabled or
// disabled by default in the absence of an explicit Settings value.
// See accompanying unittest to distinguish 0 from non-0 values.
private static final int DEFAULT_TETHER_OFFLOAD_DISABLED = 0;
private static final String NO_INTERFACE_NAME = "";
private static final String NO_IPV4_ADDRESS = "";
private static final String NO_IPV4_GATEWAY = "";
// Reference kernel/uapi/linux/netfilter/nfnetlink_compat.h
private static final int NF_NETLINK_CONNTRACK_NEW = 1;
private static final int NF_NETLINK_CONNTRACK_UPDATE = 2;
private static final int NF_NETLINK_CONNTRACK_DESTROY = 4;
private final Handler mHandler;
private final SharedLog mLog;
private IOffloadControl mOffloadControl;
private TetheringOffloadCallback mTetheringOffloadCallback;
private ControlCallback mControlCallback;
/** The callback to notify status of offload management process. */
public static class ControlCallback {
/** Offload started. */
public void onStarted() {}
/**
* Offload stopped because an error has occurred in lower layer.
*/
public void onStoppedError() {}
/**
* Offload stopped because the device has moved to a bearer on which hardware offload is
* not supported. Subsequent calls to setUpstreamParameters and add/removeDownstream will
* likely fail and cannot be presumed to be saved inside of the hardware management process.
* Upon receiving #onSupportAvailable(), the caller should reprogram the hardware to begin
* offload again.
*/
public void onStoppedUnsupported() {}
/** Indicate that offload is able to proivde support for this time. */
public void onSupportAvailable() {}
/** Offload stopped because of usage limit reached. */
public void onStoppedLimitReached() {}
/** Indicate to update NAT timeout. */
public void onNatTimeoutUpdate(int proto,
String srcAddr, int srcPort,
String dstAddr, int dstPort) {}
}
/** The object which records Tx/Rx forwarded bytes. */
public static class ForwardedStats {
public long rxBytes;
public long txBytes;
public ForwardedStats() {
rxBytes = 0;
txBytes = 0;
}
@VisibleForTesting
public ForwardedStats(long rxBytes, long txBytes) {
this.rxBytes = rxBytes;
this.txBytes = txBytes;
}
/** Add Tx/Rx bytes. */
public void add(ForwardedStats other) {
rxBytes += other.rxBytes;
txBytes += other.txBytes;
}
/** Returns the string representation of this object. */
public String toString() {
return String.format("rx:%s tx:%s", rxBytes, txBytes);
}
}
public OffloadHardwareInterface(Handler h, SharedLog log) {
mHandler = h;
mLog = log.forSubComponent(TAG);
}
/** Get default value indicating whether offload is supported. */
public int getDefaultTetherOffloadDisabled() {
return DEFAULT_TETHER_OFFLOAD_DISABLED;
}
/**
* Offload management process need to know conntrack rules to support NAT, but it may not have
* permission to create netlink netfilter sockets. Create two netlink netfilter sockets and
* share them with offload management process.
*/
public boolean initOffloadConfig() {
IOffloadConfig offloadConfig;
try {
offloadConfig = IOffloadConfig.getService();
} catch (RemoteException e) {
mLog.e("getIOffloadConfig error " + e);
return false;
}
if (offloadConfig == null) {
mLog.e("Could not find IOffloadConfig service");
return false;
}
// Per the IConfigOffload definition:
//
// h1 provides a file descriptor bound to the following netlink groups
// (NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY).
//
// h2 provides a file descriptor bound to the following netlink groups
// (NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY).
final NativeHandle h1 = createConntrackSocket(
NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY);
if (h1 == null) return false;
final NativeHandle h2 = createConntrackSocket(
NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY);
if (h2 == null) {
closeFdInNativeHandle(h1);
return false;
}
final CbResults results = new CbResults();
try {
offloadConfig.setHandles(h1, h2,
(boolean success, String errMsg) -> {
results.mSuccess = success;
results.mErrMsg = errMsg;
});
} catch (RemoteException e) {
record("initOffloadConfig, setHandles fail", e);
return false;
}
// Explicitly close FDs.
closeFdInNativeHandle(h1);
closeFdInNativeHandle(h2);
record("initOffloadConfig, setHandles results:", results);
return results.mSuccess;
}
private void closeFdInNativeHandle(final NativeHandle h) {
try {
h.close();
} catch (IOException | IllegalStateException e) {
// IllegalStateException means fd is already closed, do nothing here.
// Also nothing we can do if IOException.
}
}
private NativeHandle createConntrackSocket(final int groups) {
FileDescriptor fd;
try {
fd = NetlinkSocket.forProto(OsConstants.NETLINK_NETFILTER);
} catch (ErrnoException e) {
mLog.e("Unable to create conntrack socket " + e);
return null;
}
final SocketAddress sockAddr = SocketUtils.makeNetlinkSocketAddress(0, groups);
try {
Os.bind(fd, sockAddr);
} catch (ErrnoException | SocketException e) {
mLog.e("Unable to bind conntrack socket for groups " + groups + " error: " + e);
try {
SocketUtils.closeSocket(fd);
} catch (IOException ie) {
// Nothing we can do here
}
return null;
}
try {
Os.connect(fd, sockAddr);
} catch (ErrnoException | SocketException e) {
mLog.e("connect to kernel fail for groups " + groups + " error: " + e);
try {
SocketUtils.closeSocket(fd);
} catch (IOException ie) {
// Nothing we can do here
}
return null;
}
return new NativeHandle(fd, true);
}
/** Initialize the tethering offload HAL. */
public boolean initOffloadControl(ControlCallback controlCb) {
mControlCallback = controlCb;
if (mOffloadControl == null) {
try {
mOffloadControl = IOffloadControl.getService();
} catch (RemoteException e) {
mLog.e("tethering offload control not supported: " + e);
return false;
}
if (mOffloadControl == null) {
mLog.e("tethering IOffloadControl.getService() returned null");
return false;
}
}
final String logmsg = String.format("initOffloadControl(%s)",
(controlCb == null) ? "null"
: "0x" + Integer.toHexString(System.identityHashCode(controlCb)));
mTetheringOffloadCallback = new TetheringOffloadCallback(mHandler, mControlCallback, mLog);
final CbResults results = new CbResults();
try {
mOffloadControl.initOffload(
mTetheringOffloadCallback,
(boolean success, String errMsg) -> {
results.mSuccess = success;
results.mErrMsg = errMsg;
});
} catch (RemoteException e) {
record(logmsg, e);
return false;
}
record(logmsg, results);
return results.mSuccess;
}
/** Stop IOffloadControl. */
public void stopOffloadControl() {
if (mOffloadControl != null) {
try {
mOffloadControl.stopOffload(
(boolean success, String errMsg) -> {
if (!success) mLog.e("stopOffload failed: " + errMsg);
});
} catch (RemoteException e) {
mLog.e("failed to stopOffload: " + e);
}
}
mOffloadControl = null;
mTetheringOffloadCallback = null;
mControlCallback = null;
mLog.log("stopOffloadControl()");
}
/** Get Tx/Rx usage from last query. */
public ForwardedStats getForwardedStats(String upstream) {
final String logmsg = String.format("getForwardedStats(%s)", upstream);
final ForwardedStats stats = new ForwardedStats();
try {
mOffloadControl.getForwardedStats(
upstream,
(long rxBytes, long txBytes) -> {
stats.rxBytes = (rxBytes > 0) ? rxBytes : 0;
stats.txBytes = (txBytes > 0) ? txBytes : 0;
});
} catch (RemoteException e) {
record(logmsg, e);
return stats;
}
mLog.log(logmsg + YIELDS + stats);
return stats;
}
/** Set local prefixes to offload management process. */
public boolean setLocalPrefixes(ArrayList<String> localPrefixes) {
final String logmsg = String.format("setLocalPrefixes([%s])",
String.join(",", localPrefixes));
final CbResults results = new CbResults();
try {
mOffloadControl.setLocalPrefixes(localPrefixes,
(boolean success, String errMsg) -> {
results.mSuccess = success;
results.mErrMsg = errMsg;
});
} catch (RemoteException e) {
record(logmsg, e);
return false;
}
record(logmsg, results);
return results.mSuccess;
}
/** Set data limit value to offload management process. */
public boolean setDataLimit(String iface, long limit) {
final String logmsg = String.format("setDataLimit(%s, %d)", iface, limit);
final CbResults results = new CbResults();
try {
mOffloadControl.setDataLimit(
iface, limit,
(boolean success, String errMsg) -> {
results.mSuccess = success;
results.mErrMsg = errMsg;
});
} catch (RemoteException e) {
record(logmsg, e);
return false;
}
record(logmsg, results);
return results.mSuccess;
}
/** Set upstream parameters to offload management process. */
public boolean setUpstreamParameters(
String iface, String v4addr, String v4gateway, ArrayList<String> v6gws) {
iface = (iface != null) ? iface : NO_INTERFACE_NAME;
v4addr = (v4addr != null) ? v4addr : NO_IPV4_ADDRESS;
v4gateway = (v4gateway != null) ? v4gateway : NO_IPV4_GATEWAY;
v6gws = (v6gws != null) ? v6gws : new ArrayList<>();
final String logmsg = String.format("setUpstreamParameters(%s, %s, %s, [%s])",
iface, v4addr, v4gateway, String.join(",", v6gws));
final CbResults results = new CbResults();
try {
mOffloadControl.setUpstreamParameters(
iface, v4addr, v4gateway, v6gws,
(boolean success, String errMsg) -> {
results.mSuccess = success;
results.mErrMsg = errMsg;
});
} catch (RemoteException e) {
record(logmsg, e);
return false;
}
record(logmsg, results);
return results.mSuccess;
}
/** Add downstream prefix to offload management process. */
public boolean addDownstreamPrefix(String ifname, String prefix) {
final String logmsg = String.format("addDownstreamPrefix(%s, %s)", ifname, prefix);
final CbResults results = new CbResults();
try {
mOffloadControl.addDownstream(ifname, prefix,
(boolean success, String errMsg) -> {
results.mSuccess = success;
results.mErrMsg = errMsg;
});
} catch (RemoteException e) {
record(logmsg, e);
return false;
}
record(logmsg, results);
return results.mSuccess;
}
/** Remove downstream prefix from offload management process. */
public boolean removeDownstreamPrefix(String ifname, String prefix) {
final String logmsg = String.format("removeDownstreamPrefix(%s, %s)", ifname, prefix);
final CbResults results = new CbResults();
try {
mOffloadControl.removeDownstream(ifname, prefix,
(boolean success, String errMsg) -> {
results.mSuccess = success;
results.mErrMsg = errMsg;
});
} catch (RemoteException e) {
record(logmsg, e);
return false;
}
record(logmsg, results);
return results.mSuccess;
}
private void record(String msg, Throwable t) {
mLog.e(msg + YIELDS + "exception: " + t);
}
private void record(String msg, CbResults results) {
final String logmsg = msg + YIELDS + results;
if (!results.mSuccess) {
mLog.e(logmsg);
} else {
mLog.log(logmsg);
}
}
private static class TetheringOffloadCallback extends ITetheringOffloadCallback.Stub {
public final Handler handler;
public final ControlCallback controlCb;
public final SharedLog log;
TetheringOffloadCallback(Handler h, ControlCallback cb, SharedLog sharedLog) {
handler = h;
controlCb = cb;
log = sharedLog;
}
@Override
public void onEvent(int event) {
handler.post(() -> {
switch (event) {
case OffloadCallbackEvent.OFFLOAD_STARTED:
controlCb.onStarted();
break;
case OffloadCallbackEvent.OFFLOAD_STOPPED_ERROR:
controlCb.onStoppedError();
break;
case OffloadCallbackEvent.OFFLOAD_STOPPED_UNSUPPORTED:
controlCb.onStoppedUnsupported();
break;
case OffloadCallbackEvent.OFFLOAD_SUPPORT_AVAILABLE:
controlCb.onSupportAvailable();
break;
case OffloadCallbackEvent.OFFLOAD_STOPPED_LIMIT_REACHED:
controlCb.onStoppedLimitReached();
break;
default:
log.e("Unsupported OffloadCallbackEvent: " + event);
}
});
}
@Override
public void updateTimeout(NatTimeoutUpdate params) {
handler.post(() -> {
controlCb.onNatTimeoutUpdate(
networkProtocolToOsConstant(params.proto),
params.src.addr, uint16(params.src.port),
params.dst.addr, uint16(params.dst.port));
});
}
}
private static int networkProtocolToOsConstant(int proto) {
switch (proto) {
case NetworkProtocol.TCP: return OsConstants.IPPROTO_TCP;
case NetworkProtocol.UDP: return OsConstants.IPPROTO_UDP;
default:
// The caller checks this value and will log an error. Just make
// sure it won't collide with valid OsContants.IPPROTO_* values.
return -Math.abs(proto);
}
}
private static class CbResults {
boolean mSuccess;
String mErrMsg;
@Override
public String toString() {
if (mSuccess) {
return "ok";
} else {
return "fail: " + mErrMsg;
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,415 @@
/*
* Copyright (C) 2017 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.networkstack.tethering;
import static android.content.Context.TELEPHONY_SERVICE;
import static android.net.ConnectivityManager.TYPE_ETHERNET;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import android.content.Context;
import android.content.res.Resources;
import android.net.TetheringConfigurationParcel;
import android.net.util.SharedLog;
import android.provider.DeviceConfig;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.StringJoiner;
/**
* A utility class to encapsulate the various tethering configuration elements.
*
* This configuration data includes elements describing upstream properties
* (preferred and required types of upstream connectivity as well as default
* DNS servers to use if none are available) and downstream properties (such
* as regular expressions use to match suitable downstream interfaces and the
* DHCPv4 ranges to use).
*
* @hide
*/
public class TetheringConfiguration {
private static final String TAG = TetheringConfiguration.class.getSimpleName();
private static final String[] EMPTY_STRING_ARRAY = new String[0];
// Default ranges used for the legacy DHCP server.
// USB is 192.168.42.1 and 255.255.255.0
// Wifi is 192.168.43.1 and 255.255.255.0
// BT is limited to max default of 5 connections. 192.168.44.1 to 192.168.48.1
// with 255.255.255.0
// P2P is 192.168.49.1 and 255.255.255.0
private static final String[] LEGACY_DHCP_DEFAULT_RANGE = {
"192.168.42.2", "192.168.42.254", "192.168.43.2", "192.168.43.254",
"192.168.44.2", "192.168.44.254", "192.168.45.2", "192.168.45.254",
"192.168.46.2", "192.168.46.254", "192.168.47.2", "192.168.47.254",
"192.168.48.2", "192.168.48.254", "192.168.49.2", "192.168.49.254",
};
private static final String[] DEFAULT_IPV4_DNS = {"8.8.4.4", "8.8.8.8"};
/**
* Use the old dnsmasq DHCP server for tethering instead of the framework implementation.
*/
public static final String TETHER_ENABLE_LEGACY_DHCP_SERVER =
"tether_enable_legacy_dhcp_server";
public final String[] tetherableUsbRegexs;
public final String[] tetherableWifiRegexs;
public final String[] tetherableWifiP2pRegexs;
public final String[] tetherableBluetoothRegexs;
public final String[] tetherableNcmRegexs;
public final boolean isDunRequired;
public final boolean chooseUpstreamAutomatically;
public final Collection<Integer> preferredUpstreamIfaceTypes;
public final String[] legacyDhcpRanges;
public final String[] defaultIPv4DNS;
public final boolean enableLegacyDhcpServer;
public final String[] provisioningApp;
public final String provisioningAppNoUi;
public final int provisioningCheckPeriod;
public final int activeDataSubId;
public TetheringConfiguration(Context ctx, SharedLog log, int id) {
final SharedLog configLog = log.forSubComponent("config");
activeDataSubId = id;
Resources res = getResources(ctx, activeDataSubId);
tetherableUsbRegexs = getResourceStringArray(res, R.array.config_tether_usb_regexs);
tetherableNcmRegexs = getResourceStringArray(res, R.array.config_tether_ncm_regexs);
// TODO: Evaluate deleting this altogether now that Wi-Fi always passes
// us an interface name. Careful consideration needs to be given to
// implications for Settings and for provisioning checks.
tetherableWifiRegexs = getResourceStringArray(res, R.array.config_tether_wifi_regexs);
tetherableWifiP2pRegexs = getResourceStringArray(
res, R.array.config_tether_wifi_p2p_regexs);
tetherableBluetoothRegexs = getResourceStringArray(
res, R.array.config_tether_bluetooth_regexs);
isDunRequired = checkDunRequired(ctx);
chooseUpstreamAutomatically = getResourceBoolean(
res, R.bool.config_tether_upstream_automatic);
preferredUpstreamIfaceTypes = getUpstreamIfaceTypes(res, isDunRequired);
legacyDhcpRanges = getLegacyDhcpRanges(res);
defaultIPv4DNS = copy(DEFAULT_IPV4_DNS);
enableLegacyDhcpServer = getEnableLegacyDhcpServer(res);
provisioningApp = getResourceStringArray(res, R.array.config_mobile_hotspot_provision_app);
provisioningAppNoUi = getProvisioningAppNoUi(res);
provisioningCheckPeriod = getResourceInteger(res,
R.integer.config_mobile_hotspot_provision_check_period,
0 /* No periodic re-check */);
configLog.log(toString());
}
/** Check whether input interface belong to usb.*/
public boolean isUsb(String iface) {
return matchesDownstreamRegexs(iface, tetherableUsbRegexs);
}
/** Check whether input interface belong to wifi.*/
public boolean isWifi(String iface) {
return matchesDownstreamRegexs(iface, tetherableWifiRegexs);
}
/** Check whether this interface is Wifi P2P interface. */
public boolean isWifiP2p(String iface) {
return matchesDownstreamRegexs(iface, tetherableWifiP2pRegexs);
}
/** Check whether using legacy mode for wifi P2P. */
public boolean isWifiP2pLegacyTetheringMode() {
return (tetherableWifiP2pRegexs == null || tetherableWifiP2pRegexs.length == 0);
}
/** Check whether input interface belong to bluetooth.*/
public boolean isBluetooth(String iface) {
return matchesDownstreamRegexs(iface, tetherableBluetoothRegexs);
}
/** Check if interface is ncm */
public boolean isNcm(String iface) {
return matchesDownstreamRegexs(iface, tetherableNcmRegexs);
}
/** Check whether no ui entitlement application is available.*/
public boolean hasMobileHotspotProvisionApp() {
return !TextUtils.isEmpty(provisioningAppNoUi);
}
/** Does the dumping.*/
public void dump(PrintWriter pw) {
pw.print("activeDataSubId: ");
pw.println(activeDataSubId);
dumpStringArray(pw, "tetherableUsbRegexs", tetherableUsbRegexs);
dumpStringArray(pw, "tetherableWifiRegexs", tetherableWifiRegexs);
dumpStringArray(pw, "tetherableWifiP2pRegexs", tetherableWifiP2pRegexs);
dumpStringArray(pw, "tetherableBluetoothRegexs", tetherableBluetoothRegexs);
dumpStringArray(pw, "tetherableNcmRegexs", tetherableNcmRegexs);
pw.print("isDunRequired: ");
pw.println(isDunRequired);
pw.print("chooseUpstreamAutomatically: ");
pw.println(chooseUpstreamAutomatically);
pw.print("legacyPreredUpstreamIfaceTypes: ");
pw.println(Arrays.toString(toIntArray(preferredUpstreamIfaceTypes)));
dumpStringArray(pw, "legacyDhcpRanges", legacyDhcpRanges);
dumpStringArray(pw, "defaultIPv4DNS", defaultIPv4DNS);
dumpStringArray(pw, "provisioningApp", provisioningApp);
pw.print("provisioningAppNoUi: ");
pw.println(provisioningAppNoUi);
pw.print("enableLegacyDhcpServer: ");
pw.println(enableLegacyDhcpServer);
}
/** Returns the string representation of this object.*/
public String toString() {
final StringJoiner sj = new StringJoiner(" ");
sj.add(String.format("activeDataSubId:%d", activeDataSubId));
sj.add(String.format("tetherableUsbRegexs:%s", makeString(tetherableUsbRegexs)));
sj.add(String.format("tetherableWifiRegexs:%s", makeString(tetherableWifiRegexs)));
sj.add(String.format("tetherableWifiP2pRegexs:%s", makeString(tetherableWifiP2pRegexs)));
sj.add(String.format("tetherableBluetoothRegexs:%s",
makeString(tetherableBluetoothRegexs)));
sj.add(String.format("isDunRequired:%s", isDunRequired));
sj.add(String.format("chooseUpstreamAutomatically:%s", chooseUpstreamAutomatically));
sj.add(String.format("preferredUpstreamIfaceTypes:%s",
toIntArray(preferredUpstreamIfaceTypes)));
sj.add(String.format("provisioningApp:%s", makeString(provisioningApp)));
sj.add(String.format("provisioningAppNoUi:%s", provisioningAppNoUi));
sj.add(String.format("enableLegacyDhcpServer:%s", enableLegacyDhcpServer));
return String.format("TetheringConfiguration{%s}", sj.toString());
}
private static void dumpStringArray(PrintWriter pw, String label, String[] values) {
pw.print(label);
pw.print(": ");
if (values != null) {
final StringJoiner sj = new StringJoiner(", ", "[", "]");
for (String value : values) sj.add(value);
pw.print(sj.toString());
} else {
pw.print("null");
}
pw.println();
}
private static String makeString(String[] strings) {
if (strings == null) return "null";
final StringJoiner sj = new StringJoiner(",", "[", "]");
for (String s : strings) sj.add(s);
return sj.toString();
}
/** Check whether dun is required. */
public static boolean checkDunRequired(Context ctx) {
final TelephonyManager tm = (TelephonyManager) ctx.getSystemService(TELEPHONY_SERVICE);
// TelephonyManager would uses the active data subscription, which should be the one used
// by tethering.
return (tm != null) ? tm.isTetheringApnRequired() : false;
}
private static Collection<Integer> getUpstreamIfaceTypes(Resources res, boolean dunRequired) {
final int[] ifaceTypes = res.getIntArray(R.array.config_tether_upstream_types);
final ArrayList<Integer> upstreamIfaceTypes = new ArrayList<>(ifaceTypes.length);
for (int i : ifaceTypes) {
switch (i) {
case TYPE_MOBILE:
case TYPE_MOBILE_HIPRI:
if (dunRequired) continue;
break;
case TYPE_MOBILE_DUN:
if (!dunRequired) continue;
break;
}
upstreamIfaceTypes.add(i);
}
// Fix up upstream interface types for DUN or mobile. NOTE: independent
// of the value of |dunRequired|, cell data of one form or another is
// *always* an upstream, regardless of the upstream interface types
// specified by configuration resources.
if (dunRequired) {
appendIfNotPresent(upstreamIfaceTypes, TYPE_MOBILE_DUN);
} else {
// Do not modify if a cellular interface type is already present in the
// upstream interface types. Add TYPE_MOBILE and TYPE_MOBILE_HIPRI if no
// cellular interface types are found in the upstream interface types.
// This preserves backwards compatibility and prevents the DUN and default
// mobile types incorrectly appearing together, which could happen on
// previous releases in the common case where checkDunRequired returned
// DUN_UNSPECIFIED.
if (!containsOneOf(upstreamIfaceTypes, TYPE_MOBILE, TYPE_MOBILE_HIPRI)) {
upstreamIfaceTypes.add(TYPE_MOBILE);
upstreamIfaceTypes.add(TYPE_MOBILE_HIPRI);
}
}
// Always make sure our good friend Ethernet is present.
// TODO: consider unilaterally forcing this at the front.
prependIfNotPresent(upstreamIfaceTypes, TYPE_ETHERNET);
return upstreamIfaceTypes;
}
private static boolean matchesDownstreamRegexs(String iface, String[] regexs) {
for (String regex : regexs) {
if (iface.matches(regex)) return true;
}
return false;
}
private static String[] getLegacyDhcpRanges(Resources res) {
final String[] fromResource = getResourceStringArray(res, R.array.config_tether_dhcp_range);
if ((fromResource.length > 0) && (fromResource.length % 2 == 0)) {
return fromResource;
}
return copy(LEGACY_DHCP_DEFAULT_RANGE);
}
private static String getProvisioningAppNoUi(Resources res) {
try {
return res.getString(R.string.config_mobile_hotspot_provision_app_no_ui);
} catch (Resources.NotFoundException e) {
return "";
}
}
private static boolean getResourceBoolean(Resources res, int resId) {
try {
return res.getBoolean(resId);
} catch (Resources.NotFoundException e404) {
return false;
}
}
private static String[] getResourceStringArray(Resources res, int resId) {
try {
final String[] strArray = res.getStringArray(resId);
return (strArray != null) ? strArray : EMPTY_STRING_ARRAY;
} catch (Resources.NotFoundException e404) {
return EMPTY_STRING_ARRAY;
}
}
private static int getResourceInteger(Resources res, int resId, int defaultValue) {
try {
return res.getInteger(resId);
} catch (Resources.NotFoundException e404) {
return defaultValue;
}
}
private boolean getEnableLegacyDhcpServer(final Resources res) {
return getResourceBoolean(res, R.bool.config_tether_enable_legacy_dhcp_server)
|| getDeviceConfigBoolean(TETHER_ENABLE_LEGACY_DHCP_SERVER);
}
@VisibleForTesting
protected boolean getDeviceConfigBoolean(final String name) {
return DeviceConfig.getBoolean(NAMESPACE_CONNECTIVITY, name, false /** defaultValue */);
}
private Resources getResources(Context ctx, int subId) {
if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
return getResourcesForSubIdWrapper(ctx, subId);
} else {
return ctx.getResources();
}
}
@VisibleForTesting
protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) {
return SubscriptionManager.getResourcesForSubId(ctx, subId);
}
private static String[] copy(String[] strarray) {
return Arrays.copyOf(strarray, strarray.length);
}
private static void prependIfNotPresent(ArrayList<Integer> list, int value) {
if (list.contains(value)) return;
list.add(0, value);
}
private static void appendIfNotPresent(ArrayList<Integer> list, int value) {
if (list.contains(value)) return;
list.add(value);
}
private static boolean containsOneOf(ArrayList<Integer> list, Integer... values) {
for (Integer value : values) {
if (list.contains(value)) return true;
}
return false;
}
private static int[] toIntArray(Collection<Integer> values) {
final int[] result = new int[values.size()];
int index = 0;
for (Integer value : values) {
result[index++] = value;
}
return result;
}
/**
* Convert this TetheringConfiguration to a TetheringConfigurationParcel.
*/
public TetheringConfigurationParcel toStableParcelable() {
final TetheringConfigurationParcel parcel = new TetheringConfigurationParcel();
parcel.subId = activeDataSubId;
parcel.tetherableUsbRegexs = tetherableUsbRegexs;
parcel.tetherableWifiRegexs = tetherableWifiRegexs;
parcel.tetherableBluetoothRegexs = tetherableBluetoothRegexs;
parcel.isDunRequired = isDunRequired;
parcel.chooseUpstreamAutomatically = chooseUpstreamAutomatically;
parcel.preferredUpstreamIfaceTypes = toIntArray(preferredUpstreamIfaceTypes);
parcel.legacyDhcpRanges = legacyDhcpRanges;
parcel.defaultIPv4DNS = defaultIPv4DNS;
parcel.enableLegacyDhcpServer = enableLegacyDhcpServer;
parcel.provisioningApp = provisioningApp;
parcel.provisioningAppNoUi = provisioningAppNoUi;
parcel.provisioningCheckPeriod = provisioningCheckPeriod;
return parcel;
}
}

View file

@ -0,0 +1,127 @@
/*
* Copyright (C) 2017 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.networkstack.tethering;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.net.INetd;
import android.net.NetworkRequest;
import android.net.ip.IpServer;
import android.net.util.SharedLog;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import androidx.annotation.NonNull;
import com.android.internal.util.StateMachine;
import java.util.ArrayList;
/**
* Capture tethering dependencies, for injection.
*
* @hide
*/
public abstract class TetheringDependencies {
/**
* Get a reference to the offload hardware interface to be used by tethering.
*/
public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
return new OffloadHardwareInterface(h, log);
}
/**
* Get a reference to the UpstreamNetworkMonitor to be used by tethering.
*/
public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, StateMachine target,
SharedLog log, int what) {
return new UpstreamNetworkMonitor(ctx, target, log, what);
}
/**
* Get a reference to the IPv6TetheringCoordinator to be used by tethering.
*/
public IPv6TetheringCoordinator getIPv6TetheringCoordinator(
ArrayList<IpServer> notifyList, SharedLog log) {
return new IPv6TetheringCoordinator(notifyList, log);
}
/**
* Get dependencies to be used by IpServer.
*/
public abstract IpServer.Dependencies getIpServerDependencies();
/**
* Indicates whether tethering is supported on the device.
*/
public boolean isTetheringSupported() {
return true;
}
/**
* Get the NetworkRequest that should be fulfilled by the default network.
*/
public abstract NetworkRequest getDefaultNetworkRequest();
/**
* Get a reference to the EntitlementManager to be used by tethering.
*/
public EntitlementManager getEntitlementManager(Context ctx, StateMachine target,
SharedLog log, int what) {
return new EntitlementManager(ctx, target, log, what);
}
/**
* Generate a new TetheringConfiguration according to input sub Id.
*/
public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
int subId) {
return new TetheringConfiguration(ctx, log, subId);
}
/**
* Get a reference to INetd to be used by tethering.
*/
public INetd getINetd(Context context) {
return INetd.Stub.asInterface(
(IBinder) context.getSystemService(Context.NETD_SERVICE));
}
/**
* Get a reference to the TetheringNotificationUpdater to be used by tethering.
*/
public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx) {
return new TetheringNotificationUpdater(ctx);
}
/**
* Get tethering thread looper.
*/
public abstract Looper getTetheringLooper();
/**
* Get Context of TetheringSerice.
*/
public abstract Context getContext();
/**
* Get a reference to BluetoothAdapter to be used by tethering.
*/
public abstract BluetoothAdapter getBluetoothAdapter();
}

View file

@ -0,0 +1,102 @@
/*
* Copyright (C) 2018 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.networkstack.tethering;
import android.annotation.Nullable;
import android.net.LinkProperties;
import android.net.NetworkCapabilities;
import android.net.RouteInfo;
import android.net.util.InterfaceSet;
import com.android.net.module.util.NetUtils;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @hide
*/
public final class TetheringInterfaceUtils {
private static final InetAddress IN6ADDR_ANY = getByAddress(
new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
private static final InetAddress INADDR_ANY = getByAddress(new byte[] {0, 0, 0, 0});
/**
* Get upstream interfaces for tethering based on default routes for IPv4/IPv6.
* @return null if there is no usable interface, or a set of at least one interface otherwise.
*/
public static @Nullable InterfaceSet getTetheringInterfaces(UpstreamNetworkState ns) {
if (ns == null) {
return null;
}
final LinkProperties lp = ns.linkProperties;
final String if4 = getInterfaceForDestination(lp, INADDR_ANY);
final String if6 = getIPv6Interface(ns);
return (if4 == null && if6 == null) ? null : new InterfaceSet(if4, if6);
}
/**
* Get the upstream interface for IPv6 tethering.
* @return null if there is no usable interface, or the interface name otherwise.
*/
public static @Nullable String getIPv6Interface(UpstreamNetworkState ns) {
// Broadly speaking:
//
// [1] does the upstream have an IPv6 default route?
//
// and
//
// [2] does the upstream have one or more global IPv6 /64s
// dedicated to this device?
//
// In lieu of Prefix Delegation and other evaluation of whether a
// prefix may or may not be dedicated to this device, for now just
// check whether the upstream is TRANSPORT_CELLULAR. This works
// because "[t]he 3GPP network allocates each default bearer a unique
// /64 prefix", per RFC 6459, Section 5.2.
final boolean canTether =
(ns != null) && (ns.network != null)
&& (ns.linkProperties != null) && (ns.networkCapabilities != null)
// At least one upstream DNS server:
&& ns.linkProperties.hasIpv6DnsServer()
// Minimal amount of IPv6 provisioning:
&& ns.linkProperties.hasGlobalIpv6Address()
// Temporary approximation of "dedicated prefix":
&& ns.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
return canTether
? getInterfaceForDestination(ns.linkProperties, IN6ADDR_ANY)
: null;
}
private static String getInterfaceForDestination(LinkProperties lp, InetAddress dst) {
final RouteInfo ri = (lp != null)
? NetUtils.selectBestRoute(lp.getAllRoutes(), dst)
: null;
return (ri != null) ? ri.getInterface() : null;
}
private static InetAddress getByAddress(final byte[] addr) {
try {
return InetAddress.getByAddress(null, addr);
} catch (UnknownHostException e) {
throw new AssertionError("illegal address length" + addr.length);
}
}
}

View file

@ -0,0 +1,243 @@
/*
* 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.networkstack.tethering;
import static android.net.TetheringManager.TETHERING_BLUETOOTH;
import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.UserHandle;
import android.provider.Settings;
import android.telephony.SubscriptionManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import androidx.annotation.ArrayRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
/**
* A class to display tethering-related notifications.
*
* <p>This class is not thread safe, it is intended to be used only from the tethering handler
* thread. However the constructor is an exception, as it is called on another thread ;
* therefore for thread safety all members of this class MUST either be final or initialized
* to their default value (0, false or null).
*
* @hide
*/
public class TetheringNotificationUpdater {
private static final String TAG = TetheringNotificationUpdater.class.getSimpleName();
private static final String CHANNEL_ID = "TETHERING_STATUS";
private static final String WIFI_DOWNSTREAM = "WIFI";
private static final String USB_DOWNSTREAM = "USB";
private static final String BLUETOOTH_DOWNSTREAM = "BT";
private static final boolean NOTIFY_DONE = true;
private static final boolean NO_NOTIFY = false;
// Id to update and cancel tethering notification. Must be unique within the tethering app.
private static final int ENABLE_NOTIFICATION_ID = 1000;
// Id to update and cancel restricted notification. Must be unique within the tethering app.
private static final int RESTRICTED_NOTIFICATION_ID = 1001;
@VisibleForTesting
static final int NO_ICON_ID = 0;
@VisibleForTesting
static final int DOWNSTREAM_NONE = 0;
private final Context mContext;
private final NotificationManager mNotificationManager;
private final NotificationChannel mChannel;
// WARNING : the constructor is called on a different thread. Thread safety therefore
// relies on this value being initialized to 0, and not any other value. If you need
// to change this, you will need to change the thread where the constructor is invoked,
// or to introduce synchronization.
// Downstream type is one of ConnectivityManager.TETHERING_* constants, 0 1 or 2.
// This value has to be made 1 2 and 4, and OR'd with the others.
private int mDownstreamTypesMask = DOWNSTREAM_NONE;
// WARNING : this value is not able to being initialized to 0 and must have volatile because
// telephony service is not guaranteed that is up before tethering service starts. If telephony
// is up later than tethering, TetheringNotificationUpdater will use incorrect and valid
// subscription id(0) to query resources. Therefore, initialized subscription id must be
// INVALID_SUBSCRIPTION_ID.
private volatile int mActiveDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@IntDef({ENABLE_NOTIFICATION_ID, RESTRICTED_NOTIFICATION_ID})
@interface NotificationId {}
public TetheringNotificationUpdater(@NonNull final Context context) {
mContext = context;
mNotificationManager = (NotificationManager) context.createContextAsUser(UserHandle.ALL, 0)
.getSystemService(Context.NOTIFICATION_SERVICE);
mChannel = new NotificationChannel(
CHANNEL_ID,
context.getResources().getString(R.string.notification_channel_tethering_status),
NotificationManager.IMPORTANCE_LOW);
mNotificationManager.createNotificationChannel(mChannel);
}
/** Called when downstream has changed */
public void onDownstreamChanged(@IntRange(from = 0, to = 7) final int downstreamTypesMask) {
if (mDownstreamTypesMask == downstreamTypesMask) return;
mDownstreamTypesMask = downstreamTypesMask;
updateEnableNotification();
}
/** Called when active data subscription id changed */
public void onActiveDataSubscriptionIdChanged(final int subId) {
if (mActiveDataSubId == subId) return;
mActiveDataSubId = subId;
updateEnableNotification();
}
@VisibleForTesting
Resources getResourcesForSubId(@NonNull final Context c, final int subId) {
return SubscriptionManager.getResourcesForSubId(c, subId);
}
private void updateEnableNotification() {
final boolean tetheringInactive = mDownstreamTypesMask <= DOWNSTREAM_NONE;
if (tetheringInactive || setupNotification() == NO_NOTIFY) {
clearNotification(ENABLE_NOTIFICATION_ID);
}
}
@VisibleForTesting
void tetheringRestrictionLifted() {
clearNotification(RESTRICTED_NOTIFICATION_ID);
}
private void clearNotification(@NotificationId final int id) {
mNotificationManager.cancel(null /* tag */, id);
}
@VisibleForTesting
void notifyTetheringDisabledByRestriction() {
final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
final String title = res.getString(R.string.disable_tether_notification_title);
final String message = res.getString(R.string.disable_tether_notification_message);
showNotification(R.drawable.stat_sys_tether_general, title, message,
RESTRICTED_NOTIFICATION_ID);
}
/**
* Returns the downstream types mask which convert from given string.
*
* @param types This string has to be made by "WIFI", "USB", "BT", and OR'd with the others.
*
* @return downstream types mask value.
*/
@VisibleForTesting
@IntRange(from = 0, to = 7)
int getDownstreamTypesMask(@NonNull final String types) {
int downstreamTypesMask = DOWNSTREAM_NONE;
final String[] downstreams = types.split("\\|");
for (String downstream : downstreams) {
if (USB_DOWNSTREAM.equals(downstream.trim())) {
downstreamTypesMask |= (1 << TETHERING_USB);
} else if (WIFI_DOWNSTREAM.equals(downstream.trim())) {
downstreamTypesMask |= (1 << TETHERING_WIFI);
} else if (BLUETOOTH_DOWNSTREAM.equals(downstream.trim())) {
downstreamTypesMask |= (1 << TETHERING_BLUETOOTH);
}
}
return downstreamTypesMask;
}
/**
* Returns the icons {@link android.util.SparseArray} which get from given string-array resource
* id.
*
* @param id String-array resource id
*
* @return {@link android.util.SparseArray} with downstream types and icon id info.
*/
@VisibleForTesting
SparseArray<Integer> getIcons(@ArrayRes int id, @NonNull Resources res) {
final String[] array = res.getStringArray(id);
final SparseArray<Integer> icons = new SparseArray<>();
for (String config : array) {
if (TextUtils.isEmpty(config)) continue;
final String[] elements = config.split(";");
if (elements.length != 2) {
Log.wtf(TAG,
"Unexpected format in Tethering notification configuration : " + config);
continue;
}
final String[] types = elements[0].split(",");
for (String type : types) {
int mask = getDownstreamTypesMask(type);
if (mask == DOWNSTREAM_NONE) continue;
icons.put(mask, res.getIdentifier(
elements[1].trim(), null /* defType */, null /* defPackage */));
}
}
return icons;
}
private boolean setupNotification() {
final Resources res = getResourcesForSubId(mContext, mActiveDataSubId);
final SparseArray<Integer> downstreamIcons =
getIcons(R.array.tethering_notification_icons, res);
final int iconId = downstreamIcons.get(mDownstreamTypesMask, NO_ICON_ID);
if (iconId == NO_ICON_ID) return NO_NOTIFY;
final String title = res.getString(R.string.tethering_notification_title);
final String message = res.getString(R.string.tethering_notification_message);
showNotification(iconId, title, message, ENABLE_NOTIFICATION_ID);
return NOTIFY_DONE;
}
private void showNotification(@DrawableRes final int iconId, @NonNull final String title,
@NonNull final String message, @NotificationId final int id) {
final Intent intent = new Intent(Settings.ACTION_TETHER_SETTINGS);
final PendingIntent pi = PendingIntent.getActivity(
mContext.createContextAsUser(UserHandle.CURRENT, 0),
0 /* requestCode */, intent, 0 /* flags */, null /* options */);
final Notification notification =
new Notification.Builder(mContext, mChannel.getId())
.setSmallIcon(iconId)
.setContentTitle(title)
.setContentText(message)
.setOngoing(true)
.setColor(mContext.getColor(
android.R.color.system_notification_accent_color))
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setCategory(Notification.CATEGORY_STATUS)
.setContentIntent(pi)
.build();
mNotificationManager.notify(null /* tag */, id, notification);
}
}

View file

@ -0,0 +1,391 @@
/*
* Copyright (C) 2019 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.networkstack.tethering;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION;
import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED;
import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.Intent;
import android.net.IIntResultListener;
import android.net.INetworkStackConnector;
import android.net.ITetheringConnector;
import android.net.ITetheringEventCallback;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.NetworkStack;
import android.net.TetheringRequestParcel;
import android.net.dhcp.DhcpServerCallbacks;
import android.net.dhcp.DhcpServingParamsParcel;
import android.net.ip.IpServer;
import android.net.util.SharedLog;
import android.os.Binder;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.SystemProperties;
import android.os.UserManager;
import android.provider.Settings;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import java.io.FileDescriptor;
import java.io.PrintWriter;
/**
* Android service used to manage tethering.
*
* <p>The service returns a binder for the system server to communicate with the tethering.
*/
public class TetheringService extends Service {
private static final String TAG = TetheringService.class.getSimpleName();
private final SharedLog mLog = new SharedLog(TAG);
private TetheringConnector mConnector;
private Context mContext;
private TetheringDependencies mDeps;
private Tethering mTethering;
private UserManager mUserManager;
@Override
public void onCreate() {
mLog.mark("onCreate");
mDeps = getTetheringDependencies();
mContext = mDeps.getContext();
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
mTethering = makeTethering(mDeps);
mTethering.startStateMachineUpdaters();
}
/**
* Make a reference to Tethering object.
*/
@VisibleForTesting
public Tethering makeTethering(TetheringDependencies deps) {
System.loadLibrary("tetherutilsjni");
return new Tethering(deps);
}
/**
* Create a binder connector for the system server to communicate with the tethering.
*/
private synchronized IBinder makeConnector() {
if (mConnector == null) {
mConnector = new TetheringConnector(mTethering, TetheringService.this);
}
return mConnector;
}
@NonNull
@Override
public IBinder onBind(Intent intent) {
mLog.mark("onBind");
return makeConnector();
}
private static class TetheringConnector extends ITetheringConnector.Stub {
private final TetheringService mService;
private final Tethering mTethering;
TetheringConnector(Tethering tether, TetheringService service) {
mTethering = tether;
mService = service;
}
@Override
public void tether(String iface, String callerPkg, IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, listener)) return;
try {
listener.onResult(mTethering.tether(iface));
} catch (RemoteException e) { }
}
@Override
public void untether(String iface, String callerPkg, IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, listener)) return;
try {
listener.onResult(mTethering.untether(iface));
} catch (RemoteException e) { }
}
@Override
public void setUsbTethering(boolean enable, String callerPkg, IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, listener)) return;
try {
listener.onResult(mTethering.setUsbTethering(enable));
} catch (RemoteException e) { }
}
@Override
public void startTethering(TetheringRequestParcel request, String callerPkg,
IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, listener)) return;
mTethering.startTethering(request, listener);
}
@Override
public void stopTethering(int type, String callerPkg, IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, listener)) return;
try {
mTethering.stopTethering(type);
listener.onResult(TETHER_ERROR_NO_ERROR);
} catch (RemoteException e) { }
}
@Override
public void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver,
boolean showEntitlementUi, String callerPkg) {
if (checkAndNotifyCommonError(callerPkg, receiver)) return;
mTethering.requestLatestTetheringEntitlementResult(type, receiver, showEntitlementUi);
}
@Override
public void registerTetheringEventCallback(ITetheringEventCallback callback,
String callerPkg) {
try {
if (!mService.hasTetherAccessPermission()) {
callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
return;
}
mTethering.registerTetheringEventCallback(callback);
} catch (RemoteException e) { }
}
@Override
public void unregisterTetheringEventCallback(ITetheringEventCallback callback,
String callerPkg) {
try {
if (!mService.hasTetherAccessPermission()) {
callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION);
return;
}
mTethering.unregisterTetheringEventCallback(callback);
} catch (RemoteException e) { }
}
@Override
public void stopAllTethering(String callerPkg, IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, listener)) return;
try {
mTethering.untetherAll();
listener.onResult(TETHER_ERROR_NO_ERROR);
} catch (RemoteException e) { }
}
@Override
public void isTetheringSupported(String callerPkg, IIntResultListener listener) {
if (checkAndNotifyCommonError(callerPkg, listener)) return;
try {
listener.onResult(TETHER_ERROR_NO_ERROR);
} catch (RemoteException e) { }
}
@Override
protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
@Nullable String[] args) {
mTethering.dump(fd, writer, args);
}
private boolean checkAndNotifyCommonError(String callerPkg, IIntResultListener listener) {
try {
if (!mService.hasTetherChangePermission(callerPkg)) {
listener.onResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION);
return true;
}
if (!mService.isTetheringSupported()) {
listener.onResult(TETHER_ERROR_UNSUPPORTED);
return true;
}
} catch (RemoteException e) {
return true;
}
return false;
}
private boolean checkAndNotifyCommonError(String callerPkg, ResultReceiver receiver) {
if (!mService.hasTetherChangePermission(callerPkg)) {
receiver.send(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION, null);
return true;
}
if (!mService.isTetheringSupported()) {
receiver.send(TETHER_ERROR_UNSUPPORTED, null);
return true;
}
return false;
}
}
// if ro.tether.denied = true we default to no tethering
// gservices could set the secure setting to 1 though to enable it on a build where it
// had previously been turned off.
private boolean isTetheringSupported() {
final int defaultVal =
SystemProperties.get("ro.tether.denied").equals("true") ? 0 : 1;
final boolean tetherSupported = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.TETHER_SUPPORTED, defaultVal) != 0;
final boolean tetherEnabledInSettings = tetherSupported
&& !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING);
return tetherEnabledInSettings && mTethering.hasTetherableConfiguration();
}
private boolean hasTetherChangePermission(String callerPkg) {
if (checkCallingOrSelfPermission(
android.Manifest.permission.TETHER_PRIVILEGED) == PERMISSION_GRANTED) {
return true;
}
if (mTethering.isTetherProvisioningRequired()) return false;
int uid = Binder.getCallingUid();
// If callerPkg's uid is not same as Binder.getCallingUid(),
// checkAndNoteWriteSettingsOperation will return false and the operation will be denied.
if (Settings.checkAndNoteWriteSettingsOperation(mContext, uid, callerPkg,
false /* throwException */)) {
return true;
}
return false;
}
private boolean hasTetherAccessPermission() {
if (checkCallingOrSelfPermission(
android.Manifest.permission.TETHER_PRIVILEGED) == PERMISSION_GRANTED) {
return true;
}
if (checkCallingOrSelfPermission(
android.Manifest.permission.ACCESS_NETWORK_STATE) == PERMISSION_GRANTED) {
return true;
}
return false;
}
/**
* An injection method for testing.
*/
@VisibleForTesting
public TetheringDependencies getTetheringDependencies() {
if (mDeps == null) {
mDeps = new TetheringDependencies() {
@Override
public NetworkRequest getDefaultNetworkRequest() {
// TODO: b/147280869, add a proper system API to replace this.
final NetworkRequest trackDefaultRequest = new NetworkRequest.Builder()
.clearCapabilities()
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build();
return trackDefaultRequest;
}
@Override
public Looper getTetheringLooper() {
final HandlerThread tetherThread = new HandlerThread("android.tethering");
tetherThread.start();
return tetherThread.getLooper();
}
@Override
public boolean isTetheringSupported() {
return TetheringService.this.isTetheringSupported();
}
@Override
public Context getContext() {
return TetheringService.this;
}
@Override
public IpServer.Dependencies getIpServerDependencies() {
return new IpServer.Dependencies() {
@Override
public void makeDhcpServer(String ifName, DhcpServingParamsParcel params,
DhcpServerCallbacks cb) {
try {
final INetworkStackConnector service = getNetworkStackConnector();
if (service == null) return;
service.makeDhcpServer(ifName, params, cb);
} catch (RemoteException e) {
Log.e(TAG, "Fail to make dhcp server");
try {
cb.onDhcpServerCreated(STATUS_UNKNOWN_ERROR, null);
} catch (RemoteException re) { }
}
}
};
}
// TODO: replace this by NetworkStackClient#getRemoteConnector after refactoring
// networkStackClient.
static final int NETWORKSTACK_TIMEOUT_MS = 60_000;
private INetworkStackConnector getNetworkStackConnector() {
IBinder connector;
try {
final long before = System.currentTimeMillis();
while ((connector = NetworkStack.getService()) == null) {
if (System.currentTimeMillis() - before > NETWORKSTACK_TIMEOUT_MS) {
Log.wtf(TAG, "Timeout, fail to get INetworkStackConnector");
return null;
}
Thread.sleep(200);
}
} catch (InterruptedException e) {
Log.wtf(TAG, "Interrupted, fail to get INetworkStackConnector");
return null;
}
return INetworkStackConnector.Stub.asInterface(connector);
}
@Override
public BluetoothAdapter getBluetoothAdapter() {
return BluetoothAdapter.getDefaultAdapter();
}
};
}
return mDeps;
}
}

View file

@ -0,0 +1,606 @@
/*
* Copyright (C) 2017 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.networkstack.tethering;
import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
import static android.net.ConnectivityManager.TYPE_ETHERNET;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.IpPrefix;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.util.PrefixUtils;
import android.net.util.SharedLog;
import android.os.Handler;
import android.util.Log;
import android.util.SparseIntArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.internal.util.StateMachine;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
/**
* A class to centralize all the network and link properties information
* pertaining to the current and any potential upstream network.
*
* The owner of UNM gets it to register network callbacks by calling the
* following methods :
* Calling #startTrackDefaultNetwork() to track the system default network.
* Calling #startObserveAllNetworks() to observe all networks. Listening all
* networks is necessary while the expression of preferred upstreams remains
* a list of legacy connectivity types. In future, this can be revisited.
* Calling #registerMobileNetworkRequest() to bring up mobile DUN/HIPRI network.
*
* The methods and data members of this class are only to be accessed and
* modified from the tethering master state machine thread. Any other
* access semantics would necessitate the addition of locking.
*
* TODO: Move upstream selection logic here.
*
* All callback methods are run on the same thread as the specified target
* state machine. This class does not require locking when accessed from this
* thread. Access from other threads is not advised.
*
* @hide
*/
public class UpstreamNetworkMonitor {
private static final String TAG = UpstreamNetworkMonitor.class.getSimpleName();
private static final boolean DBG = false;
private static final boolean VDBG = false;
public static final int EVENT_ON_CAPABILITIES = 1;
public static final int EVENT_ON_LINKPROPERTIES = 2;
public static final int EVENT_ON_LOST = 3;
public static final int NOTIFY_LOCAL_PREFIXES = 10;
// This value is used by deprecated preferredUpstreamIfaceTypes selection which is default
// disabled.
@VisibleForTesting
public static final int TYPE_NONE = -1;
private static final int CALLBACK_LISTEN_ALL = 1;
private static final int CALLBACK_DEFAULT_INTERNET = 2;
private static final int CALLBACK_MOBILE_REQUEST = 3;
private static final SparseIntArray sLegacyTypeToTransport = new SparseIntArray();
static {
sLegacyTypeToTransport.put(TYPE_MOBILE, NetworkCapabilities.TRANSPORT_CELLULAR);
sLegacyTypeToTransport.put(TYPE_MOBILE_DUN, NetworkCapabilities.TRANSPORT_CELLULAR);
sLegacyTypeToTransport.put(TYPE_MOBILE_HIPRI, NetworkCapabilities.TRANSPORT_CELLULAR);
sLegacyTypeToTransport.put(TYPE_WIFI, NetworkCapabilities.TRANSPORT_WIFI);
sLegacyTypeToTransport.put(TYPE_BLUETOOTH, NetworkCapabilities.TRANSPORT_BLUETOOTH);
sLegacyTypeToTransport.put(TYPE_ETHERNET, NetworkCapabilities.TRANSPORT_ETHERNET);
}
private final Context mContext;
private final SharedLog mLog;
private final StateMachine mTarget;
private final Handler mHandler;
private final int mWhat;
private final HashMap<Network, UpstreamNetworkState> mNetworkMap = new HashMap<>();
private HashSet<IpPrefix> mLocalPrefixes;
private ConnectivityManager mCM;
private EntitlementManager mEntitlementMgr;
private NetworkCallback mListenAllCallback;
private NetworkCallback mDefaultNetworkCallback;
private NetworkCallback mMobileNetworkCallback;
private boolean mDunRequired;
// Whether the current default upstream is mobile or not.
private boolean mIsDefaultCellularUpstream;
// The current system default network (not really used yet).
private Network mDefaultInternetNetwork;
// The current upstream network used for tethering.
private Network mTetheringUpstreamNetwork;
public UpstreamNetworkMonitor(Context ctx, StateMachine tgt, SharedLog log, int what) {
mContext = ctx;
mTarget = tgt;
mHandler = mTarget.getHandler();
mLog = log.forSubComponent(TAG);
mWhat = what;
mLocalPrefixes = new HashSet<>();
mIsDefaultCellularUpstream = false;
}
@VisibleForTesting
public UpstreamNetworkMonitor(
ConnectivityManager cm, StateMachine tgt, SharedLog log, int what) {
this((Context) null, tgt, log, what);
mCM = cm;
}
/**
* Tracking the system default network. This method should be called when system is ready.
*
* @param defaultNetworkRequest should be the same as ConnectivityService default request
* @param entitle a EntitlementManager object to communicate between EntitlementManager and
* UpstreamNetworkMonitor
*/
public void startTrackDefaultNetwork(NetworkRequest defaultNetworkRequest,
EntitlementManager entitle) {
// defaultNetworkRequest is not really a "request", just a way of tracking the system
// default network. It's guaranteed not to actually bring up any networks because it's
// the should be the same request as the ConnectivityService default request, and thus
// shares fate with it. We can't use registerDefaultNetworkCallback because it will not
// track the system default network if there is a VPN that applies to our UID.
if (mDefaultNetworkCallback == null) {
mDefaultNetworkCallback = new UpstreamNetworkCallback(CALLBACK_DEFAULT_INTERNET);
cm().requestNetwork(defaultNetworkRequest, mDefaultNetworkCallback, mHandler);
}
if (mEntitlementMgr == null) {
mEntitlementMgr = entitle;
}
}
/** Listen all networks. */
public void startObserveAllNetworks() {
stop();
final NetworkRequest listenAllRequest = new NetworkRequest.Builder()
.clearCapabilities().build();
mListenAllCallback = new UpstreamNetworkCallback(CALLBACK_LISTEN_ALL);
cm().registerNetworkCallback(listenAllRequest, mListenAllCallback, mHandler);
}
/**
* Stop tracking candidate tethering upstreams and release mobile network request.
* Note: this function is used when tethering is stopped because tethering do not need to
* choose upstream anymore. But it would not stop default network tracking because
* EntitlementManager may need to know default network to decide whether to request entitlement
* check even tethering is not active yet.
*/
public void stop() {
releaseMobileNetworkRequest();
releaseCallback(mListenAllCallback);
mListenAllCallback = null;
mTetheringUpstreamNetwork = null;
mNetworkMap.clear();
}
/** Setup or teardown DUN connection according to |dunRequired|. */
public void updateMobileRequiresDun(boolean dunRequired) {
final boolean valueChanged = (mDunRequired != dunRequired);
mDunRequired = dunRequired;
if (valueChanged && mobileNetworkRequested()) {
releaseMobileNetworkRequest();
registerMobileNetworkRequest();
}
}
/** Whether mobile network is requested. */
public boolean mobileNetworkRequested() {
return (mMobileNetworkCallback != null);
}
/** Request mobile network if mobile upstream is permitted. */
public void registerMobileNetworkRequest() {
if (!isCellularUpstreamPermitted()) {
mLog.i("registerMobileNetworkRequest() is not permitted");
releaseMobileNetworkRequest();
return;
}
if (mMobileNetworkCallback != null) {
mLog.e("registerMobileNetworkRequest() already registered");
return;
}
final NetworkRequest mobileUpstreamRequest;
if (mDunRequired) {
mobileUpstreamRequest = new NetworkRequest.Builder()
.addCapability(NET_CAPABILITY_DUN)
.removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
.addTransportType(TRANSPORT_CELLULAR).build();
} else {
mobileUpstreamRequest = new NetworkRequest.Builder()
.addCapability(NET_CAPABILITY_INTERNET)
.addTransportType(TRANSPORT_CELLULAR).build();
}
// The existing default network and DUN callbacks will be notified.
// Therefore, to avoid duplicate notifications, we only register a no-op.
mMobileNetworkCallback = new UpstreamNetworkCallback(CALLBACK_MOBILE_REQUEST);
// The following use of the legacy type system cannot be removed until
// upstream selection no longer finds networks by legacy type.
// See also http://b/34364553 .
final int legacyType = mDunRequired ? TYPE_MOBILE_DUN : TYPE_MOBILE_HIPRI;
// TODO: Change the timeout from 0 (no onUnavailable callback) to some
// moderate callback timeout. This might be useful for updating some UI.
// Additionally, we log a message to aid in any subsequent debugging.
mLog.i("requesting mobile upstream network: " + mobileUpstreamRequest);
cm().requestNetwork(mobileUpstreamRequest, 0, legacyType, mHandler,
mMobileNetworkCallback);
}
/** Release mobile network request. */
public void releaseMobileNetworkRequest() {
if (mMobileNetworkCallback == null) return;
cm().unregisterNetworkCallback(mMobileNetworkCallback);
mMobileNetworkCallback = null;
}
// So many TODOs here, but chief among them is: make this functionality an
// integral part of this class such that whenever a higher priority network
// becomes available and useful we (a) file a request to keep it up as
// necessary and (b) change all upstream tracking state accordingly (by
// passing LinkProperties up to Tethering).
/**
* Select the first available network from |perferredTypes|.
*/
public UpstreamNetworkState selectPreferredUpstreamType(Iterable<Integer> preferredTypes) {
final TypeStatePair typeStatePair = findFirstAvailableUpstreamByType(
mNetworkMap.values(), preferredTypes, isCellularUpstreamPermitted());
mLog.log("preferred upstream type: " + typeStatePair.type);
switch (typeStatePair.type) {
case TYPE_MOBILE_DUN:
case TYPE_MOBILE_HIPRI:
// Tethering just selected mobile upstream in spite of the default network being
// not mobile. This can happen because of the priority list.
// Notify EntitlementManager to check permission for using mobile upstream.
if (!mIsDefaultCellularUpstream) {
mEntitlementMgr.maybeRunProvisioning();
}
// If we're on DUN, put our own grab on it.
registerMobileNetworkRequest();
break;
case TYPE_NONE:
// If we found NONE and mobile upstream is permitted we don't want to do this
// as we want any previous requests to keep trying to bring up something we can use.
if (!isCellularUpstreamPermitted()) releaseMobileNetworkRequest();
break;
default:
// If we've found an active upstream connection that's not DUN/HIPRI
// we should stop any outstanding DUN/HIPRI requests.
releaseMobileNetworkRequest();
break;
}
return typeStatePair.ns;
}
/**
* Get current preferred upstream network. If default network is cellular and DUN is required,
* preferred upstream would be DUN otherwise preferred upstream is the same as default network.
* Returns null if no current upstream is available.
*/
public UpstreamNetworkState getCurrentPreferredUpstream() {
final UpstreamNetworkState dfltState = (mDefaultInternetNetwork != null)
? mNetworkMap.get(mDefaultInternetNetwork)
: null;
if (isNetworkUsableAndNotCellular(dfltState)) return dfltState;
if (!isCellularUpstreamPermitted()) return null;
if (!mDunRequired) return dfltState;
// Find a DUN network. Note that code in Tethering causes a DUN request
// to be filed, but this might be moved into this class in future.
return findFirstDunNetwork(mNetworkMap.values());
}
/** Tell UpstreamNetworkMonitor which network is the current upstream of tethering. */
public void setCurrentUpstream(Network upstream) {
mTetheringUpstreamNetwork = upstream;
}
/** Return local prefixes. */
public Set<IpPrefix> getLocalPrefixes() {
return (Set<IpPrefix>) mLocalPrefixes.clone();
}
private boolean isCellularUpstreamPermitted() {
if (mEntitlementMgr != null) {
return mEntitlementMgr.isCellularUpstreamPermitted();
} else {
// This flow should only happens in testing.
return true;
}
}
private void handleAvailable(Network network) {
if (mNetworkMap.containsKey(network)) return;
if (VDBG) Log.d(TAG, "onAvailable for " + network);
mNetworkMap.put(network, new UpstreamNetworkState(null, null, network));
}
private void handleNetCap(Network network, NetworkCapabilities newNc) {
final UpstreamNetworkState prev = mNetworkMap.get(network);
if (prev == null || newNc.equals(prev.networkCapabilities)) {
// Ignore notifications about networks for which we have not yet
// received onAvailable() (should never happen) and any duplicate
// notifications (e.g. matching more than one of our callbacks).
return;
}
if (VDBG) {
Log.d(TAG, String.format("EVENT_ON_CAPABILITIES for %s: %s",
network, newNc));
}
mNetworkMap.put(network, new UpstreamNetworkState(
prev.linkProperties, newNc, network));
// TODO: If sufficient information is available to select a more
// preferable upstream, do so now and notify the target.
notifyTarget(EVENT_ON_CAPABILITIES, network);
}
private void handleLinkProp(Network network, LinkProperties newLp) {
final UpstreamNetworkState prev = mNetworkMap.get(network);
if (prev == null || newLp.equals(prev.linkProperties)) {
// Ignore notifications about networks for which we have not yet
// received onAvailable() (should never happen) and any duplicate
// notifications (e.g. matching more than one of our callbacks).
return;
}
if (VDBG) {
Log.d(TAG, String.format("EVENT_ON_LINKPROPERTIES for %s: %s",
network, newLp));
}
mNetworkMap.put(network, new UpstreamNetworkState(
newLp, prev.networkCapabilities, network));
// TODO: If sufficient information is available to select a more
// preferable upstream, do so now and notify the target.
notifyTarget(EVENT_ON_LINKPROPERTIES, network);
}
private void handleLost(Network network) {
// There are few TODOs within ConnectivityService's rematching code
// pertaining to spurious onLost() notifications.
//
// TODO: simplify this, probably if favor of code that:
// - selects a new upstream if mTetheringUpstreamNetwork has
// been lost (by any callback)
// - deletes the entry from the map only when the LISTEN_ALL
// callback gets notified.
if (!mNetworkMap.containsKey(network)) {
// Ignore loss of networks about which we had not previously
// learned any information or for which we have already processed
// an onLost() notification.
return;
}
if (VDBG) Log.d(TAG, "EVENT_ON_LOST for " + network);
// TODO: If sufficient information is available to select a more
// preferable upstream, do so now and notify the target. Likewise,
// if the current upstream network is gone, notify the target of the
// fact that we now have no upstream at all.
notifyTarget(EVENT_ON_LOST, mNetworkMap.remove(network));
}
private void recomputeLocalPrefixes() {
final HashSet<IpPrefix> localPrefixes = allLocalPrefixes(mNetworkMap.values());
if (!mLocalPrefixes.equals(localPrefixes)) {
mLocalPrefixes = localPrefixes;
notifyTarget(NOTIFY_LOCAL_PREFIXES, localPrefixes.clone());
}
}
// Fetch (and cache) a ConnectivityManager only if and when we need one.
private ConnectivityManager cm() {
if (mCM == null) {
// MUST call the String variant to be able to write unittests.
mCM = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
}
return mCM;
}
/**
* A NetworkCallback class that handles information of interest directly
* in the thread on which it is invoked. To avoid locking, this MUST be
* run on the same thread as the target state machine's handler.
*/
private class UpstreamNetworkCallback extends NetworkCallback {
private final int mCallbackType;
UpstreamNetworkCallback(int callbackType) {
mCallbackType = callbackType;
}
@Override
public void onAvailable(Network network) {
handleAvailable(network);
}
@Override
public void onCapabilitiesChanged(Network network, NetworkCapabilities newNc) {
if (mCallbackType == CALLBACK_DEFAULT_INTERNET) {
mDefaultInternetNetwork = network;
final boolean newIsCellular = isCellular(newNc);
if (mIsDefaultCellularUpstream != newIsCellular) {
mIsDefaultCellularUpstream = newIsCellular;
mEntitlementMgr.notifyUpstream(newIsCellular);
}
return;
}
handleNetCap(network, newNc);
}
@Override
public void onLinkPropertiesChanged(Network network, LinkProperties newLp) {
if (mCallbackType == CALLBACK_DEFAULT_INTERNET) return;
handleLinkProp(network, newLp);
// Any non-LISTEN_ALL callback will necessarily concern a network that will
// also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
// So it's not useful to do this work for non-LISTEN_ALL callbacks.
if (mCallbackType == CALLBACK_LISTEN_ALL) {
recomputeLocalPrefixes();
}
}
@Override
public void onLost(Network network) {
if (mCallbackType == CALLBACK_DEFAULT_INTERNET) {
mDefaultInternetNetwork = null;
mIsDefaultCellularUpstream = false;
mEntitlementMgr.notifyUpstream(false);
return;
}
handleLost(network);
// Any non-LISTEN_ALL callback will necessarily concern a network that will
// also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback.
// So it's not useful to do this work for non-LISTEN_ALL callbacks.
if (mCallbackType == CALLBACK_LISTEN_ALL) {
recomputeLocalPrefixes();
}
}
}
private void releaseCallback(NetworkCallback cb) {
if (cb != null) cm().unregisterNetworkCallback(cb);
}
private void notifyTarget(int which, Network network) {
notifyTarget(which, mNetworkMap.get(network));
}
private void notifyTarget(int which, Object obj) {
mTarget.sendMessage(mWhat, which, 0, obj);
}
private static class TypeStatePair {
public int type = TYPE_NONE;
public UpstreamNetworkState ns = null;
}
private static TypeStatePair findFirstAvailableUpstreamByType(
Iterable<UpstreamNetworkState> netStates, Iterable<Integer> preferredTypes,
boolean isCellularUpstreamPermitted) {
final TypeStatePair result = new TypeStatePair();
for (int type : preferredTypes) {
NetworkCapabilities nc;
try {
nc = networkCapabilitiesForType(type);
} catch (IllegalArgumentException iae) {
Log.e(TAG, "No NetworkCapabilities mapping for legacy type: " + type);
continue;
}
if (!isCellularUpstreamPermitted && isCellular(nc)) {
continue;
}
for (UpstreamNetworkState value : netStates) {
if (!nc.satisfiedByNetworkCapabilities(value.networkCapabilities)) {
continue;
}
result.type = type;
result.ns = value;
return result;
}
}
return result;
}
private static HashSet<IpPrefix> allLocalPrefixes(Iterable<UpstreamNetworkState> netStates) {
final HashSet<IpPrefix> prefixSet = new HashSet<>();
for (UpstreamNetworkState ns : netStates) {
final LinkProperties lp = ns.linkProperties;
if (lp == null) continue;
prefixSet.addAll(PrefixUtils.localPrefixesFrom(lp));
}
return prefixSet;
}
private static boolean isCellular(UpstreamNetworkState ns) {
return (ns != null) && isCellular(ns.networkCapabilities);
}
private static boolean isCellular(NetworkCapabilities nc) {
return (nc != null) && nc.hasTransport(TRANSPORT_CELLULAR)
&& nc.hasCapability(NET_CAPABILITY_NOT_VPN);
}
private static boolean hasCapability(UpstreamNetworkState ns, int netCap) {
return (ns != null) && (ns.networkCapabilities != null)
&& ns.networkCapabilities.hasCapability(netCap);
}
private static boolean isNetworkUsableAndNotCellular(UpstreamNetworkState ns) {
return (ns != null) && (ns.networkCapabilities != null) && (ns.linkProperties != null)
&& !isCellular(ns.networkCapabilities);
}
private static UpstreamNetworkState findFirstDunNetwork(
Iterable<UpstreamNetworkState> netStates) {
for (UpstreamNetworkState ns : netStates) {
if (isCellular(ns) && hasCapability(ns, NET_CAPABILITY_DUN)) return ns;
}
return null;
}
/**
* Given a legacy type (TYPE_WIFI, ...) returns the corresponding NetworkCapabilities instance.
* This function is used for deprecated legacy type and be disabled by default.
*/
@VisibleForTesting
public static NetworkCapabilities networkCapabilitiesForType(int type) {
final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder();
// Map from type to transports.
final int notFound = -1;
final int transport = sLegacyTypeToTransport.get(type, notFound);
Preconditions.checkArgument(transport != notFound, "unknown legacy type: " + type);
builder.addTransportType(transport);
if (type == TYPE_MOBILE_DUN) {
builder.addCapability(NetworkCapabilities.NET_CAPABILITY_DUN);
// DUN is restricted network, see NetworkCapabilities#FORCE_RESTRICTED_CAPABILITIES.
builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
} else {
builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
}
return builder.build();
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright (C) 2019 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.networkstack.tethering;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import androidx.annotation.NonNull;
/**
* Snapshot of tethering upstream network state.
*/
public class UpstreamNetworkState {
/** {@link LinkProperties}. */
public final LinkProperties linkProperties;
/** {@link NetworkCapabilities}. */
public final NetworkCapabilities networkCapabilities;
/** {@link Network}. */
public final Network network;
/** Constructs a new UpstreamNetworkState. */
public UpstreamNetworkState(LinkProperties linkProperties,
NetworkCapabilities networkCapabilities, Network network) {
this.linkProperties = linkProperties;
this.networkCapabilities = networkCapabilities;
this.network = network;
}
@NonNull
@Override
public String toString() {
return String.format("UpstreamNetworkState{%s, %s, %s}",
network == null ? "null" : network,
networkCapabilities == null ? "null" : networkCapabilities,
linkProperties == null ? "null" : linkProperties);
}
}

View file

@ -21,11 +21,11 @@
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
<service
android:name="com.android.server.connectivity.tethering.MockTetheringService"
android:name="com.android.networkstack.tethering.MockTetheringService"
android:permission="android.permission.TETHER_PRIVILEGED"
android:exported="true">
<intent-filter>
<action android:name="com.android.server.connectivity.tethering.TetheringService"/>
<action android:name="com.android.networkstack.tethering.TetheringService"/>
</intent-filter>
</service>
</application>

View file

@ -0,0 +1,162 @@
/*
* 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.networkstack.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(
makeAddrInfo("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 = makeAddrInfo(
"192.168.43.45/32", "my_hostname", clock.time + 30)
private val client2 = TetheredClient(client2Addr, listOf(
client2Exp30AddrInfo,
makeAddrInfo("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(makeAddrInfo("2001:db8:34::34/72", "other_other_hostname", clock.time + 10)),
TETHERING_USB)
private fun makeAddrInfo(addr: String, hostname: String?, expTime: Long) =
LinkAddress(addr).let {
AddressInfo(LinkAddress(it.address, it.prefixLength, it.flags, it.scope,
expTime /* deprecationTime */, expTime /* expirationTime */), hostname)
}
@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
}
}
}

View file

@ -0,0 +1,522 @@
/*
* Copyright (C) 2018 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.networkstack.tethering;
import static android.net.TetheringManager.TETHERING_BLUETOOTH;
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
import android.net.util.SharedLog;
import android.os.Bundle;
import android.os.Message;
import android.os.PersistableBundle;
import android.os.ResultReceiver;
import android.os.SystemProperties;
import android.os.test.TestLooper;
import android.provider.DeviceConfig;
import android.telephony.CarrierConfigManager;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.internal.util.test.BroadcastInterceptingContext;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
import java.util.ArrayList;
@RunWith(AndroidJUnit4.class)
@SmallTest
public final class EntitlementManagerTest {
private static final int EVENT_EM_UPDATE = 1;
private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
@Mock private CarrierConfigManager mCarrierConfigManager;
@Mock private Context mContext;
@Mock private Resources mResources;
@Mock private SharedLog mLog;
@Mock private EntitlementManager.OnUiEntitlementFailedListener mEntitlementFailedListener;
// Like so many Android system APIs, these cannot be mocked because it is marked final.
// We have to use the real versions.
private final PersistableBundle mCarrierConfig = new PersistableBundle();
private final TestLooper mLooper = new TestLooper();
private Context mMockContext;
private TestStateMachine mSM;
private WrappedEntitlementManager mEnMgr;
private TetheringConfiguration mConfig;
private MockitoSession mMockingSession;
private class MockContext extends BroadcastInterceptingContext {
MockContext(Context base) {
super(base);
}
@Override
public Resources getResources() {
return mResources;
}
}
public class WrappedEntitlementManager extends EntitlementManager {
public int fakeEntitlementResult = TETHER_ERROR_ENTITLEMENT_UNKNOWN;
public int uiProvisionCount = 0;
public int silentProvisionCount = 0;
public WrappedEntitlementManager(Context ctx, StateMachine target,
SharedLog log, int what) {
super(ctx, target, log, what);
}
public void reset() {
fakeEntitlementResult = TETHER_ERROR_ENTITLEMENT_UNKNOWN;
uiProvisionCount = 0;
silentProvisionCount = 0;
}
@Override
protected void runUiTetherProvisioning(int type, int subId, ResultReceiver receiver) {
uiProvisionCount++;
receiver.send(fakeEntitlementResult, null);
}
@Override
protected void runSilentTetherProvisioning(int type, int subId) {
silentProvisionCount++;
addDownstreamMapping(type, fakeEntitlementResult);
}
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mMockingSession = mockitoSession()
.initMocks(this)
.mockStatic(SystemProperties.class)
.mockStatic(DeviceConfig.class)
.strictness(Strictness.WARN)
.startMocking();
// Don't disable tethering provisioning unless requested.
doReturn(false).when(
() -> SystemProperties.getBoolean(
eq(EntitlementManager.DISABLE_PROVISIONING_SYSPROP_KEY), anyBoolean()));
doReturn(false).when(
() -> DeviceConfig.getBoolean(eq(NAMESPACE_CONNECTIVITY),
eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER), anyBoolean()));
when(mResources.getStringArray(R.array.config_tether_dhcp_range))
.thenReturn(new String[0]);
when(mResources.getStringArray(R.array.config_tether_usb_regexs))
.thenReturn(new String[0]);
when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
.thenReturn(new String[0]);
when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs))
.thenReturn(new String[0]);
when(mResources.getIntArray(R.array.config_tether_upstream_types))
.thenReturn(new int[0]);
when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
false);
when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
when(mLog.forSubComponent(anyString())).thenReturn(mLog);
mMockContext = new MockContext(mContext);
mSM = new TestStateMachine();
mEnMgr = new WrappedEntitlementManager(mMockContext, mSM, mLog, EVENT_EM_UPDATE);
mEnMgr.setOnUiEntitlementFailedListener(mEntitlementFailedListener);
mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
mEnMgr.setTetheringConfigurationFetcher(() -> {
return mConfig;
});
}
@After
public void tearDown() throws Exception {
if (mSM != null) {
mSM.quit();
mSM = null;
}
mMockingSession.finishMocking();
}
private void setupForRequiredProvisioning() {
// Produce some acceptable looking provision app setting if requested.
when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
.thenReturn(PROVISIONING_APP_NAME);
when(mResources.getString(R.string.config_mobile_hotspot_provision_app_no_ui))
.thenReturn(PROVISIONING_NO_UI_APP_NAME);
// Act like the CarrierConfigManager is present and ready unless told otherwise.
when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
.thenReturn(mCarrierConfigManager);
when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig);
mCarrierConfig.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true);
mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
}
@Test
public void canRequireProvisioning() {
setupForRequiredProvisioning();
assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
}
@Test
public void toleratesCarrierConfigManagerMissing() {
setupForRequiredProvisioning();
when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE))
.thenReturn(null);
mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
// Couldn't get the CarrierConfigManager, but still had a declared provisioning app.
// Therefore provisioning still be required.
assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
}
@Test
public void toleratesCarrierConfigMissing() {
setupForRequiredProvisioning();
when(mCarrierConfigManager.getConfig()).thenReturn(null);
mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
// We still have a provisioning app configured, so still require provisioning.
assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
}
@Test
public void toleratesCarrierConfigNotLoaded() {
setupForRequiredProvisioning();
mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, false);
// We still have a provisioning app configured, so still require provisioning.
assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig));
}
@Test
public void provisioningNotRequiredWhenAppNotFound() {
setupForRequiredProvisioning();
when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
.thenReturn(null);
mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig));
when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app))
.thenReturn(new String[] {"malformedApp"});
mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig));
}
@Test
public void testRequestLastEntitlementCacheValue() throws Exception {
// 1. Entitlement check is not required.
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
ResultReceiver receiver = new ResultReceiver(null) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
}
};
mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
mLooper.dispatchAll();
assertEquals(0, mEnMgr.uiProvisionCount);
mEnMgr.reset();
setupForRequiredProvisioning();
// 2. No cache value and don't need to run entitlement check.
receiver = new ResultReceiver(null) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
}
};
mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
mLooper.dispatchAll();
assertEquals(0, mEnMgr.uiProvisionCount);
mEnMgr.reset();
// 3. No cache value and ui entitlement check is needed.
mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
receiver = new ResultReceiver(null) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
}
};
mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
mLooper.dispatchAll();
assertEquals(1, mEnMgr.uiProvisionCount);
mEnMgr.reset();
// 4. Cache value is TETHER_ERROR_PROVISIONING_FAILED and don't need to run entitlement
// check.
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
receiver = new ResultReceiver(null) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode);
}
};
mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false);
mLooper.dispatchAll();
assertEquals(0, mEnMgr.uiProvisionCount);
mEnMgr.reset();
// 5. Cache value is TETHER_ERROR_PROVISIONING_FAILED and ui entitlement check is needed.
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
receiver = new ResultReceiver(null) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
}
};
mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
mLooper.dispatchAll();
assertEquals(1, mEnMgr.uiProvisionCount);
mEnMgr.reset();
// 6. Cache value is TETHER_ERROR_NO_ERROR.
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
receiver = new ResultReceiver(null) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
assertEquals(TETHER_ERROR_NO_ERROR, resultCode);
}
};
mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true);
mLooper.dispatchAll();
assertEquals(0, mEnMgr.uiProvisionCount);
mEnMgr.reset();
// 7. Test get value for other downstream type.
receiver = new ResultReceiver(null) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
}
};
mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_USB, receiver, false);
mLooper.dispatchAll();
assertEquals(0, mEnMgr.uiProvisionCount);
mEnMgr.reset();
// 8. Test get value for invalid downstream type.
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
receiver = new ResultReceiver(null) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode);
}
};
mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI_P2P, receiver, true);
mLooper.dispatchAll();
assertEquals(0, mEnMgr.uiProvisionCount);
mEnMgr.reset();
}
@Test
public void verifyPermissionResult() {
setupForRequiredProvisioning();
mEnMgr.notifyUpstream(true);
mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
mLooper.dispatchAll();
assertFalse(mEnMgr.isCellularUpstreamPermitted());
mEnMgr.stopProvisioningIfNeeded(TETHERING_WIFI);
mLooper.dispatchAll();
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
mLooper.dispatchAll();
assertTrue(mEnMgr.isCellularUpstreamPermitted());
}
@Test
public void verifyPermissionIfAllNotApproved() {
setupForRequiredProvisioning();
mEnMgr.notifyUpstream(true);
mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
mLooper.dispatchAll();
assertFalse(mEnMgr.isCellularUpstreamPermitted());
mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
mLooper.dispatchAll();
assertFalse(mEnMgr.isCellularUpstreamPermitted());
mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.startProvisioningIfNeeded(TETHERING_BLUETOOTH, true);
mLooper.dispatchAll();
assertFalse(mEnMgr.isCellularUpstreamPermitted());
}
@Test
public void verifyPermissionIfAnyApproved() {
setupForRequiredProvisioning();
mEnMgr.notifyUpstream(true);
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
mLooper.dispatchAll();
assertTrue(mEnMgr.isCellularUpstreamPermitted());
mLooper.dispatchAll();
mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
mLooper.dispatchAll();
assertTrue(mEnMgr.isCellularUpstreamPermitted());
mEnMgr.stopProvisioningIfNeeded(TETHERING_WIFI);
mLooper.dispatchAll();
assertFalse(mEnMgr.isCellularUpstreamPermitted());
}
@Test
public void verifyPermissionWhenProvisioningNotStarted() {
assertTrue(mEnMgr.isCellularUpstreamPermitted());
setupForRequiredProvisioning();
assertFalse(mEnMgr.isCellularUpstreamPermitted());
}
@Test
public void testRunTetherProvisioning() {
setupForRequiredProvisioning();
// 1. start ui provisioning, upstream is mobile
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.notifyUpstream(true);
mLooper.dispatchAll();
mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
mLooper.dispatchAll();
assertEquals(1, mEnMgr.uiProvisionCount);
assertEquals(0, mEnMgr.silentProvisionCount);
assertTrue(mEnMgr.isCellularUpstreamPermitted());
mEnMgr.reset();
// 2. start no-ui provisioning
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, false);
mLooper.dispatchAll();
assertEquals(0, mEnMgr.uiProvisionCount);
assertEquals(1, mEnMgr.silentProvisionCount);
assertTrue(mEnMgr.isCellularUpstreamPermitted());
mEnMgr.reset();
// 3. tear down mobile, then start ui provisioning
mEnMgr.notifyUpstream(false);
mLooper.dispatchAll();
mEnMgr.startProvisioningIfNeeded(TETHERING_BLUETOOTH, true);
mLooper.dispatchAll();
assertEquals(0, mEnMgr.uiProvisionCount);
assertEquals(0, mEnMgr.silentProvisionCount);
mEnMgr.reset();
// 4. switch upstream back to mobile
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.notifyUpstream(true);
mLooper.dispatchAll();
assertEquals(1, mEnMgr.uiProvisionCount);
assertEquals(0, mEnMgr.silentProvisionCount);
assertTrue(mEnMgr.isCellularUpstreamPermitted());
mEnMgr.reset();
// 5. tear down mobile, then switch SIM
mEnMgr.notifyUpstream(false);
mLooper.dispatchAll();
mEnMgr.reevaluateSimCardProvisioning(mConfig);
assertEquals(0, mEnMgr.uiProvisionCount);
assertEquals(0, mEnMgr.silentProvisionCount);
mEnMgr.reset();
// 6. switch upstream back to mobile again
mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.notifyUpstream(true);
mLooper.dispatchAll();
assertEquals(0, mEnMgr.uiProvisionCount);
assertEquals(3, mEnMgr.silentProvisionCount);
assertFalse(mEnMgr.isCellularUpstreamPermitted());
mEnMgr.reset();
// 7. start ui provisioning, upstream is mobile, downstream is ethernet
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.startProvisioningIfNeeded(TETHERING_ETHERNET, true);
mLooper.dispatchAll();
assertEquals(1, mEnMgr.uiProvisionCount);
assertEquals(0, mEnMgr.silentProvisionCount);
assertTrue(mEnMgr.isCellularUpstreamPermitted());
mEnMgr.reset();
// 8. downstream is invalid
mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI_P2P, true);
mLooper.dispatchAll();
assertEquals(0, mEnMgr.uiProvisionCount);
assertEquals(0, mEnMgr.silentProvisionCount);
mEnMgr.reset();
}
@Test
public void testCallStopTetheringWhenUiProvisioningFail() {
setupForRequiredProvisioning();
verify(mEntitlementFailedListener, times(0)).onUiEntitlementFailed(TETHERING_WIFI);
mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED;
mEnMgr.notifyUpstream(true);
mLooper.dispatchAll();
mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true);
mLooper.dispatchAll();
assertEquals(1, mEnMgr.uiProvisionCount);
verify(mEntitlementFailedListener, times(1)).onUiEntitlementFailed(TETHERING_WIFI);
}
public class TestStateMachine extends StateMachine {
public final ArrayList<Message> messages = new ArrayList<>();
private final State
mLoggingState = new EntitlementManagerTest.TestStateMachine.LoggingState();
class LoggingState extends State {
@Override public void enter() {
messages.clear();
}
@Override public void exit() {
messages.clear();
}
@Override public boolean processMessage(Message msg) {
messages.add(msg);
return false;
}
}
public TestStateMachine() {
super("EntitlementManagerTest.TestStateMachine", mLooper.getLooper());
addState(mLoggingState);
setInitialState(mLoggingState);
super.start();
}
}
}

Some files were not shown because too many files have changed in this diff Show more