Merge "Fix ConnectivityManagerTest initialization on Q"
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index 33f1c29..a33af61 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -33,6 +33,8 @@
import com.android.networkstack.tethering.Tether4Value;
import com.android.networkstack.tethering.TetherStatsValue;
+import java.util.function.BiConsumer;
+
/**
* Bpf coordinator class for API shims.
*/
@@ -161,6 +163,12 @@
}
@Override
+ public void tetherOffloadRuleForEach(boolean downstream,
+ @NonNull BiConsumer<Tether4Key, Tether4Value> action) {
+ /* no op */
+ }
+
+ @Override
public boolean attachProgram(String iface, boolean downstream) {
/* no op */
return true;
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 74ddcbc..611c828 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -47,6 +47,7 @@
import java.io.FileDescriptor;
import java.io.IOException;
+import java.util.function.BiConsumer;
/**
* Bpf coordinator class for API shims.
@@ -380,10 +381,7 @@
try {
if (downstream) {
- if (!mBpfDownstream4Map.deleteEntry(key)) {
- mLog.e("Could not delete entry (key: " + key + ")");
- return false;
- }
+ if (!mBpfDownstream4Map.deleteEntry(key)) return false; // Rule did not exist
// Decrease the rule count while a deleting rule is not using a given upstream
// interface anymore.
@@ -401,19 +399,32 @@
mRule4CountOnUpstream.put(upstreamIfindex, count);
}
} else {
- mBpfUpstream4Map.deleteEntry(key);
+ if (!mBpfUpstream4Map.deleteEntry(key)) return false; // Rule did not exist
}
} catch (ErrnoException e) {
- // Silent if the rule did not exist.
- if (e.errno != OsConstants.ENOENT) {
- mLog.e("Could not delete entry: ", e);
- return false;
- }
+ mLog.e("Could not delete entry (key: " + key + ")", e);
+ return false;
}
return true;
}
@Override
+ public void tetherOffloadRuleForEach(boolean downstream,
+ @NonNull BiConsumer<Tether4Key, Tether4Value> action) {
+ if (!isInitialized()) return;
+
+ try {
+ if (downstream) {
+ mBpfDownstream4Map.forEach(action);
+ } else {
+ mBpfUpstream4Map.forEach(action);
+ }
+ } catch (ErrnoException e) {
+ mLog.e("Could not iterate map: ", e);
+ }
+ }
+
+ @Override
public boolean attachProgram(String iface, boolean downstream) {
if (!isInitialized()) return false;
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index 8a7a49c..08ab9ca 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -28,6 +28,8 @@
import com.android.networkstack.tethering.Tether4Value;
import com.android.networkstack.tethering.TetherStatsValue;
+import java.util.function.BiConsumer;
+
/**
* Bpf coordinator class for API shims.
*/
@@ -145,10 +147,25 @@
/**
* Deletes a tethering IPv4 offload rule from the appropriate BPF map.
+ *
+ * @param downstream true if downstream, false if upstream.
+ * @param key the key to delete.
+ * @return true iff the map was modified, false if the key did not exist or there was an error.
*/
public abstract boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key);
/**
+ * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
+ *
+ * @param downstream true if downstream, false if upstream.
+ * @param action represents the action for each key -> value. The entry deletion is not
+ * allowed and use #tetherOffloadRuleRemove instead.
+ */
+ @Nullable
+ public abstract void tetherOffloadRuleForEach(boolean downstream,
+ @NonNull BiConsumer<Tether4Key, Tether4Value> action);
+
+ /**
* Whether there is currently any IPv4 rule on the specified upstream.
*/
public abstract boolean isAnyIpv4RuleOnUpstream(int ifIndex);
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 3428c1d..822bdf6 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -596,6 +596,7 @@
// into calls to InterfaceController, shared with startIPv4().
mInterfaceCtrl.clearIPv4Address();
mPrivateAddressCoordinator.releaseDownstream(this);
+ mBpfCoordinator.tetherOffloadClientClear(this);
mIpv4Address = null;
mStaticIpv4ServerAddr = null;
mStaticIpv4ClientAddr = null;
@@ -949,7 +950,6 @@
if (e.isValid()) {
mBpfCoordinator.tetherOffloadClientAdd(this, clientInfo);
} else {
- // TODO: Delete all related offload rules which are using this client.
mBpfCoordinator.tetherOffloadClientRemove(this, clientInfo);
}
}
@@ -1283,6 +1283,16 @@
super.exit();
}
+ // Note that IPv4 offload rules cleanup is implemented in BpfCoordinator while upstream
+ // state is null or changed because IPv4 and IPv6 tethering have different code flow
+ // and behaviour. While upstream is switching from offload supported interface to
+ // offload non-supportted interface, event CMD_TETHER_CONNECTION_CHANGED calls
+ // #cleanupUpstreamInterface but #cleanupUpstream because new UpstreamIfaceSet is not null.
+ // This case won't happen in IPv6 tethering because IPv6 tethering upstream state is
+ // reported by IPv6TetheringCoordinator. #cleanupUpstream is also called by unwirding
+ // adding NAT failure. In that case, the IPv4 offload rules are removed by #stopIPv4
+ // in the state machine. Once there is any case out whish is not covered by previous cases,
+ // probably consider clearing rules in #cleanupUpstream as well.
private void cleanupUpstream() {
if (mUpstreamIfaceSet == null) return;
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 4a05c9f..9b95dac 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -34,7 +34,6 @@
import android.app.usage.NetworkStatsManager;
import android.net.INetd;
-import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.NetworkStats;
import android.net.NetworkStats.Entry;
@@ -42,7 +41,9 @@
import android.net.ip.ConntrackMonitor;
import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
import android.net.ip.IpServer;
+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.InterfaceParams;
import android.net.util.SharedLog;
@@ -50,7 +51,9 @@
import android.os.Handler;
import android.os.SystemClock;
import android.system.ErrnoException;
+import android.system.OsConstants;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
@@ -69,6 +72,7 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
@@ -120,6 +124,13 @@
}
@VisibleForTesting
+ static final int POLLING_CONNTRACK_TIMEOUT_MS = 60_000;
+ @VisibleForTesting
+ static final int NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED = 432000;
+ @VisibleForTesting
+ static final int NF_CONNTRACK_UDP_TIMEOUT_STREAM = 180;
+
+ @VisibleForTesting
enum StatsType {
STATS_PER_IFACE,
STATS_PER_UID,
@@ -228,12 +239,22 @@
// BpfCoordinatorTest needs predictable iteration order.
private final Set<Integer> mDeviceMapSet = new LinkedHashSet<>();
+ // Tracks the last IPv4 upstream index. Support single upstream only.
+ // TODO: Support multi-upstream interfaces.
+ private int mLastIPv4UpstreamIfindex = 0;
+
// Runnable that used by scheduling next polling of stats.
- private final Runnable mScheduledPollingTask = () -> {
+ private final Runnable mScheduledPollingStats = () -> {
updateForwardedStats();
maybeSchedulePollingStats();
};
+ // Runnable that used by scheduling next polling of conntrack timeout.
+ private final Runnable mScheduledPollingConntrackTimeout = () -> {
+ maybeRefreshConntrackTimeout();
+ maybeSchedulePollingConntrackTimeout();
+ };
+
// TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
@VisibleForTesting
public abstract static class Dependencies {
@@ -263,13 +284,19 @@
}
/**
+ * Represents an estimate of elapsed time since boot in nanoseconds.
+ */
+ public long elapsedRealtimeNanos() {
+ return SystemClock.elapsedRealtimeNanos();
+ }
+
+ /**
* Check OS Build at least S.
*
* TODO: move to BpfCoordinatorShim once the test doesn't need the mocked OS build for
* testing different code flows concurrently.
*/
public boolean isAtLeastS() {
- // TODO: consider using ShimUtils.isAtLeastS.
return SdkLevel.isAtLeastS();
}
@@ -407,6 +434,7 @@
mPollingStarted = true;
maybeSchedulePollingStats();
+ maybeSchedulePollingConntrackTimeout();
mLog.i("Polling started");
}
@@ -422,9 +450,13 @@
public void stopPolling() {
if (!mPollingStarted) return;
- // Stop scheduled polling tasks and poll the latest stats from BPF maps.
- if (mHandler.hasCallbacks(mScheduledPollingTask)) {
- mHandler.removeCallbacks(mScheduledPollingTask);
+ // Stop scheduled polling conntrack timeout.
+ if (mHandler.hasCallbacks(mScheduledPollingConntrackTimeout)) {
+ mHandler.removeCallbacks(mScheduledPollingConntrackTimeout);
+ }
+ // Stop scheduled polling stats and poll the latest stats from BPF maps.
+ if (mHandler.hasCallbacks(mScheduledPollingStats)) {
+ mHandler.removeCallbacks(mScheduledPollingStats);
}
updateForwardedStats();
mPollingStarted = false;
@@ -576,6 +608,7 @@
/**
* Clear all forwarding rules for a given downstream.
* Note that this can be only called on handler thread.
+ * TODO: rename to tetherOffloadRuleClear6 because of IPv6 only.
*/
public void tetherOffloadRuleClear(@NonNull final IpServer ipServer) {
if (!isUsingBpf()) return;
@@ -647,6 +680,7 @@
/**
* Add downstream client.
+ * Note that this can be only called on handler thread.
*/
public void tetherOffloadClientAdd(@NonNull final IpServer ipServer,
@NonNull final ClientInfo client) {
@@ -661,54 +695,180 @@
}
/**
- * Remove downstream client.
+ * Remove a downstream client and its rules if any.
+ * Note that this can be only called on handler thread.
*/
public void tetherOffloadClientRemove(@NonNull final IpServer ipServer,
@NonNull final ClientInfo client) {
if (!isUsingBpf()) return;
+ // No clients on the downstream, return early.
HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
if (clients == null) return;
- // If no rule is removed, return early. Avoid unnecessary work on a non-existent rule
- // which may have never been added or removed already.
+ // No client is removed, return early.
if (clients.remove(client.clientAddress) == null) return;
- // Remove the downstream entry if it has no more rule.
+ // Remove the client's rules. Removing the client implies that its rules are not used
+ // anymore.
+ tetherOffloadRuleClear(client);
+
+ // Remove the downstream entry if it has no more client.
if (clients.isEmpty()) {
mTetherClients.remove(ipServer);
}
}
/**
- * Call when UpstreamNetworkState may be changed.
- * If upstream has ipv4 for tethering, update this new UpstreamNetworkState to map. The
- * upstream interface index and its address mapping is prepared for building IPv4
- * offload rule.
- *
- * TODO: Delete the unused upstream interface mapping.
- * TODO: Support ether ip upstream interface.
+ * Clear all downstream clients and their rules if any.
+ * Note that this can be only called on handler thread.
*/
- public void addUpstreamIfindexToMap(LinkProperties lp) {
- if (!mPollingStarted) return;
+ public void tetherOffloadClientClear(@NonNull final IpServer ipServer) {
+ if (!isUsingBpf()) return;
+
+ final HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
+ if (clients == null) return;
+
+ // Need to build a client list because the client map may be changed in the iteration.
+ for (final ClientInfo c : new ArrayList<ClientInfo>(clients.values())) {
+ tetherOffloadClientRemove(ipServer, c);
+ }
+ }
+
+ /**
+ * Clear all forwarding IPv4 rules for a given client.
+ * Note that this can be only called on handler thread.
+ */
+ private void tetherOffloadRuleClear(@NonNull final ClientInfo clientInfo) {
+ // TODO: consider removing the rules in #tetherOffloadRuleForEach once BpfMap#forEach
+ // can guarantee that deleting some pass-in rules in the BPF map iteration can still
+ // walk through every entry.
+ final Inet4Address clientAddr = clientInfo.clientAddress;
+ final Set<Integer> upstreamIndiceSet = new ArraySet<Integer>();
+ final Set<Tether4Key> deleteUpstreamRuleKeys = new ArraySet<Tether4Key>();
+ final Set<Tether4Key> deleteDownstreamRuleKeys = new ArraySet<Tether4Key>();
+
+ // Find the rules which are related with the given client.
+ mBpfCoordinatorShim.tetherOffloadRuleForEach(UPSTREAM, (k, v) -> {
+ if (Arrays.equals(k.src4, clientAddr.getAddress())) {
+ deleteUpstreamRuleKeys.add(k);
+ }
+ });
+ mBpfCoordinatorShim.tetherOffloadRuleForEach(DOWNSTREAM, (k, v) -> {
+ if (Arrays.equals(v.dst46, toIpv4MappedAddressBytes(clientAddr))) {
+ deleteDownstreamRuleKeys.add(k);
+ upstreamIndiceSet.add((int) k.iif);
+ }
+ });
+
+ // The rules should be paired on upstream and downstream map because they are added by
+ // conntrack events which have bidirectional information.
+ // TODO: Consider figuring out a way to fix. Probably delete all rules to fallback.
+ if (deleteUpstreamRuleKeys.size() != deleteDownstreamRuleKeys.size()) {
+ Log.wtf(TAG, "The deleting rule numbers are different on upstream4 and downstream4 ("
+ + "upstream: " + deleteUpstreamRuleKeys.size() + ", "
+ + "downstream: " + deleteDownstreamRuleKeys.size() + ").");
+ return;
+ }
+
+ // Delete the rules which are related with the given client.
+ for (final Tether4Key k : deleteUpstreamRuleKeys) {
+ mBpfCoordinatorShim.tetherOffloadRuleRemove(UPSTREAM, k);
+ }
+ for (final Tether4Key k : deleteDownstreamRuleKeys) {
+ mBpfCoordinatorShim.tetherOffloadRuleRemove(DOWNSTREAM, k);
+ }
+
+ // Cleanup each upstream interface by a set which avoids duplicated work on the same
+ // upstream interface. Cleaning up the same interface twice (or more) here may raise
+ // an exception because all related information were removed in the first deletion.
+ for (final int upstreamIndex : upstreamIndiceSet) {
+ maybeClearLimit(upstreamIndex);
+ }
+ }
+
+ /**
+ * Clear all forwarding IPv4 rules for a given downstream. Needed because the client may still
+ * connect on the downstream but the existing rules are not required anymore. Ex: upstream
+ * changed.
+ */
+ private void tetherOffloadRule4Clear(@NonNull final IpServer ipServer) {
+ if (!isUsingBpf()) return;
+
+ final HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
+ if (clients == null) return;
+
+ // The value should be unique as its key because currently the key was using from its
+ // client address of ClientInfo. See #tetherOffloadClientAdd.
+ for (final ClientInfo client : clients.values()) {
+ tetherOffloadRuleClear(client);
+ }
+ }
+
+ private boolean isValidUpstreamIpv4Address(@NonNull final InetAddress addr) {
+ if (!(addr instanceof Inet4Address)) return false;
+ Inet4Address v4 = (Inet4Address) addr;
+ if (v4.isAnyLocalAddress() || v4.isLinkLocalAddress()
+ || v4.isLoopbackAddress() || v4.isMulticastAddress()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Call when UpstreamNetworkState may be changed.
+ * If upstream has ipv4 for tethering, update this new UpstreamNetworkState
+ * to BpfCoordinator for building upstream interface index mapping. Otherwise,
+ * clear the all existing rules if any.
+ *
+ * Note that this can be only called on handler thread.
+ */
+ public void updateUpstreamNetworkState(UpstreamNetworkState ns) {
+ if (!isUsingBpf()) return;
+
+ int upstreamIndex = 0;
// This will not work on a network that is using 464xlat because hasIpv4Address will not be
// true.
// TODO: need to consider 464xlat.
- if (lp == null || !lp.hasIpv4Address()) return;
+ if (ns != null && ns.linkProperties != null && ns.linkProperties.hasIpv4Address()) {
+ // TODO: support ether ip upstream interface.
+ final InterfaceParams params = mDeps.getInterfaceParams(
+ ns.linkProperties.getInterfaceName());
+ if (params != null && !params.hasMacAddress /* raw ip upstream only */) {
+ upstreamIndex = params.index;
+ }
+ }
+ if (mLastIPv4UpstreamIfindex == upstreamIndex) return;
- // Support raw ip upstream interface only.
- final InterfaceParams params = mDeps.getInterfaceParams(lp.getInterfaceName());
- if (params == null || params.hasMacAddress) return;
+ // Clear existing rules if upstream interface is changed. The existing rules should be
+ // cleared before upstream index mapping is cleared. It can avoid that ipServer or
+ // conntrack event may use the non-existing upstream interfeace index to build a removing
+ // key while removeing the rules. Can't notify each IpServer to clear the rules as
+ // IPv6TetheringCoordinator#updateUpstreamNetworkState because the IpServer may not
+ // handle the upstream changing notification before changing upstream index mapping.
+ if (mLastIPv4UpstreamIfindex != 0) {
+ // Clear all forwarding IPv4 rules for all downstreams.
+ for (final IpServer ipserver : mTetherClients.keySet()) {
+ tetherOffloadRule4Clear(ipserver);
+ }
+ }
- Collection<InetAddress> addresses = lp.getAddresses();
- for (InetAddress addr: addresses) {
- if (addr instanceof Inet4Address) {
- Inet4Address i4addr = (Inet4Address) addr;
- if (!i4addr.isAnyLocalAddress() && !i4addr.isLinkLocalAddress()
- && !i4addr.isLoopbackAddress() && !i4addr.isMulticastAddress()) {
- mIpv4UpstreamIndices.put(i4addr, params.index);
- }
+ // Don't update mLastIPv4UpstreamIfindex before clearing existing rules if any. Need that
+ // to tell if it is required to clean the out-of-date rules.
+ mLastIPv4UpstreamIfindex = upstreamIndex;
+
+ // If link properties are valid, build the upstream information mapping. Otherwise, clear
+ // the upstream interface index mapping, to ensure that any conntrack events that arrive
+ // after the upstream is lost do not incorrectly add rules pointing at the upstream.
+ if (upstreamIndex == 0) {
+ mIpv4UpstreamIndices.clear();
+ return;
+ }
+ Collection<InetAddress> addresses = ns.linkProperties.getAddresses();
+ for (final InetAddress addr: addresses) {
+ if (isValidUpstreamIpv4Address(addr)) {
+ mIpv4UpstreamIndices.put((Inet4Address) addr, upstreamIndex);
}
}
}
@@ -793,6 +953,24 @@
dumpDevmap(pw);
pw.decreaseIndent();
+ pw.println("Client Information:");
+ pw.increaseIndent();
+ if (mTetherClients.isEmpty()) {
+ pw.println("<empty>");
+ } else {
+ pw.println(mTetherClients.toString());
+ }
+ pw.decreaseIndent();
+
+ pw.println("IPv4 Upstream Indices:");
+ pw.increaseIndent();
+ if (mIpv4UpstreamIndices.isEmpty()) {
+ pw.println("<empty>");
+ } else {
+ pw.println(mIpv4UpstreamIndices.toString());
+ }
+ pw.decreaseIndent();
+
pw.println();
pw.println("Forwarding counters:");
pw.increaseIndent();
@@ -971,14 +1149,14 @@
return;
}
if (map.isEmpty()) {
- pw.println("No interface index");
+ pw.println("<empty>");
return;
}
pw.println("ifindex (iface) -> ifindex (iface)");
pw.increaseIndent();
map.forEach((k, v) -> {
// Only get upstream interface name. Just do the best to make the index readable.
- // TODO: get downstream interface name because the index is either upstrema or
+ // TODO: get downstream interface name because the index is either upstream or
// downstream interface in dev map.
pw.println(String.format("%d (%s) -> %d (%s)", k.ifIndex, getIfName(k.ifIndex),
v.ifIndex, getIfName(v.ifIndex)));
@@ -1248,12 +1426,99 @@
return null;
}
- // Support raw ip only.
- // TODO: add ether ip support.
+ @NonNull
+ private byte[] toIpv4MappedAddressBytes(Inet4Address ia4) {
+ final byte[] addr4 = ia4.getAddress();
+ final byte[] addr6 = new byte[16];
+ addr6[10] = (byte) 0xff;
+ addr6[11] = (byte) 0xff;
+ addr6[12] = addr4[0];
+ addr6[13] = addr4[1];
+ addr6[14] = addr4[2];
+ addr6[15] = addr4[3];
+ return addr6;
+ }
+
+ @Nullable
+ private Inet4Address ipv4MappedAddressBytesToIpv4Address(final byte[] addr46) {
+ if (addr46.length != 16) return null;
+ if (addr46[0] != 0 || addr46[1] != 0 || addr46[2] != 0 || addr46[3] != 0
+ || addr46[4] != 0 || addr46[5] != 0 || addr46[6] != 0 || addr46[7] != 0
+ || addr46[8] != 0 && addr46[9] != 0 || (addr46[10] & 0xff) != 0xff
+ || (addr46[11] & 0xff) != 0xff) {
+ return null;
+ }
+
+ final byte[] addr4 = new byte[4];
+ addr4[0] = addr46[12];
+ addr4[1] = addr46[13];
+ addr4[2] = addr46[14];
+ addr4[3] = addr46[15];
+
+ return parseIPv4Address(addr4);
+ }
+
// TODO: parse CTA_PROTOINFO of conntrack event in ConntrackMonitor. For TCP, only add rules
// while TCP status is established.
@VisibleForTesting
class BpfConntrackEventConsumer implements ConntrackEventConsumer {
+ // The upstream4 and downstream4 rules are built as the following tables. Only raw ip
+ // upstream interface is supported. Note that the field "lastUsed" is only updated by
+ // BPF program which records the last used time for a given rule.
+ // TODO: support ether ip upstream interface.
+ //
+ // NAT network topology:
+ //
+ // public network (rawip) private network
+ // | UE |
+ // +------------+ V +------------+------------+ V +------------+
+ // | Sever +---------+ Upstream | Downstream +---------+ Client |
+ // +------------+ +------------+------------+ +------------+
+ //
+ // upstream4 key and value:
+ //
+ // +------+------------------------------------------------+
+ // | | TetherUpstream4Key |
+ // +------+------+------+------+------+------+------+------+
+ // |field |iif |dstMac|l4prot|src4 |dst4 |srcPor|dstPor|
+ // | | | |o | | |t |t |
+ // +------+------+------+------+------+------+------+------+
+ // |value |downst|downst|tcp/ |client|server|client|server|
+ // | |ream |ream |udp | | | | |
+ // +------+------+------+------+------+------+------+------+
+ //
+ // +------+---------------------------------------------------------------------+
+ // | | TetherUpstream4Value |
+ // +------+------+------+------+------+------+------+------+------+------+------+
+ // |field |oif |ethDst|ethSrc|ethPro|pmtu |src46 |dst46 |srcPor|dstPor|lastUs|
+ // | | |mac |mac |to | | | |t |t |ed |
+ // +------+------+------+------+------+------+------+------+------+------+------+
+ // |value |upstre|-- |-- |ETH_P_|1500 |upstre|server|upstre|server|-- |
+ // | |am | | |IP | |am | |am | | |
+ // +------+------+------+------+------+------+------+------+------+------+------+
+ //
+ // downstream4 key and value:
+ //
+ // +------+------------------------------------------------+
+ // | | TetherDownstream4Key |
+ // +------+------+------+------+------+------+------+------+
+ // |field |iif |dstMac|l4prot|src4 |dst4 |srcPor|dstPor|
+ // | | | |o | | |t |t |
+ // +------+------+------+------+------+------+------+------+
+ // |value |upstre|-- |tcp/ |server|upstre|server|upstre|
+ // | |am | |udp | |am | |am |
+ // +------+------+------+------+------+------+------+------+
+ //
+ // +------+---------------------------------------------------------------------+
+ // | | TetherDownstream4Value |
+ // +------+------+------+------+------+------+------+------+------+------+------+
+ // |field |oif |ethDst|ethSrc|ethPro|pmtu |src46 |dst46 |srcPor|dstPor|lastUs|
+ // | | |mac |mac |to | | | |t |t |ed |
+ // +------+------+------+------+------+------+------+------+------+------+------+
+ // |value |downst|client|downst|ETH_P_|1500 |server|client|server|client|-- |
+ // | |ream | |ream |IP | | | | | | |
+ // +------+------+------+------+------+------+------+------+------+------+------+
+ //
@NonNull
private Tether4Key makeTetherUpstream4Key(
@NonNull ConntrackEvent e, @NonNull ClientInfo c) {
@@ -1292,19 +1557,6 @@
0 /* lastUsed, filled by bpf prog only */);
}
- @NonNull
- private byte[] toIpv4MappedAddressBytes(Inet4Address ia4) {
- final byte[] addr4 = ia4.getAddress();
- final byte[] addr6 = new byte[16];
- addr6[10] = (byte) 0xff;
- addr6[11] = (byte) 0xff;
- addr6[12] = addr4[0];
- addr6[13] = addr4[1];
- addr6[14] = addr4[2];
- addr6[15] = addr4[3];
- return addr6;
- }
-
public void accept(ConntrackEvent e) {
final ClientInfo tetherClient = getClientInfo(e.tupleOrig.srcIp);
if (tetherClient == null) return;
@@ -1318,8 +1570,23 @@
if (e.msgType == (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8
| NetlinkConstants.IPCTNL_MSG_CT_DELETE)) {
- mBpfCoordinatorShim.tetherOffloadRuleRemove(UPSTREAM, upstream4Key);
- mBpfCoordinatorShim.tetherOffloadRuleRemove(DOWNSTREAM, downstream4Key);
+ final boolean deletedUpstream = mBpfCoordinatorShim.tetherOffloadRuleRemove(
+ UPSTREAM, upstream4Key);
+ final boolean deletedDownstream = mBpfCoordinatorShim.tetherOffloadRuleRemove(
+ DOWNSTREAM, downstream4Key);
+
+ if (!deletedUpstream && !deletedDownstream) {
+ // The rules may have been already removed by losing client or losing upstream.
+ return;
+ }
+
+ if (deletedUpstream != deletedDownstream) {
+ Log.wtf(TAG, "The bidirectional rules should be removed concurrently ("
+ + "upstream: " + deletedUpstream
+ + ", downstream: " + deletedDownstream + ")");
+ return;
+ }
+
maybeClearLimit(upstreamIndex);
return;
}
@@ -1585,14 +1852,89 @@
return Math.max(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, configInterval);
}
+ @Nullable
+ private Inet4Address parseIPv4Address(byte[] addrBytes) {
+ try {
+ final InetAddress ia = Inet4Address.getByAddress(addrBytes);
+ if (ia instanceof Inet4Address) return (Inet4Address) ia;
+ } catch (UnknownHostException | IllegalArgumentException e) {
+ mLog.e("Failed to parse IPv4 address: " + e);
+ }
+ return null;
+ }
+
+ // Update CTA_TUPLE_ORIG timeout for a given conntrack entry. Note that there will also be
+ // coming a conntrack event to notify updated timeout.
+ private void updateConntrackTimeout(byte proto, Inet4Address src4, short srcPort,
+ Inet4Address dst4, short dstPort) {
+ if (src4 == null || dst4 == null) return;
+
+ // TODO: consider acquiring the timeout setting from nf_conntrack_* variables.
+ // - proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established
+ // - proc/sys/net/netfilter/nf_conntrack_udp_timeout_stream
+ // See kernel document nf_conntrack-sysctl.txt.
+ final int timeoutSec = (proto == OsConstants.IPPROTO_TCP)
+ ? NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED
+ : NF_CONNTRACK_UDP_TIMEOUT_STREAM;
+ final byte[] msg = ConntrackMessage.newIPv4TimeoutUpdateRequest(
+ proto, src4, (int) srcPort, dst4, (int) dstPort, timeoutSec);
+ try {
+ NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_NETFILTER, msg);
+ } catch (ErrnoException e) {
+ mLog.e("Error updating conntrack entry ("
+ + "proto: " + proto + ", "
+ + "src4: " + src4 + ", "
+ + "srcPort: " + Short.toUnsignedInt(srcPort) + ", "
+ + "dst4: " + dst4 + ", "
+ + "dstPort: " + Short.toUnsignedInt(dstPort) + "), "
+ + "msg: " + NetlinkConstants.hexify(msg) + ", "
+ + "e: " + e);
+ }
+ }
+
+ private void maybeRefreshConntrackTimeout() {
+ final long now = mDeps.elapsedRealtimeNanos();
+
+ // Reverse the source and destination {address, port} from downstream value because
+ // #updateConntrackTimeout refresh the timeout of netlink attribute CTA_TUPLE_ORIG
+ // which is opposite direction for downstream map value.
+ mBpfCoordinatorShim.tetherOffloadRuleForEach(DOWNSTREAM, (k, v) -> {
+ if ((now - v.lastUsed) / 1_000_000 < POLLING_CONNTRACK_TIMEOUT_MS) {
+ updateConntrackTimeout((byte) k.l4proto,
+ ipv4MappedAddressBytesToIpv4Address(v.dst46), (short) v.dstPort,
+ ipv4MappedAddressBytesToIpv4Address(v.src46), (short) v.srcPort);
+ }
+ });
+
+ // TODO: Consider ignoring TCP traffic on upstream and monitor on downstream only
+ // because TCP is a bidirectional traffic. Probably don't need to extend timeout by
+ // both directions for TCP.
+ mBpfCoordinatorShim.tetherOffloadRuleForEach(UPSTREAM, (k, v) -> {
+ if ((now - v.lastUsed) / 1_000_000 < POLLING_CONNTRACK_TIMEOUT_MS) {
+ updateConntrackTimeout((byte) k.l4proto, parseIPv4Address(k.src4),
+ (short) k.srcPort, parseIPv4Address(k.dst4), (short) k.dstPort);
+ }
+ });
+ }
+
private void maybeSchedulePollingStats() {
if (!mPollingStarted) return;
- if (mHandler.hasCallbacks(mScheduledPollingTask)) {
- mHandler.removeCallbacks(mScheduledPollingTask);
+ if (mHandler.hasCallbacks(mScheduledPollingStats)) {
+ mHandler.removeCallbacks(mScheduledPollingStats);
}
- mHandler.postDelayed(mScheduledPollingTask, getPollingInterval());
+ mHandler.postDelayed(mScheduledPollingStats, getPollingInterval());
+ }
+
+ private void maybeSchedulePollingConntrackTimeout() {
+ if (!mPollingStarted) return;
+
+ if (mHandler.hasCallbacks(mScheduledPollingConntrackTimeout)) {
+ mHandler.removeCallbacks(mScheduledPollingConntrackTimeout);
+ }
+
+ mHandler.postDelayed(mScheduledPollingConntrackTimeout, POLLING_CONNTRACK_TIMEOUT_MS);
}
// Return forwarding rule map. This is used for testing only.
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index b52ec86..e91ae85 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -1039,7 +1039,7 @@
final boolean rndisEnabled = intent.getBooleanExtra(USB_FUNCTION_RNDIS, false);
final boolean ncmEnabled = intent.getBooleanExtra(USB_FUNCTION_NCM, false);
- mLog.log(String.format("USB bcast connected:%s configured:%s rndis:%s ncm:%s",
+ mLog.i(String.format("USB bcast connected:%s configured:%s rndis:%s ncm:%s",
usbConnected, usbConfigured, rndisEnabled, ncmEnabled));
// There are three types of ACTION_USB_STATE:
@@ -1416,7 +1416,7 @@
// If TETHERING_USB is forced to use ncm function, TETHERING_NCM would no longer be
// available.
- if (mConfig.isUsingNcm()) return TETHER_ERROR_SERVICE_UNAVAIL;
+ if (mConfig.isUsingNcm() && enable) return TETHER_ERROR_SERVICE_UNAVAIL;
UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
usbManager.setCurrentFunctions(enable ? UsbManager.FUNCTION_NCM : UsbManager.FUNCTION_NONE);
@@ -1720,13 +1720,7 @@
protected void handleNewUpstreamNetworkState(UpstreamNetworkState ns) {
mIPv6TetheringCoordinator.updateUpstreamNetworkState(ns);
mOffload.updateUpstreamNetworkState(ns);
-
- // TODO: Delete all related offload rules which are using this upstream.
- if (ns != null) {
- // Add upstream index to the map. The upstream interface index is required while
- // the conntrack event builds the offload rules.
- mBpfCoordinator.addUpstreamIfindexToMap(ns.linkProperties);
- }
+ mBpfCoordinator.updateUpstreamNetworkState(ns);
}
private void handleInterfaceServingStateActive(int mode, IpServer who) {
@@ -2558,7 +2552,7 @@
return;
}
- mLog.log("adding IpServer for: " + iface);
+ mLog.i("adding IpServer for: " + iface);
final TetherState tetherState = new TetherState(
new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
makeControlCallback(), mConfig.enableLegacyDhcpServer,
@@ -2573,7 +2567,7 @@
if (tetherState == null) return;
tetherState.ipServer.stop();
- mLog.log("removing IpServer for: " + iface);
+ mLog.i("removing IpServer for: " + iface);
mTetherStates.remove(iface);
}
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index ce69cb3..378a21c 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -584,6 +584,7 @@
inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
inOrder.verify(mAddressCoordinator).releaseDownstream(any());
+ inOrder.verify(mBpfCoordinator).tetherOffloadClientClear(mIpServer);
inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer);
inOrder.verify(mCallback).updateInterfaceState(
mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index cc912f4..914e0d4 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -36,9 +36,13 @@
import static android.system.OsConstants.ETH_P_IPV6;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.NETLINK_NETFILTER;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
+import static com.android.networkstack.tethering.BpfCoordinator.NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED;
+import static com.android.networkstack.tethering.BpfCoordinator.NF_CONNTRACK_UDP_TIMEOUT_STREAM;
+import static com.android.networkstack.tethering.BpfCoordinator.POLLING_CONNTRACK_TIMEOUT_MS;
import static com.android.networkstack.tethering.BpfCoordinator.StatsType;
import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE;
import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID;
@@ -70,13 +74,17 @@
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.MacAddress;
+import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.NetworkStats;
import android.net.TetherOffloadRuleParcel;
import android.net.TetherStatsParcel;
import android.net.ip.ConntrackMonitor;
import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
import android.net.ip.IpServer;
+import android.net.netlink.ConntrackMessage;
import android.net.netlink.NetlinkConstants;
+import android.net.netlink.NetlinkSocket;
import android.net.util.InterfaceParams;
import android.net.util.SharedLog;
import android.os.Build;
@@ -127,6 +135,8 @@
@Rule
public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+ private static final int TEST_NET_ID = 24;
+
private static final int UPSTREAM_IFINDEX = 1001;
private static final int DOWNSTREAM_IFINDEX = 1002;
@@ -217,6 +227,7 @@
// it has to access the non-static function of BPF coordinator.
private BpfConntrackEventConsumer mConsumer;
+ private long mElapsedRealtimeNanos = 0;
private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
ArgumentCaptor.forClass(ArrayList.class);
private final TestLooper mTestLooper = new TestLooper();
@@ -256,6 +267,10 @@
return mConntrackMonitor;
}
+ public long elapsedRealtimeNanos() {
+ return mElapsedRealtimeNanos;
+ }
+
@Nullable
public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
return mBpfDownstream4Map;
@@ -1340,6 +1355,11 @@
}
@NonNull
+ private Tether4Key makeDownstream4Key() {
+ return makeDownstream4Key(IPPROTO_TCP);
+ }
+
+ @NonNull
private ConntrackEvent makeTestConntrackEvent(short msgType, int proto) {
if (msgType != IPCTNL_MSG_CT_NEW && msgType != IPCTNL_MSG_CT_DELETE) {
fail("Not support message type " + msgType);
@@ -1365,7 +1385,10 @@
final LinkProperties lp = new LinkProperties();
lp.setInterfaceName(UPSTREAM_IFACE);
lp.addLinkAddress(new LinkAddress(PUBLIC_ADDR, 32 /* prefix length */));
- coordinator.addUpstreamIfindexToMap(lp);
+ final NetworkCapabilities capabilities = new NetworkCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+ coordinator.updateUpstreamNetworkState(new UpstreamNetworkState(lp, capabilities,
+ new Network(TEST_NET_ID)));
}
private void setDownstreamAndClientInformationTo(final BpfCoordinator coordinator) {
@@ -1379,8 +1402,11 @@
// was started.
coordinator.startPolling();
- // Needed because tetherOffloadRuleRemove of api31.BpfCoordinatorShimImpl only decreases
- // the count while the entry is deleted. In the other words, deleteEntry returns true.
+ // Needed because two reasons: (1) BpfConntrackEventConsumer#accept only performs cleanup
+ // when both upstream and downstream rules are removed. (2) tetherOffloadRuleRemove of
+ // api31.BpfCoordinatorShimImpl only decreases the count while the entry is deleted.
+ // In the other words, deleteEntry returns true.
+ doReturn(true).when(mBpfUpstream4Map).deleteEntry(any());
doReturn(true).when(mBpfDownstream4Map).deleteEntry(any());
// Needed because BpfCoordinator#addUpstreamIfindexToMap queries interface parameter for
@@ -1494,4 +1520,104 @@
mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP));
verify(mBpfDevMap, never()).updateEntry(any(), any());
}
+
+ private void setElapsedRealtimeNanos(long nanoSec) {
+ mElapsedRealtimeNanos = nanoSec;
+ }
+
+ private void checkRefreshConntrackTimeout(final TestBpfMap<Tether4Key, Tether4Value> bpfMap,
+ final Tether4Key tcpKey, final Tether4Value tcpValue, final Tether4Key udpKey,
+ final Tether4Value udpValue) throws Exception {
+ // Both system elapsed time since boot and the rule last used time are used to measure
+ // the rule expiration. In this test, all test rules are fixed the last used time to 0.
+ // Set the different testing elapsed time to make the rule to be valid or expired.
+ //
+ // Timeline:
+ // 0 60 (seconds)
+ // +---+---+---+---+--...--+---+---+---+---+---+- ..
+ // | POLLING_CONNTRACK_TIMEOUT_MS |
+ // +---+---+---+---+--...--+---+---+---+---+---+- ..
+ // |<- valid diff ->|
+ // |<- expired diff ->|
+ // ^ ^ ^
+ // last used time elapsed time (valid) elapsed time (expired)
+ final long validTime = (POLLING_CONNTRACK_TIMEOUT_MS - 1) * 1_000_000L;
+ final long expiredTime = (POLLING_CONNTRACK_TIMEOUT_MS + 1) * 1_000_000L;
+
+ // Static mocking for NetlinkSocket.
+ MockitoSession mockSession = ExtendedMockito.mockitoSession()
+ .mockStatic(NetlinkSocket.class)
+ .startMocking();
+ try {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ coordinator.startPolling();
+ bpfMap.insertEntry(tcpKey, tcpValue);
+ bpfMap.insertEntry(udpKey, udpValue);
+
+ // [1] Don't refresh contrack timeout.
+ setElapsedRealtimeNanos(expiredTime);
+ mTestLooper.moveTimeForward(POLLING_CONNTRACK_TIMEOUT_MS);
+ waitForIdle();
+ ExtendedMockito.verifyNoMoreInteractions(staticMockMarker(NetlinkSocket.class));
+ ExtendedMockito.clearInvocations(staticMockMarker(NetlinkSocket.class));
+
+ // [2] Refresh contrack timeout.
+ setElapsedRealtimeNanos(validTime);
+ mTestLooper.moveTimeForward(POLLING_CONNTRACK_TIMEOUT_MS);
+ waitForIdle();
+ final byte[] expectedNetlinkTcp = ConntrackMessage.newIPv4TimeoutUpdateRequest(
+ IPPROTO_TCP, PRIVATE_ADDR, (int) PRIVATE_PORT, REMOTE_ADDR,
+ (int) REMOTE_PORT, NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED);
+ final byte[] expectedNetlinkUdp = ConntrackMessage.newIPv4TimeoutUpdateRequest(
+ IPPROTO_UDP, PRIVATE_ADDR, (int) PRIVATE_PORT, REMOTE_ADDR,
+ (int) REMOTE_PORT, NF_CONNTRACK_UDP_TIMEOUT_STREAM);
+ ExtendedMockito.verify(() -> NetlinkSocket.sendOneShotKernelMessage(
+ eq(NETLINK_NETFILTER), eq(expectedNetlinkTcp)));
+ ExtendedMockito.verify(() -> NetlinkSocket.sendOneShotKernelMessage(
+ eq(NETLINK_NETFILTER), eq(expectedNetlinkUdp)));
+ ExtendedMockito.verifyNoMoreInteractions(staticMockMarker(NetlinkSocket.class));
+ ExtendedMockito.clearInvocations(staticMockMarker(NetlinkSocket.class));
+
+ // [3] Don't refresh contrack timeout if polling stopped.
+ coordinator.stopPolling();
+ mTestLooper.moveTimeForward(POLLING_CONNTRACK_TIMEOUT_MS);
+ waitForIdle();
+ ExtendedMockito.verifyNoMoreInteractions(staticMockMarker(NetlinkSocket.class));
+ ExtendedMockito.clearInvocations(staticMockMarker(NetlinkSocket.class));
+ } finally {
+ mockSession.finishMocking();
+ }
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testRefreshConntrackTimeout_Upstream4Map() throws Exception {
+ // TODO: Replace the dependencies BPF map with a non-mocked TestBpfMap object.
+ final TestBpfMap<Tether4Key, Tether4Value> bpfUpstream4Map =
+ new TestBpfMap<>(Tether4Key.class, Tether4Value.class);
+ doReturn(bpfUpstream4Map).when(mDeps).getBpfUpstream4Map();
+
+ final Tether4Key tcpKey = makeUpstream4Key(IPPROTO_TCP);
+ final Tether4Key udpKey = makeUpstream4Key(IPPROTO_UDP);
+ final Tether4Value tcpValue = makeUpstream4Value();
+ final Tether4Value udpValue = makeUpstream4Value();
+
+ checkRefreshConntrackTimeout(bpfUpstream4Map, tcpKey, tcpValue, udpKey, udpValue);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testRefreshConntrackTimeout_Downstream4Map() throws Exception {
+ // TODO: Replace the dependencies BPF map with a non-mocked TestBpfMap object.
+ final TestBpfMap<Tether4Key, Tether4Value> bpfDownstream4Map =
+ new TestBpfMap<>(Tether4Key.class, Tether4Value.class);
+ doReturn(bpfDownstream4Map).when(mDeps).getBpfDownstream4Map();
+
+ final Tether4Key tcpKey = makeDownstream4Key(IPPROTO_TCP);
+ final Tether4Key udpKey = makeDownstream4Key(IPPROTO_UDP);
+ final Tether4Value tcpValue = makeDownstream4Value();
+ final Tether4Value udpValue = makeDownstream4Value();
+
+ checkRefreshConntrackTimeout(bpfDownstream4Map, tcpKey, tcpValue, udpKey, udpValue);
+ }
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
index b8389ea..e692015 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
@@ -20,6 +20,8 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static com.android.networkstack.apishim.common.ShimUtils.isAtLeastS;
+
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
@@ -68,10 +70,10 @@
public static final boolean BROADCAST_FIRST = false;
public static final boolean CALLBACKS_FIRST = true;
- final Map<NetworkCallback, NetworkCallbackInfo> mAllCallbacks = new ArrayMap<>();
+ final Map<NetworkCallback, Handler> mAllCallbacks = new ArrayMap<>();
// This contains the callbacks tracking the system default network, whether it's registered
// with registerSystemDefaultNetworkCallback (S+) or with a custom request (R-).
- final Map<NetworkCallback, NetworkCallbackInfo> mTrackingDefault = new ArrayMap<>();
+ final Map<NetworkCallback, Handler> mTrackingDefault = new ArrayMap<>();
final Map<NetworkCallback, NetworkRequestInfo> mListening = new ArrayMap<>();
final Map<NetworkCallback, NetworkRequestInfo> mRequested = new ArrayMap<>();
final Map<NetworkCallback, Integer> mLegacyTypeMap = new ArrayMap<>();
@@ -92,18 +94,12 @@
mContext = ctx;
}
- static class NetworkCallbackInfo {
- public final Handler handler;
- NetworkCallbackInfo(Handler h) {
- handler = h;
- }
- }
-
- static class NetworkRequestInfo extends NetworkCallbackInfo {
+ static class NetworkRequestInfo {
public final NetworkRequest request;
+ public final Handler handler;
NetworkRequestInfo(NetworkRequest r, Handler h) {
- super(h);
request = r;
+ handler = h;
}
}
@@ -152,15 +148,15 @@
private void sendDefaultNetworkCallbacks(TestNetworkAgent formerDefault,
TestNetworkAgent defaultNetwork) {
for (NetworkCallback cb : mTrackingDefault.keySet()) {
- final NetworkCallbackInfo nri = mTrackingDefault.get(cb);
+ final Handler handler = mTrackingDefault.get(cb);
if (defaultNetwork != null) {
- nri.handler.post(() -> cb.onAvailable(defaultNetwork.networkId));
- nri.handler.post(() -> cb.onCapabilitiesChanged(
+ handler.post(() -> cb.onAvailable(defaultNetwork.networkId));
+ handler.post(() -> cb.onCapabilitiesChanged(
defaultNetwork.networkId, defaultNetwork.networkCapabilities));
- nri.handler.post(() -> cb.onLinkPropertiesChanged(
+ handler.post(() -> cb.onLinkPropertiesChanged(
defaultNetwork.networkId, defaultNetwork.linkProperties));
} else if (formerDefault != null) {
- nri.handler.post(() -> cb.onLost(formerDefault.networkId));
+ handler.post(() -> cb.onLost(formerDefault.networkId));
}
}
}
@@ -201,10 +197,11 @@
// For R- devices, Tethering will invoke this function in 2 cases, one is to request mobile
// network, the other is to track system default network.
if (looksLikeDefaultRequest(req)) {
- registerSystemDefaultNetworkCallback(cb, h);
+ assertFalse(isAtLeastS());
+ addTrackDefaultCallback(cb, h);
} else {
assertFalse(mAllCallbacks.containsKey(cb));
- mAllCallbacks.put(cb, new NetworkRequestInfo(req, h));
+ mAllCallbacks.put(cb, h);
assertFalse(mRequested.containsKey(cb));
mRequested.put(cb, new NetworkRequestInfo(req, h));
}
@@ -213,10 +210,14 @@
@Override
public void registerSystemDefaultNetworkCallback(
@NonNull NetworkCallback cb, @NonNull Handler h) {
+ addTrackDefaultCallback(cb, h);
+ }
+
+ private void addTrackDefaultCallback(@NonNull NetworkCallback cb, @NonNull Handler h) {
assertFalse(mAllCallbacks.containsKey(cb));
- mAllCallbacks.put(cb, new NetworkCallbackInfo(h));
+ mAllCallbacks.put(cb, h);
assertFalse(mTrackingDefault.containsKey(cb));
- mTrackingDefault.put(cb, new NetworkCallbackInfo(h));
+ mTrackingDefault.put(cb, h);
}
@Override
@@ -230,7 +231,7 @@
assertFalse(mAllCallbacks.containsKey(cb));
NetworkRequest newReq = new NetworkRequest(req.networkCapabilities, legacyType,
-1 /** testId */, req.type);
- mAllCallbacks.put(cb, new NetworkRequestInfo(newReq, h));
+ mAllCallbacks.put(cb, h);
assertFalse(mRequested.containsKey(cb));
mRequested.put(cb, new NetworkRequestInfo(newReq, h));
assertFalse(mLegacyTypeMap.containsKey(cb));
@@ -242,7 +243,7 @@
@Override
public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) {
assertFalse(mAllCallbacks.containsKey(cb));
- mAllCallbacks.put(cb, new NetworkRequestInfo(req, h));
+ mAllCallbacks.put(cb, h);
assertFalse(mListening.containsKey(cb));
mListening.put(cb, new NetworkRequestInfo(req, h));
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index af28dd7..9e0c880 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -2610,12 +2610,57 @@
reset(mBluetoothAdapter, mBluetoothPan);
}
+ private void runDualStackUsbTethering(final String expectedIface) throws Exception {
+ when(mNetd.interfaceGetList()).thenReturn(new String[] {expectedIface});
+ when(mRouterAdvertisementDaemon.start())
+ .thenReturn(true);
+ final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
+ runUsbTethering(upstreamState);
+
+ verify(mNetd).interfaceGetList();
+ verify(mNetd).tetherAddForward(expectedIface, TEST_MOBILE_IFNAME);
+ verify(mNetd).ipfwdAddInterfaceForward(expectedIface, TEST_MOBILE_IFNAME);
+
+ verify(mRouterAdvertisementDaemon).start();
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
+ any(), any());
+ sendIPv6TetherUpdates(upstreamState);
+ assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */);
+ verify(mRouterAdvertisementDaemon).buildNewRa(any(), notNull());
+ verify(mNetd).tetherApplyDnsInterfaces();
+ }
+
+ private void forceUsbTetheringUse(final int function) {
+ Settings.Global.putInt(mContentResolver, TETHER_FORCE_USB_FUNCTIONS, function);
+ final ContentObserver observer = mTethering.getSettingsObserverForTest();
+ observer.onChange(false /* selfChange */);
+ mLooper.dispatchAll();
+ }
+
+ private void verifyUsbTetheringStopDueToSettingChange(final String iface) {
+ verify(mUsbManager, times(2)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ mTethering.interfaceRemoved(iface);
+ sendUsbBroadcast(true, true, -1 /* no functions enabled */);
+ reset(mUsbManager, mNetd, mDhcpServer, mRouterAdvertisementDaemon,
+ mIPv6TetheringCoordinator, mDadProxy);
+ }
+
@Test
- public void testUsbTetheringWithNcmFunction() throws Exception {
- when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
- TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
+ public void testUsbFunctionConfigurationChange() throws Exception {
+ // Run TETHERING_NCM.
+ runNcmTethering();
+ verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
+ any(), any());
+
+ // Change the USB tethering function to NCM. Because the USB tethering function was set to
+ // RNDIS (the default), tethering is stopped.
+ forceUsbTetheringUse(TETHER_USB_NCM_FUNCTION);
+ verifyUsbTetheringStopDueToSettingChange(TEST_NCM_IFNAME);
+
+ // TODO: move this into setup after allowing configure TEST_NCM_REGEX into
+ // config_tether_usb_regexs and config_tether_ncm_regexs at the same time.
when(mResources.getStringArray(R.array.config_tether_usb_regexs))
- .thenReturn(new String[] {TEST_NCM_REGEX});
+ .thenReturn(new String[] {TEST_RNDIS_REGEX, TEST_NCM_REGEX});
sendConfigurationChanged();
// If TETHERING_USB is forced to use ncm function, TETHERING_NCM would no longer be
@@ -2625,30 +2670,16 @@
mLooper.dispatchAll();
ncmResult.assertHasResult();
- final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
- runUsbTethering(upstreamState);
+ // Run TETHERING_USB with ncm configuration.
+ runDualStackUsbTethering(TEST_NCM_IFNAME);
- verify(mNetd).interfaceGetList();
- verify(mNetd).tetherAddForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
- verify(mNetd).ipfwdAddInterfaceForward(TEST_NCM_IFNAME, TEST_MOBILE_IFNAME);
+ // Change configuration to rndis.
+ forceUsbTetheringUse(TETHER_USB_RNDIS_FUNCTION);
+ verifyUsbTetheringStopDueToSettingChange(TEST_NCM_IFNAME);
- verify(mRouterAdvertisementDaemon).start();
- verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
- any(), any());
- sendIPv6TetherUpdates(upstreamState);
- assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */);
- verify(mRouterAdvertisementDaemon).buildNewRa(any(), notNull());
- verify(mNetd).tetherApplyDnsInterfaces();
-
- Settings.Global.putInt(mContentResolver, TETHER_FORCE_USB_FUNCTIONS,
- TETHER_USB_RNDIS_FUNCTION);
- final ContentObserver observer = mTethering.getSettingsObserverForTest();
- observer.onChange(false /* selfChange */);
- mLooper.dispatchAll();
- // stop TETHERING_USB and TETHERING_NCM
- verify(mUsbManager, times(2)).setCurrentFunctions(UsbManager.FUNCTION_NONE);
- mTethering.interfaceRemoved(TEST_NCM_IFNAME);
- sendUsbBroadcast(true, true, -1 /* function */);
+ // Run TETHERING_USB with rndis configuration.
+ runDualStackUsbTethering(TEST_RNDIS_IFNAME);
+ runStopUSBTethering();
}
// TODO: Test that a request for hotspot mode doesn't interfere with an
// already operating tethering mode interface.
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 05bacfa..352d266 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -7597,9 +7597,16 @@
// If apps could file multi-layer requests with PendingIntents, they'd need to know
// which of the layer is satisfied alongside with some ID for the request. Hence, if
// such an API is ever implemented, there is no doubt the right request to send in
- // EXTRA_NETWORK_REQUEST is mActiveRequest, and whatever ID would be added would need to
- // be sent as a separate extra.
- intent.putExtra(ConnectivityManager.EXTRA_NETWORK_REQUEST, nri.getActiveRequest());
+ // EXTRA_NETWORK_REQUEST is the active request, and whatever ID would be added would
+ // need to be sent as a separate extra.
+ final NetworkRequest req = nri.isMultilayerRequest()
+ ? nri.getActiveRequest()
+ // Non-multilayer listen requests do not have an active request
+ : nri.mRequests.get(0);
+ if (req == null) {
+ Log.wtf(TAG, "No request in NRI " + nri);
+ }
+ intent.putExtra(ConnectivityManager.EXTRA_NETWORK_REQUEST, req);
nri.mPendingIntentSent = true;
sendIntent(nri.mPendingIntent, intent);
}
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
old mode 100644
new mode 100755
index 32e06e5..99118ac
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -230,11 +230,11 @@
boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
if (isNetwork || hasRestrictedPermission) {
- Boolean permission = mApps.get(uid);
+ Boolean permission = mApps.get(UserHandle.getAppId(uid));
// If multiple packages share a UID (cf: android:sharedUserId) and ask for different
// permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is).
if (permission == null || permission == NETWORK) {
- mApps.put(uid, hasRestrictedPermission);
+ mApps.put(UserHandle.getAppId(uid), hasRestrictedPermission);
}
}
@@ -325,14 +325,14 @@
// networks. mApps contains the result of checks for both hasNetworkPermission and
// hasRestrictedNetworkPermission. If uid is in the mApps list that means uid has one of
// permissions at least.
- return mApps.containsKey(uid);
+ return mApps.containsKey(UserHandle.getAppId(uid));
}
/**
* Returns whether the given uid has permission to use restricted networks.
*/
public synchronized boolean hasRestrictedNetworksPermission(int uid) {
- return Boolean.TRUE.equals(mApps.get(uid));
+ return Boolean.TRUE.equals(mApps.get(UserHandle.getAppId(uid)));
}
private void update(Set<UserHandle> users, Map<Integer, Boolean> apps, boolean add) {
@@ -452,12 +452,13 @@
// If multiple packages share a UID (cf: android:sharedUserId) and ask for different
// permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is).
- final Boolean permission = highestPermissionForUid(mApps.get(uid), packageName);
- if (permission != mApps.get(uid)) {
- mApps.put(uid, permission);
+ final int appId = UserHandle.getAppId(uid);
+ final Boolean permission = highestPermissionForUid(mApps.get(appId), packageName);
+ if (permission != mApps.get(appId)) {
+ mApps.put(appId, permission);
Map<Integer, Boolean> apps = new HashMap<>();
- apps.put(uid, permission);
+ apps.put(appId, permission);
update(mUsers, apps, true);
}
@@ -472,7 +473,7 @@
updateVpnUids(vpn.getKey(), changedUids, true);
}
}
- mAllApps.add(UserHandle.getAppId(uid));
+ mAllApps.add(appId);
}
private Boolean highestUidNetworkPermission(int uid) {
@@ -529,16 +530,17 @@
return;
}
- if (permission == mApps.get(uid)) {
+ final int appId = UserHandle.getAppId(uid);
+ if (permission == mApps.get(appId)) {
// The permissions of this UID have not changed. Nothing to do.
return;
} else if (permission != null) {
- mApps.put(uid, permission);
- apps.put(uid, permission);
+ mApps.put(appId, permission);
+ apps.put(appId, permission);
update(mUsers, apps, true);
} else {
- mApps.remove(uid);
- apps.put(uid, NETWORK); // doesn't matter which permission we pick here
+ mApps.remove(appId);
+ apps.put(appId, NETWORK); // doesn't matter which permission we pick here
update(mUsers, apps, false);
}
}
@@ -653,7 +655,7 @@
*/
private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
uids.remove(vpnAppUid);
- uids.removeIf(uid -> mApps.getOrDefault(uid, NETWORK) == SYSTEM);
+ uids.removeIf(uid -> mApps.getOrDefault(UserHandle.getAppId(uid), NETWORK) == SYSTEM);
}
/**
@@ -795,12 +797,13 @@
for (Integer uid : uidsToUpdate) {
final Boolean permission = highestUidNetworkPermission(uid);
+ final int appId = UserHandle.getAppId(uid);
if (null == permission) {
- removedUids.put(uid, NETWORK); // Doesn't matter which permission is set here.
- mApps.remove(uid);
+ removedUids.put(appId, NETWORK); // Doesn't matter which permission is set here.
+ mApps.remove(appId);
} else {
- updatedUids.put(uid, permission);
- mApps.put(uid, permission);
+ updatedUids.put(appId, permission);
+ mApps.put(appId, permission);
}
}
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 515c00c..28339f1 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -50,9 +50,6 @@
// TODO: change to 31 as soon as it is available
target_sdk_version: "30",
test_suites: ["device-tests", "mts"],
- test_mainline_modules: [
- "CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex",
- ],
defaults: [
"framework-connectivity-test-defaults",
"FrameworksNetTests-jni-defaults",
diff --git a/tests/common/AndroidTest_Coverage.xml b/tests/common/AndroidTest_Coverage.xml
index 577f36a..7c8e710 100644
--- a/tests/common/AndroidTest_Coverage.xml
+++ b/tests/common/AndroidTest_Coverage.xml
@@ -18,6 +18,7 @@
</target_preparer>
<option name="test-tag" value="ConnectivityCoverageTests" />
+ <option name="config-descriptor:metadata" key="mainline-param" value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.connectivity.tests.coverage" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index a358a38..8f471c1 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -72,7 +72,6 @@
import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
-import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
import static com.android.testutils.MiscAsserts.assertThrows;
@@ -530,8 +529,10 @@
Objects.requireNonNull(mCm.getNetworkCapabilities(network));
// Redact specifier of the capabilities of the snapshot before comparing since
// the result returned from getNetworkCapabilities always get redacted.
+ final NetworkSpecifier snapshotCapSpecifier =
+ snapshot.getNetworkCapabilities().getNetworkSpecifier();
final NetworkSpecifier redactedSnapshotCapSpecifier =
- snapshot.getNetworkCapabilities().getNetworkSpecifier().redact();
+ snapshotCapSpecifier == null ? null : snapshotCapSpecifier.redact();
assertEquals("", caps.describeImmutableDifferences(
snapshot.getNetworkCapabilities()
.setNetworkSpecifier(redactedSnapshotCapSpecifier)));
@@ -947,8 +948,6 @@
private void assertPendingIntentRequestMatches(NetworkRequest broadcasted, NetworkRequest filed,
boolean useListen) {
- // TODO: BUG (b/191713869): on S the request extra is null on listens
- if (isAtLeastS() && useListen && broadcasted == null) return;
assertArrayEquals(filed.networkCapabilities.getCapabilities(),
broadcasted.networkCapabilities.getCapabilities());
// TODO: BUG (b/189868426): this should also apply to listens
diff --git a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
index c54ee91..7f710d7 100644
--- a/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
+++ b/tests/cts/net/src/android/net/cts/IpSecBaseTest.java
@@ -23,6 +23,7 @@
import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA384;
import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA512;
import static android.net.IpSecAlgorithm.CRYPT_AES_CBC;
+import static android.system.OsConstants.FIONREAD;
import static org.junit.Assert.assertArrayEquals;
@@ -32,8 +33,10 @@
import android.net.IpSecManager;
import android.net.IpSecTransform;
import android.platform.test.annotations.AppModeFull;
+import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
+import android.system.StructTimeval;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
@@ -46,15 +49,21 @@
import org.junit.runner.RunWith;
import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
-import java.net.ServerSocket;
import java.net.Socket;
+import java.net.SocketAddress;
import java.net.SocketException;
+import java.net.SocketImpl;
+import java.net.SocketOptions;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@@ -232,6 +241,12 @@
public NativeTcpSocket(FileDescriptor fd) {
super(fd);
}
+
+ public JavaTcpSocket acceptToJavaSocket() throws Exception {
+ InetSocketAddress peer = new InetSocketAddress(0);
+ FileDescriptor newFd = Os.accept(mFd, peer);
+ return new JavaTcpSocket(new AcceptedTcpFileDescriptorSocket(newFd, peer, getPort()));
+ }
}
public static class NativeUdpSocket extends NativeSocket implements GenericUdpSocket {
@@ -357,6 +372,137 @@
}
}
+ private static class AcceptedTcpFileDescriptorSocket extends Socket {
+
+ AcceptedTcpFileDescriptorSocket(FileDescriptor fd, InetSocketAddress remote,
+ int localPort) throws IOException {
+ super(new FileDescriptorSocketImpl(fd, remote, localPort));
+ connect(remote);
+ }
+
+ private static class FileDescriptorSocketImpl extends SocketImpl {
+
+ private FileDescriptorSocketImpl(FileDescriptor fd, InetSocketAddress remote,
+ int localPort) {
+ this.fd = fd;
+ this.address = remote.getAddress();
+ this.port = remote.getPort();
+ this.localport = localPort;
+ }
+
+ @Override
+ protected void create(boolean stream) throws IOException {
+ // The socket has been created.
+ }
+
+ @Override
+ protected void connect(String host, int port) throws IOException {
+ // The socket has connected.
+ }
+
+ @Override
+ protected void connect(InetAddress address, int port) throws IOException {
+ // The socket has connected.
+ }
+
+ @Override
+ protected void connect(SocketAddress address, int timeout) throws IOException {
+ // The socket has connected.
+ }
+
+ @Override
+ protected void bind(InetAddress host, int port) throws IOException {
+ // The socket is bounded.
+ }
+
+ @Override
+ protected void listen(int backlog) throws IOException {
+ throw new UnsupportedOperationException("listen");
+ }
+
+ @Override
+ protected void accept(SocketImpl s) throws IOException {
+ throw new UnsupportedOperationException("accept");
+ }
+
+ @Override
+ protected InputStream getInputStream() throws IOException {
+ return new FileInputStream(fd);
+ }
+
+ @Override
+ protected OutputStream getOutputStream() throws IOException {
+ return new FileOutputStream(fd);
+ }
+
+ @Override
+ protected int available() throws IOException {
+ try {
+ return Os.ioctlInt(fd, FIONREAD);
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ protected void close() throws IOException {
+ try {
+ Os.close(fd);
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ protected void sendUrgentData(int data) throws IOException {
+ throw new UnsupportedOperationException("sendUrgentData");
+ }
+
+ @Override
+ public void setOption(int optID, Object value) throws SocketException {
+ try {
+ setOptionInternal(optID, value);
+ } catch (ErrnoException e) {
+ throw new SocketException(e.getMessage());
+ }
+ }
+
+ private void setOptionInternal(int optID, Object value) throws ErrnoException,
+ SocketException {
+ switch(optID) {
+ case SocketOptions.SO_TIMEOUT:
+ int millis = (Integer) value;
+ StructTimeval tv = StructTimeval.fromMillis(millis);
+ Os.setsockoptTimeval(fd, OsConstants.SOL_SOCKET, OsConstants.SO_RCVTIMEO,
+ tv);
+ return;
+ default:
+ throw new SocketException("Unknown socket option: " + optID);
+ }
+ }
+
+ @Override
+ public Object getOption(int optID) throws SocketException {
+ try {
+ return getOptionInternal(optID);
+ } catch (ErrnoException e) {
+ throw new SocketException(e.getMessage());
+ }
+ }
+
+ private Object getOptionInternal(int optID) throws ErrnoException, SocketException {
+ switch (optID) {
+ case SocketOptions.SO_LINGER:
+ // Returns an arbitrary value because IpSecManager doesn't actually
+ // use this value.
+ return 10;
+ default:
+ throw new SocketException("Unknown socket option: " + optID);
+ }
+ }
+ }
+ }
+
public static class SocketPair<T> {
public final T mLeftSock;
public final T mRightSock;
@@ -441,8 +587,6 @@
public static SocketPair<JavaTcpSocket> getJavaTcpSocketPair(
InetAddress localAddr, IpSecManager ism, IpSecTransform transform) throws Exception {
JavaTcpSocket clientSock = new JavaTcpSocket(new Socket());
- ServerSocket serverSocket = new ServerSocket();
- serverSocket.bind(new InetSocketAddress(localAddr, 0));
// While technically the client socket does not need to be bound, the OpenJDK implementation
// of Socket only allocates an FD when bind() or connect() or other similar methods are
@@ -451,16 +595,19 @@
clientSock.mSocket.bind(new InetSocketAddress(localAddr, 0));
// IpSecService doesn't support serverSockets at the moment; workaround using FD
- FileDescriptor serverFd = serverSocket.getImpl().getFD$();
+ NativeTcpSocket server = new NativeTcpSocket(
+ Os.socket(getDomain(localAddr), OsConstants.SOCK_STREAM, OsConstants.IPPROTO_TCP));
+ Os.bind(server.mFd, localAddr, 0);
- applyTransformBidirectionally(ism, transform, new NativeTcpSocket(serverFd));
+ applyTransformBidirectionally(ism, transform, server);
applyTransformBidirectionally(ism, transform, clientSock);
- clientSock.mSocket.connect(new InetSocketAddress(localAddr, serverSocket.getLocalPort()));
- JavaTcpSocket acceptedSock = new JavaTcpSocket(serverSocket.accept());
+ Os.listen(server.mFd, 10 /* backlog */);
+ clientSock.mSocket.connect(new InetSocketAddress(localAddr, server.getPort()));
+ JavaTcpSocket acceptedSock = server.acceptToJavaSocket();
applyTransformBidirectionally(ism, transform, acceptedSock);
- serverSocket.close();
+ server.close();
return new SocketPair<>(clientSock, acceptedSock);
}
diff --git a/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt b/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt
new file mode 100644
index 0000000..8f17199
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NetworkScoreTest.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2021 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 android.net.cts
+
+import android.Manifest.permission.MANAGE_TEST_NETWORKS
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.LinkProperties
+import android.net.NetworkAgent
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.VpnManager
+import android.net.VpnTransportInfo
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.test.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.TestableNetworkCallback.HasNetwork
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// This test doesn't really have a constraint on how fast the methods should return. If it's
+// going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
+// without affecting the run time of successful runs. Thus, set a very high timeout.
+private const val TIMEOUT_MS = 30_000L
+// When waiting for a NetworkCallback to determine there was no timeout, waiting is the
+// only possible thing (the relevant handler is the one in the real ConnectivityService,
+// and then there is the Binder call), so have a short timeout for this as it will be
+// exhausted every time.
+private const val NO_CALLBACK_TIMEOUT = 200L
+
+private val testContext: Context
+ get() = InstrumentationRegistry.getContext()
+
+private fun score(exiting: Boolean = false, primary: Boolean = false) =
+ NetworkScore.Builder().setExiting(exiting).setTransportPrimary(primary)
+ // TODO : have a constant KEEP_CONNECTED_FOR_TEST ?
+ .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_FOR_HANDOVER)
+ .build()
+
+@IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkScoreTest {
+ private val mCm = testContext.getSystemService(ConnectivityManager::class.java)
+ private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread")
+ private val mHandler by lazy { Handler(mHandlerThread.looper) }
+ private val agentsToCleanUp = mutableListOf<NetworkAgent>()
+ private val callbacksToCleanUp = mutableListOf<TestableNetworkCallback>()
+
+ @Before
+ fun setUp() {
+ mHandlerThread.start()
+ }
+
+ @After
+ fun tearDown() {
+ agentsToCleanUp.forEach { it.unregister() }
+ mHandlerThread.quitSafely()
+ callbacksToCleanUp.forEach { mCm.unregisterNetworkCallback(it) }
+ }
+
+ // Returns a networkCallback that sends onAvailable on the best network with TRANSPORT_TEST.
+ private fun makeTestNetworkCallback() = TestableNetworkCallback(TIMEOUT_MS).also { cb ->
+ mCm.registerBestMatchingNetworkCallback(NetworkRequest.Builder().clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST).build(), cb, mHandler)
+ callbacksToCleanUp.add(cb)
+ }
+
+ // TestNetworkCallback is made to interact with a wrapper of NetworkAgent, because it's
+ // made for ConnectivityServiceTest.
+ // TODO : have TestNetworkCallback work for NetworkAgent too and remove this class.
+ private class AgentWrapper(val agent: NetworkAgent) : HasNetwork {
+ override val network = agent.network
+ fun sendNetworkScore(s: NetworkScore) = agent.sendNetworkScore(s)
+ }
+
+ private fun createTestNetworkAgent(
+ // The network always has TRANSPORT_TEST, plus optional transports
+ optionalTransports: IntArray = IntArray(size = 0),
+ everUserSelected: Boolean = false,
+ acceptUnvalidated: Boolean = false,
+ isExiting: Boolean = false,
+ isPrimary: Boolean = false
+ ): AgentWrapper {
+ val nc = NetworkCapabilities.Builder().apply {
+ addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ optionalTransports.forEach { addTransportType(it) }
+ // Add capabilities that are common, just for realism. It's not strictly necessary
+ addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+ // Remove capabilities that a test network agent shouldn't have and that are not
+ // needed for the purposes of this test.
+ removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+ removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ if (optionalTransports.contains(NetworkCapabilities.TRANSPORT_VPN)) {
+ addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+ removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ setTransportInfo(VpnTransportInfo(VpnManager.TYPE_VPN_SERVICE, null))
+ }
+ addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+ }.build()
+ val config = NetworkAgentConfig.Builder()
+ .setExplicitlySelected(everUserSelected)
+ .setUnvalidatedConnectivityAcceptable(acceptUnvalidated)
+ .build()
+ val score = score(exiting = isExiting, primary = isPrimary)
+ val context = testContext
+ val looper = mHandlerThread.looper
+ val agent = object : NetworkAgent(context, looper, "NetworkScore test agent", nc,
+ LinkProperties(), score, config, NetworkProvider(context, looper,
+ "NetworkScore test provider")) {}.also {
+ agentsToCleanUp.add(it)
+ }
+ runWithShellPermissionIdentity({ agent.register() }, MANAGE_TEST_NETWORKS)
+ agent.markConnected()
+ return AgentWrapper(agent)
+ }
+
+ @Test
+ fun testExitingLosesAndOldSatisfierWins() {
+ val cb = makeTestNetworkCallback()
+ val agent1 = createTestNetworkAgent()
+ cb.expectAvailableThenValidatedCallbacks(agent1)
+ val agent2 = createTestNetworkAgent()
+ // Because the existing network must win, the callback stays on agent1.
+ cb.assertNoCallback(NO_CALLBACK_TIMEOUT)
+ agent1.sendNetworkScore(score(exiting = true))
+ // Now that agent1 is exiting, the callback is satisfied by agent2.
+ cb.expectAvailableCallbacks(agent2.network)
+ agent1.sendNetworkScore(score(exiting = false))
+ // Agent1 is no longer exiting, but agent2 is the current satisfier.
+ cb.assertNoCallback(NO_CALLBACK_TIMEOUT)
+ }
+
+ @Test
+ fun testVpnWins() {
+ val cb = makeTestNetworkCallback()
+ val agent1 = createTestNetworkAgent()
+ cb.expectAvailableThenValidatedCallbacks(agent1.network)
+ val agent2 = createTestNetworkAgent(intArrayOf(NetworkCapabilities.TRANSPORT_VPN))
+ // VPN wins out against agent1 even before it's validated (hence the "then validated",
+ // because it becomes the best network for this callback before it validates)
+ cb.expectAvailableThenValidatedCallbacks(agent2.network)
+ }
+
+ @Test
+ fun testEverUserSelectedAcceptUnvalidatedWins() {
+ val cb = makeTestNetworkCallback()
+ val agent1 = createTestNetworkAgent()
+ cb.expectAvailableThenValidatedCallbacks(agent1.network)
+ val agent2 = createTestNetworkAgent(everUserSelected = true, acceptUnvalidated = true)
+ // agent2 wins out against agent1 even before it's validated, because user-selected and
+ // accept unvalidated networks should win against even networks that are validated.
+ cb.expectAvailableThenValidatedCallbacks(agent2.network)
+ }
+
+ @Test
+ fun testPreferredTransportOrder() {
+ val cb = makeTestNetworkCallback()
+ val agentCell = createTestNetworkAgent(intArrayOf(NetworkCapabilities.TRANSPORT_CELLULAR))
+ cb.expectAvailableThenValidatedCallbacks(agentCell.network)
+ val agentWifi = createTestNetworkAgent(intArrayOf(NetworkCapabilities.TRANSPORT_WIFI))
+ // In the absence of other discriminating factors, agentWifi wins against agentCell because
+ // of its better transport, but only after it validates.
+ cb.expectAvailableDoubleValidatedCallbacks(agentWifi)
+ val agentEth = createTestNetworkAgent(intArrayOf(NetworkCapabilities.TRANSPORT_ETHERNET))
+ // Likewise, agentEth wins against agentWifi after validation because of its better
+ // transport.
+ cb.expectAvailableCallbacksValidated(agentEth)
+ }
+
+ @Test
+ fun testTransportPrimary() {
+ val cb = makeTestNetworkCallback()
+ val agent1 = createTestNetworkAgent()
+ cb.expectAvailableThenValidatedCallbacks(agent1)
+ val agent2 = createTestNetworkAgent()
+ // Because the existing network must win, the callback stays on agent1.
+ cb.assertNoCallback(NO_CALLBACK_TIMEOUT)
+ agent2.sendNetworkScore(score(primary = true))
+ // Now that agent2 is primary, the callback is satisfied by agent2.
+ cb.expectAvailableCallbacks(agent2.network)
+ agent1.sendNetworkScore(score(primary = true))
+ // Agent1 is primary too, but agent2 is the current satisfier
+ cb.assertNoCallback(NO_CALLBACK_TIMEOUT)
+ agent2.sendNetworkScore(score(primary = false))
+ // Now agent1 is primary and agent2 isn't
+ cb.expectAvailableCallbacks(agent1.network)
+ }
+
+ // TODO (b/187929636) : add a test making sure that validated networks win over unvalidated
+ // ones. Right now this is not possible because this CTS can't directly manipulate the
+ // validation state of a network.
+}
diff --git a/tests/cts/net/src/android/net/ipv6/cts/PingTest.java b/tests/cts/net/src/android/net/ipv6/cts/PingTest.java
index 146fd83..8665fc8 100644
--- a/tests/cts/net/src/android/net/ipv6/cts/PingTest.java
+++ b/tests/cts/net/src/android/net/ipv6/cts/PingTest.java
@@ -115,7 +115,7 @@
// Receive the response.
if (useRecvfrom) {
- InetSocketAddress from = new InetSocketAddress();
+ InetSocketAddress from = new InetSocketAddress(0);
bytesRead = Os.recvfrom(s, responseBuffer, 0, from);
// Check the source address and scope ID.
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index 7c733da..9c7d9c0 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -528,13 +528,13 @@
// MOCK_UID1: MOCK_PACKAGE1 only has network permission.
// SYSTEM_UID: SYSTEM_PACKAGE1 has system permission.
// SYSTEM_UID: SYSTEM_PACKAGE2 only has network permission.
- doReturn(SYSTEM).when(mPermissionMonitor).highestPermissionForUid(eq(SYSTEM), anyString());
doReturn(SYSTEM).when(mPermissionMonitor).highestPermissionForUid(any(),
eq(SYSTEM_PACKAGE1));
doReturn(NETWORK).when(mPermissionMonitor).highestPermissionForUid(any(),
eq(SYSTEM_PACKAGE2));
doReturn(NETWORK).when(mPermissionMonitor).highestPermissionForUid(any(),
eq(MOCK_PACKAGE1));
+ doReturn(SYSTEM).when(mPermissionMonitor).highestPermissionForUid(eq(SYSTEM), anyString());
// Add SYSTEM_PACKAGE2, expect only have network permission.
mPermissionMonitor.onUserAdded(MOCK_USER1);
@@ -549,6 +549,21 @@
netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2},
new int[]{SYSTEM_UID});
+ // Remove SYSTEM_PACKAGE2, expect keep system permission.
+ when(mPackageManager.getPackagesForUid(MOCK_USER1.getUid(SYSTEM_UID)))
+ .thenReturn(new String[]{SYSTEM_PACKAGE1});
+ when(mPackageManager.getPackagesForUid(MOCK_USER2.getUid(SYSTEM_UID)))
+ .thenReturn(new String[]{SYSTEM_PACKAGE1});
+ removePackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2},
+ SYSTEM_PACKAGE2, SYSTEM_UID);
+ netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2},
+ new int[]{SYSTEM_UID});
+
+ // Add SYSTEM_PACKAGE2, expect keep system permission.
+ addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, SYSTEM_PACKAGE2, SYSTEM_UID);
+ netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2},
+ new int[]{SYSTEM_UID});
+
addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_UID1);
netdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2},
new int[]{SYSTEM_UID});
@@ -556,6 +571,10 @@
new int[]{MOCK_UID1});
// Remove MOCK_UID1, expect no permission left for all user.
+ when(mPackageManager.getPackagesForUid(MOCK_USER1.getUid(MOCK_UID1)))
+ .thenReturn(new String[]{});
+ when(mPackageManager.getPackagesForUid(MOCK_USER2.getUid(MOCK_UID1)))
+ .thenReturn(new String[]{});
mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1);
removePackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_UID1);
netdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2},