Merge "Increase HandlerUtilsTest timeout to 500ms" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index fafd3bb..46308af 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -5,6 +5,9 @@
"options": [
{
"exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
}
]
},
@@ -260,7 +263,12 @@
"name": "netd_updatable_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
},
{
- "name": "ConnectivityCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+ "name": "ConnectivityCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ }
+ ]
},
{
"name": "traffic_controller_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 4478b1e..e69b872 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -95,6 +95,7 @@
],
static_libs: [
"NetworkStackApiCurrentShims",
+ "net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
lint: { strict_updatability_linting: true },
@@ -109,6 +110,7 @@
],
static_libs: [
"NetworkStackApiStableShims",
+ "net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
lint: { strict_updatability_linting: true },
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 9757daa..cd8eac8 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -96,6 +96,8 @@
},
binaries: [
"clatd",
+ "ethtool",
+ "netbpfload",
"ot-daemon",
],
canned_fs_config: "canned_fs_config",
diff --git a/Tethering/apex/canned_fs_config b/Tethering/apex/canned_fs_config
index 5a03347..1f5fcfa 100644
--- a/Tethering/apex/canned_fs_config
+++ b/Tethering/apex/canned_fs_config
@@ -1,2 +1,3 @@
/bin/for-system 0 1000 0750
/bin/for-system/clatd 1029 1029 06755
+/bin/netbpfload 0 0 0750
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index e030902..f275d49 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -33,6 +33,7 @@
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM;
import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
@@ -44,6 +45,7 @@
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.RouteInfo;
+import android.net.RoutingCoordinatorManager;
import android.net.TetheredClient;
import android.net.TetheringManager;
import android.net.TetheringRequestParcel;
@@ -55,7 +57,6 @@
import android.net.dhcp.IDhcpServer;
import android.net.ip.RouterAdvertisementDaemon.RaParams;
import android.os.Handler;
-import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
@@ -65,12 +66,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.MessageUtils;
import com.android.internal.util.State;
-import com.android.internal.util.StateMachine;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.ip.InterfaceController;
import com.android.net.module.util.ip.IpNeighborMonitor;
@@ -83,12 +85,15 @@
import com.android.networkstack.tethering.metrics.TetheringMetrics;
import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.networkstack.tethering.util.PrefixUtils;
+import com.android.networkstack.tethering.util.StateMachineShim;
+import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -102,7 +107,7 @@
*
* @hide
*/
-public class IpServer extends StateMachine {
+public class IpServer extends StateMachineShim {
public static final int STATE_UNAVAILABLE = 0;
public static final int STATE_AVAILABLE = 1;
public static final int STATE_TETHERED = 2;
@@ -234,6 +239,7 @@
public static final int CMD_NEW_PREFIX_REQUEST = BASE_IPSERVER + 12;
// request from PrivateAddressCoordinator to restart tethering.
public static final int CMD_NOTIFY_PREFIX_CONFLICT = BASE_IPSERVER + 13;
+ public static final int CMD_SERVICE_FAILED_TO_START = BASE_IPSERVER + 14;
private final State mInitialState;
private final State mLocalHotspotState;
@@ -245,6 +251,11 @@
private final INetd mNetd;
@NonNull
private final BpfCoordinator mBpfCoordinator;
+ // Contains null if the connectivity module is unsupported, as the routing coordinator is not
+ // available. Must use LateSdk because MessageUtils enumerates fields in this class, so it
+ // must be able to find all classes at runtime.
+ @NonNull
+ private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinator;
private final Callback mCallback;
private final InterfaceController mInterfaceCtrl;
private final PrivateAddressCoordinator mPrivateAddressCoordinator;
@@ -290,6 +301,8 @@
private int mLastIPv6UpstreamIfindex = 0;
private boolean mUpstreamSupportsBpf = false;
+ @NonNull
+ private Set<IpPrefix> mLastIPv6UpstreamPrefixes = Collections.emptySet();
private class MyNeighborEventConsumer implements IpNeighborMonitor.NeighborEventConsumer {
public void accept(NeighborEvent e) {
@@ -302,18 +315,22 @@
private LinkAddress mIpv4Address;
private final TetheringMetrics mTetheringMetrics;
+ private final Handler mHandler;
// TODO: Add a dependency object to pass the data members or variables from the tethering
// object. It helps to reduce the arguments of the constructor.
public IpServer(
- String ifaceName, Looper looper, int interfaceType, SharedLog log,
- INetd netd, @NonNull BpfCoordinator coordinator, Callback callback,
+ String ifaceName, Handler handler, int interfaceType, SharedLog log,
+ INetd netd, @NonNull BpfCoordinator bpfCoordinator,
+ @Nullable LateSdk<RoutingCoordinatorManager> routingCoordinator, Callback callback,
TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
TetheringMetrics tetheringMetrics, Dependencies deps) {
- super(ifaceName, looper);
+ super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
+ mHandler = handler;
mLog = log.forSubComponent(ifaceName);
mNetd = netd;
- mBpfCoordinator = coordinator;
+ mBpfCoordinator = bpfCoordinator;
+ mRoutingCoordinator = routingCoordinator;
mCallback = callback;
mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
mIfaceName = ifaceName;
@@ -342,13 +359,22 @@
mTetheredState = new TetheredState();
mUnavailableState = new UnavailableState();
mWaitingForRestartState = new WaitingForRestartState();
- addState(mInitialState);
- addState(mLocalHotspotState);
- addState(mTetheredState);
- addState(mWaitingForRestartState, mTetheredState);
- addState(mUnavailableState);
+ final ArrayList allStates = new ArrayList<StateInfo>();
+ allStates.add(new StateInfo(mInitialState, null));
+ allStates.add(new StateInfo(mLocalHotspotState, null));
+ allStates.add(new StateInfo(mTetheredState, null));
+ allStates.add(new StateInfo(mWaitingForRestartState, mTetheredState));
+ allStates.add(new StateInfo(mUnavailableState, null));
+ addAllStates(allStates);
+ }
- setInitialState(mInitialState);
+ private Handler getHandler() {
+ return mHandler;
+ }
+
+ /** Start IpServer state machine. */
+ public void start() {
+ start(mInitialState);
}
/** Interface name which IpServer served.*/
@@ -489,7 +515,12 @@
private void handleError() {
mLastError = TETHER_ERROR_DHCPSERVER_ERROR;
- transitionTo(mInitialState);
+ if (USE_SYNC_SM) {
+ sendMessage(CMD_SERVICE_FAILED_TO_START, TETHER_ERROR_DHCPSERVER_ERROR);
+ } else {
+ sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START,
+ TETHER_ERROR_DHCPSERVER_ERROR);
+ }
}
}
@@ -761,13 +792,8 @@
if (params.hasDefaultRoute) params.hopLimit = getHopLimit(upstreamIface, ttlAdjustment);
- for (LinkAddress linkAddr : v6only.getLinkAddresses()) {
- if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
-
- final IpPrefix prefix = new IpPrefix(
- linkAddr.getAddress(), linkAddr.getPrefixLength());
- params.prefixes.add(prefix);
-
+ params.prefixes = getTetherableIpv6Prefixes(v6only);
+ for (IpPrefix prefix : params.prefixes) {
final Inet6Address dnsServer = getLocalDnsIpFor(prefix);
if (dnsServer != null) {
params.dnses.add(dnsServer);
@@ -787,9 +813,12 @@
// Not support BPF on virtual upstream interface
final boolean upstreamSupportsBpf = upstreamIface != null && !isVcnInterface(upstreamIface);
- updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, upstreamIfIndex, upstreamSupportsBpf);
+ final Set<IpPrefix> upstreamPrefixes = params != null ? params.prefixes : Set.of();
+ updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamPrefixes,
+ upstreamIfIndex, upstreamPrefixes, upstreamSupportsBpf);
mLastIPv6LinkProperties = v6only;
mLastIPv6UpstreamIfindex = upstreamIfIndex;
+ mLastIPv6UpstreamPrefixes = upstreamPrefixes;
mUpstreamSupportsBpf = upstreamSupportsBpf;
if (mDadProxy != null) {
mDadProxy.setUpstreamIface(upstreamIfaceParams);
@@ -807,23 +836,33 @@
for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route);
}
- private void addRoutesToLocalNetwork(@NonNull final List<RouteInfo> toBeAdded) {
+ private void addInterfaceToNetwork(final int netId, @NonNull final String ifaceName) {
try {
- // It's safe to call networkAddInterface() even if
- // the interface is already in the local_network.
- mNetd.networkAddInterface(INetd.LOCAL_NET_ID, mIfaceName);
- try {
- // Add routes from local network. Note that adding routes that
- // already exist does not cause an error (EEXIST is silently ignored).
- NetdUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded);
- } catch (IllegalStateException e) {
- mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
- return;
+ if (null != mRoutingCoordinator.value) {
+ // TODO : remove this call in favor of using the LocalNetworkConfiguration
+ // correctly, which will let ConnectivityService do it automatically.
+ mRoutingCoordinator.value.addInterfaceToNetwork(netId, ifaceName);
+ } else {
+ mNetd.networkAddInterface(netId, ifaceName);
}
} catch (ServiceSpecificException | RemoteException e) {
mLog.e("Failed to add " + mIfaceName + " to local table: ", e);
return;
}
+ }
+
+ private void addRoutesToLocalNetwork(@NonNull final List<RouteInfo> toBeAdded) {
+ // It's safe to call addInterfaceToNetwork() even if
+ // the interface is already in the local_network.
+ addInterfaceToNetwork(INetd.LOCAL_NET_ID, mIfaceName);
+ try {
+ // Add routes from local network. Note that adding routes that
+ // already exist does not cause an error (EEXIST is silently ignored).
+ NetdUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded);
+ } catch (IllegalStateException e) {
+ mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
+ return;
+ }
for (RouteInfo route : toBeAdded) mLinkProperties.addRoute(route);
}
@@ -897,14 +936,17 @@
return supportsBpf ? ifindex : NO_UPSTREAM;
}
- // Handles updates to IPv6 forwarding rules if the upstream changes.
- private void updateIpv6ForwardingRules(int prevUpstreamIfindex, int upstreamIfindex,
- boolean upstreamSupportsBpf) {
+ // Handles updates to IPv6 forwarding rules if the upstream or its prefixes change.
+ private void updateIpv6ForwardingRules(int prevUpstreamIfindex,
+ @NonNull Set<IpPrefix> prevUpstreamPrefixes, int upstreamIfindex,
+ @NonNull Set<IpPrefix> upstreamPrefixes, boolean upstreamSupportsBpf) {
// If the upstream interface has changed, remove all rules and re-add them with the new
// upstream interface. If upstream is a virtual network, treated as no upstream.
- if (prevUpstreamIfindex != upstreamIfindex) {
+ if (prevUpstreamIfindex != upstreamIfindex
+ || !prevUpstreamPrefixes.equals(upstreamPrefixes)) {
mBpfCoordinator.updateAllIpv6Rules(this, this.mInterfaceParams,
- getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf));
+ getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf),
+ upstreamPrefixes);
}
}
@@ -1089,7 +1131,19 @@
startServingInterface();
if (mLastError != TETHER_ERROR_NO_ERROR) {
- transitionTo(mInitialState);
+ // This will transition to InitialState right away, regardless of whether any
+ // message is already waiting in the StateMachine queue (including maybe some
+ // message to go to InitialState). InitialState will then process any pending
+ // message (and generally ignores them). It is difficult to know for sure whether
+ // this is correct in all cases, but this is equivalent to what IpServer was doing
+ // in previous versions of the mainline module.
+ // TODO : remove sendMessageAtFrontOfQueueToAsyncSM after migrating to the Sync
+ // StateMachine.
+ if (USE_SYNC_SM) {
+ sendSelfMessageToSyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
+ } else {
+ sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
+ }
}
if (DBG) Log.d(TAG, getStateString(mDesiredInterfaceState) + " serve " + mIfaceName);
@@ -1179,6 +1233,9 @@
mCallback.requestEnableTethering(mInterfaceType, false /* enabled */);
transitionTo(mWaitingForRestartState);
break;
+ case CMD_SERVICE_FAILED_TO_START:
+ mLog.e("start serving fail, error: " + message.arg1);
+ transitionTo(mInitialState);
default:
return false;
}
@@ -1309,7 +1366,7 @@
for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname);
mUpstreamIfaceSet = null;
mBpfCoordinator.updateAllIpv6Rules(
- IpServer.this, IpServer.this.mInterfaceParams, NO_UPSTREAM);
+ IpServer.this, IpServer.this.mInterfaceParams, NO_UPSTREAM, Set.of());
}
private void cleanupUpstreamInterface(String upstreamIface) {
@@ -1496,4 +1553,21 @@
}
return random;
}
+
+ /** Get IPv6 prefixes from LinkProperties */
+ @NonNull
+ @VisibleForTesting
+ static HashSet<IpPrefix> getTetherableIpv6Prefixes(@NonNull Collection<LinkAddress> addrs) {
+ final HashSet<IpPrefix> prefixes = new HashSet<>();
+ for (LinkAddress linkAddr : addrs) {
+ if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
+ prefixes.add(new IpPrefix(linkAddr.getAddress(), RFC7421_PREFIX_LENGTH));
+ }
+ return prefixes;
+ }
+
+ @NonNull
+ private HashSet<IpPrefix> getTetherableIpv6Prefixes(@NonNull LinkProperties lp) {
+ return getTetherableIpv6Prefixes(lp.getLinkAddresses());
+ }
}
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 46c815f..2b14a42 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -28,6 +28,7 @@
import static android.system.OsConstants.ETH_P_IPV6;
import static com.android.net.module.util.NetworkStackConstants.IPV4_MIN_MTU;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
import static com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
@@ -90,7 +91,6 @@
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -123,7 +123,6 @@
private static final int DUMP_TIMEOUT_MS = 10_000;
private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString(
"00:00:00:00:00:00");
- private static final IpPrefix IPV6_ZERO_PREFIX64 = new IpPrefix("::/64");
private static final String TETHER_DOWNSTREAM4_MAP_PATH = makeMapPath(DOWNSTREAM, 4);
private static final String TETHER_UPSTREAM4_MAP_PATH = makeMapPath(UPSTREAM, 4);
private static final String TETHER_DOWNSTREAM6_FS_PATH = makeMapPath(DOWNSTREAM, 6);
@@ -768,7 +767,8 @@
* Note that this can be only called on handler thread.
*/
public void updateAllIpv6Rules(@NonNull final IpServer ipServer,
- final InterfaceParams interfaceParams, int newUpstreamIfindex) {
+ final InterfaceParams interfaceParams, int newUpstreamIfindex,
+ @NonNull final Set<IpPrefix> newUpstreamPrefixes) {
if (!isUsingBpf()) return;
// Remove IPv6 downstream rules. Remove the old ones before adding the new rules, otherwise
@@ -791,9 +791,11 @@
// Add new upstream rules.
if (newUpstreamIfindex != 0 && interfaceParams != null && interfaceParams.macAddr != null) {
- addIpv6UpstreamRule(ipServer, new Ipv6UpstreamRule(
- newUpstreamIfindex, interfaceParams.index, IPV6_ZERO_PREFIX64,
- interfaceParams.macAddr, NULL_MAC_ADDRESS, NULL_MAC_ADDRESS));
+ for (final IpPrefix ipPrefix : newUpstreamPrefixes) {
+ addIpv6UpstreamRule(ipServer, new Ipv6UpstreamRule(
+ newUpstreamIfindex, interfaceParams.index, ipPrefix,
+ interfaceParams.macAddr, NULL_MAC_ADDRESS, NULL_MAC_ADDRESS));
+ }
}
// Add updated downstream rules.
@@ -1256,10 +1258,24 @@
pw.decreaseIndent();
}
+ private IpPrefix longToPrefix(long ip64) {
+ final ByteBuffer prefixBuffer = ByteBuffer.allocate(IPV6_ADDR_LEN);
+ prefixBuffer.putLong(ip64);
+ IpPrefix sourcePrefix;
+ try {
+ sourcePrefix = new IpPrefix(InetAddress.getByAddress(prefixBuffer.array()), 64);
+ } catch (UnknownHostException e) {
+ // Cannot happen. InetAddress.getByAddress can only throw an exception if the byte array
+ // is the wrong length, but we allocate it with fixed length IPV6_ADDR_LEN.
+ throw new IllegalArgumentException("Invalid IPv6 address");
+ }
+ return sourcePrefix;
+ }
+
private String ipv6UpstreamRuleToString(TetherUpstream6Key key, Tether6Value value) {
- return String.format("%d(%s) [%s] -> %d(%s) %04x [%s] [%s]",
- key.iif, getIfName(key.iif), key.dstMac, value.oif, getIfName(value.oif),
- value.ethProto, value.ethSrcMac, value.ethDstMac);
+ return String.format("%d(%s) [%s] [%s] -> %d(%s) %04x [%s] [%s]",
+ key.iif, getIfName(key.iif), key.dstMac, longToPrefix(key.src64), value.oif,
+ getIfName(value.oif), value.ethProto, value.ethSrcMac, value.ethDstMac);
}
private void dumpIpv6UpstreamRules(IndentingPrintWriter pw) {
@@ -1309,8 +1325,8 @@
// TODO: use dump utils with headerline and lambda which prints key and value to reduce
// duplicate bpf map dump code.
private void dumpBpfForwardingRulesIpv6(IndentingPrintWriter pw) {
- pw.println("IPv6 Upstream: iif(iface) [inDstMac] -> oif(iface) etherType [outSrcMac] "
- + "[outDstMac]");
+ pw.println("IPv6 Upstream: iif(iface) [inDstMac] [sourcePrefix] -> oif(iface) etherType "
+ + "[outSrcMac] [outDstMac]");
pw.increaseIndent();
dumpIpv6UpstreamRules(pw);
pw.decreaseIndent();
@@ -1554,8 +1570,7 @@
*/
@NonNull
public TetherUpstream6Key makeTetherUpstream6Key() {
- byte[] prefixBytes = Arrays.copyOf(sourcePrefix.getRawAddress(), 8);
- long prefix64 = ByteBuffer.wrap(prefixBytes).order(ByteOrder.BIG_ENDIAN).getLong();
+ long prefix64 = ByteBuffer.wrap(sourcePrefix.getRawAddress()).getLong();
return new TetherUpstream6Key(downstreamIfindex, inDstMac, prefix64);
}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index b371178..996ee11 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -90,6 +90,7 @@
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkInfo;
+import android.net.RoutingCoordinatorManager;
import android.net.TetherStatesParcel;
import android.net.TetheredClient;
import android.net.TetheringCallbackStartedParcel;
@@ -136,6 +137,7 @@
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.common.BluetoothPanShim;
import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
@@ -250,6 +252,10 @@
private final Handler mHandler;
private final INetd mNetd;
private final NetdCallback mNetdCallback;
+ // Contains null if the connectivity module is unsupported, as the routing coordinator is not
+ // available. Must use LateSdk because MessageUtils enumerates fields in this class, so it
+ // must be able to find all classes at runtime.
+ @NonNull private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinator;
private final UserRestrictionActionListener mTetheringRestriction;
private final ActiveDataSubIdListener mActiveDataSubIdListener;
private final ConnectedClientsTracker mConnectedClientsTracker;
@@ -296,6 +302,7 @@
mDeps = deps;
mContext = mDeps.getContext();
mNetd = mDeps.getINetd(mContext);
+ mRoutingCoordinator = mDeps.getRoutingCoordinator(mContext);
mLooper = mDeps.getTetheringLooper();
mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
mTetheringMetrics = mDeps.getTetheringMetrics();
@@ -1680,6 +1687,8 @@
static final int EVENT_IFACE_UPDATE_LINKPROPERTIES = BASE_MAIN_SM + 7;
// Events from EntitlementManager to choose upstream again.
static final int EVENT_UPSTREAM_PERMISSION_CHANGED = BASE_MAIN_SM + 8;
+ // Internal request from IpServer to enable or disable downstream.
+ static final int EVENT_REQUEST_CHANGE_DOWNSTREAM = BASE_MAIN_SM + 9;
private final State mInitialState;
private final State mTetherModeAliveState;
@@ -2179,6 +2188,12 @@
}
break;
}
+ case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
+ final int tetheringType = message.arg1;
+ final Boolean enabled = (Boolean) message.obj;
+ enableTetheringInternal(tetheringType, enabled, null);
+ break;
+ }
default:
retValue = false;
break;
@@ -2736,7 +2751,8 @@
@Override
public void requestEnableTethering(int tetheringType, boolean enabled) {
- enableTetheringInternal(tetheringType, enabled, null);
+ mTetherMainSM.sendMessage(TetherMainSM.EVENT_REQUEST_CHANGE_DOWNSTREAM,
+ tetheringType, 0, enabled ? Boolean.TRUE : Boolean.FALSE);
}
};
}
@@ -2834,9 +2850,10 @@
mLog.i("adding IpServer for: " + iface);
final TetherState tetherState = new TetherState(
- new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
- makeControlCallback(), mConfig, mPrivateAddressCoordinator,
- mTetheringMetrics, mDeps.getIpServerDependencies()), isNcm);
+ new IpServer(iface, mHandler, interfaceType, mLog, mNetd, mBpfCoordinator,
+ mRoutingCoordinator, makeControlCallback(), mConfig,
+ mPrivateAddressCoordinator, mTetheringMetrics,
+ mDeps.getIpServerDependencies()), isNcm);
mTetherStates.put(iface, tetherState);
tetherState.ipServer.start();
}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 747cc20..502fee8 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -136,6 +136,9 @@
*/
public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000;
+ /** A flag for using synchronous or asynchronous state machine. */
+ public static final boolean USE_SYNC_SM = false;
+
public final String[] tetherableUsbRegexs;
public final String[] tetherableWifiRegexs;
public final String[] tetherableWigigRegexs;
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 741a5c5..c6468a0 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -16,11 +16,14 @@
package com.android.networkstack.tethering;
+import android.annotation.Nullable;
import android.app.usage.NetworkStatsManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothPan;
import android.content.Context;
import android.net.INetd;
+import android.net.RoutingCoordinatorManager;
+import android.net.connectivity.TiramisuConnectivityInternalApiUtil;
import android.net.ip.IpServer;
import android.os.Build;
import android.os.Handler;
@@ -33,6 +36,8 @@
import androidx.annotation.RequiresApi;
import com.android.internal.util.StateMachine;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.BluetoothPanShimImpl;
import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -122,6 +127,16 @@
}
/**
+ * Get the routing coordinator, or null if below S.
+ */
+ @Nullable
+ public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(Context context) {
+ if (!SdkLevel.isAtLeastS()) return new LateSdk<>(null);
+ return new LateSdk<>(
+ TiramisuConnectivityInternalApiUtil.getRoutingCoordinatorManager(context));
+ }
+
+ /**
* Get a reference to the TetheringNotificationUpdater to be used by tethering.
*/
public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx,
@@ -135,7 +150,7 @@
public abstract Looper getTetheringLooper();
/**
- * Get Context of TetheringSerice.
+ * Get Context of TetheringService.
*/
public abstract Context getContext();
diff --git a/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
new file mode 100644
index 0000000..078a35f
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2023 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.util;
+
+import android.annotation.Nullable;
+import android.os.Looper;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
+
+import java.util.List;
+
+/** A wrapper to decide whether use synchronous state machine for tethering. */
+public class StateMachineShim {
+ // Exactly one of mAsyncSM or mSyncSM is non-null.
+ private final AsyncStateMachine mAsyncSM;
+ private final SyncStateMachine mSyncSM;
+
+ /**
+ * The Looper parameter is only needed for AsyncSM, so if looper is null, the shim will be
+ * created for SyncSM.
+ */
+ public StateMachineShim(final String name, @Nullable final Looper looper) {
+ this(name, looper, new Dependencies());
+ }
+
+ @VisibleForTesting
+ public StateMachineShim(final String name, @Nullable final Looper looper,
+ final Dependencies deps) {
+ if (looper == null) {
+ mAsyncSM = null;
+ mSyncSM = deps.makeSyncStateMachine(name, Thread.currentThread());
+ } else {
+ mAsyncSM = deps.makeAsyncStateMachine(name, looper);
+ mSyncSM = null;
+ }
+ }
+
+ /** A dependencies class which used for testing injection. */
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Create SyncSM instance, for injection. */
+ public SyncStateMachine makeSyncStateMachine(final String name, final Thread thread) {
+ return new SyncStateMachine(name, thread);
+ }
+
+ /** Create AsyncSM instance, for injection. */
+ public AsyncStateMachine makeAsyncStateMachine(final String name, final Looper looper) {
+ return new AsyncStateMachine(name, looper);
+ }
+ }
+
+ /** Start the state machine */
+ public void start(final State initialState) {
+ if (mSyncSM != null) {
+ mSyncSM.start(initialState);
+ } else {
+ mAsyncSM.setInitialState(initialState);
+ mAsyncSM.start();
+ }
+ }
+
+ /** Add states to state machine. */
+ public void addAllStates(final List<StateInfo> stateInfos) {
+ if (mSyncSM != null) {
+ mSyncSM.addAllStates(stateInfos);
+ } else {
+ for (final StateInfo info : stateInfos) {
+ mAsyncSM.addState(info.state, info.parent);
+ }
+ }
+ }
+
+ /**
+ * Transition to given state.
+ *
+ * SyncSM doesn't allow this be called during state transition (#enter() or #exit() methods),
+ * or multiple times while processing a single message.
+ */
+ public void transitionTo(final State state) {
+ if (mSyncSM != null) {
+ mSyncSM.transitionTo(state);
+ } else {
+ mAsyncSM.transitionTo(state);
+ }
+ }
+
+ /** Send message to state machine. */
+ public void sendMessage(int what) {
+ sendMessage(what, 0, 0, null);
+ }
+
+ /** Send message to state machine. */
+ public void sendMessage(int what, Object obj) {
+ sendMessage(what, 0, 0, obj);
+ }
+
+ /** Send message to state machine. */
+ public void sendMessage(int what, int arg1) {
+ sendMessage(what, arg1, 0, null);
+ }
+
+ /**
+ * Send message to state machine.
+ *
+ * If using asynchronous state machine, putting the message into looper's message queue.
+ * Tethering runs on single looper thread that ipServers and mainSM all share with same message
+ * queue. The enqueued message will be processed by asynchronous state machine when all the
+ * messages before such enqueued message are processed.
+ * If using synchronous state machine, the message is processed right away without putting into
+ * looper's message queue.
+ */
+ public void sendMessage(int what, int arg1, int arg2, Object obj) {
+ if (mSyncSM != null) {
+ mSyncSM.processMessage(what, arg1, arg2, obj);
+ } else {
+ mAsyncSM.sendMessage(what, arg1, arg2, obj);
+ }
+ }
+
+ /**
+ * Send message after delayMillis millisecond.
+ *
+ * This can only be used with async state machine, so this will throw if using sync state
+ * machine.
+ */
+ public void sendMessageDelayedToAsyncSM(final int what, final long delayMillis) {
+ if (mSyncSM != null) {
+ throw new IllegalStateException("sendMessageDelayed can only be used with async SM");
+ }
+
+ mAsyncSM.sendMessageDelayed(what, delayMillis);
+ }
+
+ /**
+ * Enqueue a message to the front of the queue.
+ * Protected, may only be called by instances of async state machine.
+ *
+ * Message is ignored if state machine has quit.
+ */
+ protected void sendMessageAtFrontOfQueueToAsyncSM(int what, int arg1) {
+ if (mSyncSM != null) {
+ throw new IllegalStateException("sendMessageAtFrontOfQueue can only be used with"
+ + " async SM");
+ }
+
+ mAsyncSM.sendMessageAtFrontOfQueueToAsyncSM(what, arg1);
+ }
+
+ /**
+ * Send self message.
+ * This can only be used with sync state machine, so this will throw if using async state
+ * machine.
+ */
+ public void sendSelfMessageToSyncSM(final int what, final Object obj) {
+ if (mSyncSM == null) {
+ throw new IllegalStateException("sendSelfMessage can only be used with sync SM");
+ }
+
+ mSyncSM.sendSelfMessage(what, 0, 0, obj);
+ }
+
+ /**
+ * An alias StateMahchine class with public construtor.
+ *
+ * Since StateMachine.java only provides protected construtor, adding a child class so that this
+ * shim could create StateMachine instance.
+ */
+ @VisibleForTesting
+ public static class AsyncStateMachine extends StateMachine {
+ public AsyncStateMachine(final String name, final Looper looper) {
+ super(name, looper);
+ }
+
+ /** Enqueue a message to the front of the queue for this state machine. */
+ public void sendMessageAtFrontOfQueueToAsyncSM(int what, int arg1) {
+ sendMessageAtFrontOfQueue(what, arg1);
+ }
+ }
+}
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
index 328e3fb..dac5b63 100644
--- a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
+++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
@@ -16,8 +16,6 @@
package android.net.ip;
-import static android.net.RouteInfo.RTN_UNICAST;
-
import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU;
@@ -42,12 +40,13 @@
import android.net.INetd;
import android.net.IpPrefix;
import android.net.MacAddress;
-import android.net.RouteInfo;
import android.net.ip.RouterAdvertisementDaemon.RaParams;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -55,7 +54,6 @@
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.Ipv6Utils;
-import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.Struct;
import com.android.net.module.util.structs.EthernetHeader;
import com.android.net.module.util.structs.Icmpv6Header;
@@ -80,7 +78,6 @@
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.HashSet;
-import java.util.List;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -332,10 +329,12 @@
// Add a default route "fe80::/64 -> ::" to local network, otherwise, device will fail to
// send the unicast RA out due to the ENETUNREACH error(No route to the peer's link-local
// address is present).
- final String iface = mTetheredParams.name;
- final RouteInfo linkLocalRoute =
- new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST);
- NetdUtils.addRoutesToLocalNetwork(sNetd, iface, List.of(linkLocalRoute));
+ try {
+ sNetd.networkAddRoute(INetd.LOCAL_NET_ID, mTetheredParams.name,
+ "fe80::/64", INetd.NEXTHOP_NONE);
+ } catch (RemoteException | ServiceSpecificException e) {
+ throw new IllegalStateException(e);
+ }
final ByteBuffer rs = createRsPacket("fe80::1122:3344:5566:7788");
mTetheredPacketReader.sendResponse(rs);
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
index 81d4fbe..60f2d17 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
@@ -44,6 +44,7 @@
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.StructNlMsgHdr;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -84,6 +85,14 @@
mOffloadHw = new OffloadHardwareInterface(mHandler, mLog, mDeps);
}
+ @After
+ public void tearDown() throws Exception {
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+ }
+ }
+
void findConnectionOrThrow(FileDescriptor fd, InetSocketAddress local, InetSocketAddress remote)
throws Exception {
Log.d(TAG, "Looking for socket " + local + " -> " + remote);
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index d497a4d..08fca4a 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -34,6 +34,7 @@
import static android.net.ip.IpServer.STATE_LOCAL_ONLY;
import static android.net.ip.IpServer.STATE_TETHERED;
import static android.net.ip.IpServer.STATE_UNAVAILABLE;
+import static android.net.ip.IpServer.getTetherableIpv6Prefixes;
import static android.system.OsConstants.ETH_P_IPV6;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
@@ -62,6 +63,7 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
@@ -80,6 +82,7 @@
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.RouteInfo;
+import android.net.RoutingCoordinatorManager;
import android.net.TetherOffloadRuleParcel;
import android.net.TetherStatsParcel;
import android.net.dhcp.DhcpServerCallbacks;
@@ -93,15 +96,19 @@
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.bpf.Tether4Key;
@@ -141,12 +148,15 @@
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.mockito.verification.VerificationMode;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
+import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
+import java.util.Set;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -184,6 +194,18 @@
private final LinkAddress mTestAddress = new LinkAddress("192.168.42.5/24");
private final IpPrefix mBluetoothPrefix = new IpPrefix("192.168.44.0/24");
+ private static final Set<LinkAddress> NO_ADDRESSES = Set.of();
+ private static final Set<IpPrefix> NO_PREFIXES = Set.of();
+ private static final Set<LinkAddress> UPSTREAM_ADDRESSES =
+ Set.of(new LinkAddress("2001:db8:0:1234::168/64"));
+ private static final Set<IpPrefix> UPSTREAM_PREFIXES =
+ Set.of(new IpPrefix("2001:db8:0:1234::/64"));
+ private static final Set<LinkAddress> UPSTREAM_ADDRESSES2 = Set.of(
+ new LinkAddress("2001:db8:0:1234::168/64"),
+ new LinkAddress("2001:db8:0:abcd::168/64"));
+ private static final Set<IpPrefix> UPSTREAM_PREFIXES2 = Set.of(
+ new IpPrefix("2001:db8:0:1234::/64"), new IpPrefix("2001:db8:0:abcd::/64"));
+
@Mock private INetd mNetd;
@Mock private IpServer.Callback mCallback;
@Mock private SharedLog mSharedLog;
@@ -193,6 +215,8 @@
@Mock private IpNeighborMonitor mIpNeighborMonitor;
@Mock private IpServer.Dependencies mDependencies;
@Mock private PrivateAddressCoordinator mAddressCoordinator;
+ private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinatorManager =
+ new LateSdk<>(SdkLevel.isAtLeastS() ? mock(RoutingCoordinatorManager.class) : null);
@Mock private NetworkStatsManager mStatsManager;
@Mock private TetheringConfiguration mTetherConfig;
@Mock private ConntrackMonitor mConntrackMonitor;
@@ -208,7 +232,8 @@
@Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
- private final TestLooper mLooper = new TestLooper();
+ private TestLooper mLooper;
+ private Handler mHandler;
private final ArgumentCaptor<LinkProperties> mLinkPropertiesCaptor =
ArgumentCaptor.forClass(LinkProperties.class);
private IpServer mIpServer;
@@ -245,11 +270,7 @@
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(usingBpfOffload);
when(mTetherConfig.useLegacyDhcpServer()).thenReturn(usingLegacyDhcp);
when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
- // Recreate mBpfCoordinator again here because mTetherConfig has changed
- mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
- mIpServer = new IpServer(
- IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator,
- mCallback, mTetherConfig, mAddressCoordinator, mTetheringMetrics, mDependencies);
+ mIpServer = createIpServer(interfaceType);
mIpServer.start();
mNeighborEventConsumer = neighborCaptor.getValue();
@@ -263,24 +284,27 @@
private void initTetheredStateMachine(int interfaceType, String upstreamIface)
throws Exception {
- initTetheredStateMachine(interfaceType, upstreamIface, false,
+ initTetheredStateMachine(interfaceType, upstreamIface, NO_ADDRESSES, false,
DEFAULT_USING_BPF_OFFLOAD);
}
private void initTetheredStateMachine(int interfaceType, String upstreamIface,
- boolean usingLegacyDhcp, boolean usingBpfOffload) throws Exception {
+ Set<LinkAddress> upstreamAddresses, boolean usingLegacyDhcp, boolean usingBpfOffload)
+ throws Exception {
initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload);
dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
if (upstreamIface != null) {
LinkProperties lp = new LinkProperties();
lp.setInterfaceName(upstreamIface);
+ lp.setLinkAddresses(upstreamAddresses);
dispatchTetherConnectionChanged(upstreamIface, lp, 0);
- if (usingBpfOffload) {
+ if (usingBpfOffload && !lp.getLinkAddresses().isEmpty()) {
+ Set<IpPrefix> upstreamPrefixes = getTetherableIpv6Prefixes(lp.getLinkAddresses());
InterfaceParams interfaceParams = mDependencies.getInterfaceParams(upstreamIface);
assertNotNull("missing upstream interface: " + upstreamIface, interfaceParams);
verify(mBpfCoordinator).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, interfaceParams.index);
- verifyStartUpstreamIpv6Forwarding(null, interfaceParams.index);
+ mIpServer, TEST_IFACE_PARAMS, interfaceParams.index, upstreamPrefixes);
+ verifyStartUpstreamIpv6Forwarding(null, interfaceParams.index, upstreamPrefixes);
} else {
verifyNoUpstreamIpv6ForwardingChange(null);
}
@@ -314,10 +338,17 @@
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(DEFAULT_USING_BPF_OFFLOAD);
when(mTetherConfig.useLegacyDhcpServer()).thenReturn(false /* default value */);
+ setUpDhcpServer();
+ }
+
+ // In order to interact with syncSM from the test, IpServer must be created in test thread.
+ private IpServer createIpServer(final int interfaceType) {
+ mLooper = new TestLooper();
+ mHandler = new Handler(mLooper.getLooper());
mBpfDeps = new BpfCoordinator.Dependencies() {
@NonNull
public Handler getHandler() {
- return new Handler(mLooper.getLooper());
+ return mHandler;
}
@NonNull
@@ -386,18 +417,19 @@
return mBpfErrorMap;
}
};
- mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
- setUpDhcpServer();
+ mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
+ return new IpServer(IFACE_NAME, mHandler, interfaceType, mSharedLog, mNetd, mBpfCoordinator,
+ mRoutingCoordinatorManager, mCallback, mTetherConfig, mAddressCoordinator,
+ mTetheringMetrics, mDependencies);
+
}
@Test
- public void startsOutAvailable() {
+ public void startsOutAvailable() throws Exception {
when(mDependencies.getIpNeighborMonitor(any(), any(), any()))
.thenReturn(mIpNeighborMonitor);
- mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog,
- mNetd, mBpfCoordinator, mCallback, mTetherConfig, mAddressCoordinator,
- mTetheringMetrics, mDependencies);
+ mIpServer = createIpServer(TETHERING_BLUETOOTH);
mIpServer.start();
mLooper.dispatchAll();
verify(mCallback).updateInterfaceState(
@@ -639,10 +671,7 @@
inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mBpfCoordinator).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM);
- if (!mBpfDeps.isAtLeastS()) {
- inOrder.verify(mNetd).tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX);
- }
+ mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
// When tethering stops, upstream interface is set to zero and thus clearing all upstream
// rules. Downstream rules are needed to be cleared explicitly by calling
// BpfCoordinator#clearAllIpv6Rules in TetheredState#exit.
@@ -832,8 +861,8 @@
@Test
public void doesNotStartDhcpServerIfDisabled() throws Exception {
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, true /* usingLegacyDhcp */,
- DEFAULT_USING_BPF_OFFLOAD);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, NO_ADDRESSES,
+ true /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
verify(mDependencies, never()).makeDhcpServer(any(), any(), any());
@@ -938,11 +967,19 @@
TEST_IFACE_PARAMS.macAddr, ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
}
+ private static long prefixToLong(IpPrefix prefix) {
+ return ByteBuffer.wrap(prefix.getRawAddress()).getLong();
+ }
+
private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+ return verifyWithOrder(inOrder, t, times(1));
+ }
+
+ private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t, VerificationMode mode) {
if (inOrder != null) {
- return inOrder.verify(t);
+ return inOrder.verify(t, mode);
} else {
- return verify(t);
+ return verify(t, mode);
}
}
@@ -1002,23 +1039,49 @@
}
}
- private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int upstreamIfindex)
- throws Exception {
+ private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int upstreamIfindex,
+ @NonNull Set<IpPrefix> upstreamPrefixes) throws Exception {
if (!mBpfDeps.isAtLeastS()) return;
- final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
- TEST_IFACE_PARAMS.macAddr, 0);
- final Tether6Value value = new Tether6Value(upstreamIfindex,
- MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
- ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
- verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value);
+ ArrayMap<TetherUpstream6Key, Tether6Value> expected = new ArrayMap<>();
+ for (IpPrefix upstreamPrefix : upstreamPrefixes) {
+ long prefix64 = prefixToLong(upstreamPrefix);
+ final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
+ TEST_IFACE_PARAMS.macAddr, prefix64);
+ final Tether6Value value = new Tether6Value(upstreamIfindex,
+ MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
+ ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
+ expected.put(key, value);
+ }
+ ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+ ArgumentCaptor.forClass(TetherUpstream6Key.class);
+ ArgumentCaptor<Tether6Value> valueCaptor =
+ ArgumentCaptor.forClass(Tether6Value.class);
+ verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).insertEntry(
+ keyCaptor.capture(), valueCaptor.capture());
+ List<TetherUpstream6Key> keys = keyCaptor.getAllValues();
+ List<Tether6Value> values = valueCaptor.getAllValues();
+ ArrayMap<TetherUpstream6Key, Tether6Value> captured = new ArrayMap<>();
+ for (int i = 0; i < keys.size(); i++) {
+ captured.put(keys.get(i), values.get(i));
+ }
+ assertEquals(expected, captured);
}
- private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder)
- throws Exception {
+ private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder,
+ @NonNull Set<IpPrefix> upstreamPrefixes) throws Exception {
if (!mBpfDeps.isAtLeastS()) return;
- final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
- TEST_IFACE_PARAMS.macAddr, 0);
- verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key);
+ Set<TetherUpstream6Key> expected = new ArraySet<>();
+ for (IpPrefix upstreamPrefix : upstreamPrefixes) {
+ long prefix64 = prefixToLong(upstreamPrefix);
+ final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
+ TEST_IFACE_PARAMS.macAddr, prefix64);
+ expected.add(key);
+ }
+ ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+ ArgumentCaptor.forClass(TetherUpstream6Key.class);
+ verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).deleteEntry(
+ keyCaptor.capture());
+ assertEquals(expected, new ArraySet(keyCaptor.getAllValues()));
}
private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception {
@@ -1059,8 +1122,8 @@
@Test
public void addRemoveipv6ForwardingRules() throws Exception {
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- DEFAULT_USING_BPF_OFFLOAD);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+ false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
final int myIfindex = TEST_IFACE_PARAMS.index;
final int notMyIfindex = myIfindex - 1;
@@ -1121,7 +1184,7 @@
UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macNull);
resetNetdBpfMapAndCoordinator();
- // Upstream changes result in updating the rules.
+ // Upstream interface changes result in updating the rules.
recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
resetNetdBpfMapAndCoordinator();
@@ -1129,14 +1192,36 @@
InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
LinkProperties lp = new LinkProperties();
lp.setInterfaceName(UPSTREAM_IFACE2);
+ lp.setLinkAddresses(UPSTREAM_ADDRESSES);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1);
- verify(mBpfCoordinator).updateAllIpv6Rules(mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2);
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
verifyTetherOffloadRuleRemove(inOrder,
UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
verifyTetherOffloadRuleRemove(inOrder,
UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
- verifyStopUpstreamIpv6Forwarding(inOrder);
- verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2);
+ verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES);
+ verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
+ verifyTetherOffloadRuleAdd(inOrder,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
+ verifyTetherOffloadRuleAdd(inOrder,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
+ verifyNoUpstreamIpv6ForwardingChange(inOrder);
+ resetNetdBpfMapAndCoordinator();
+
+ // Upstream link addresses change result in updating the rules.
+ LinkProperties lp2 = new LinkProperties();
+ lp2.setInterfaceName(UPSTREAM_IFACE2);
+ lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, -1);
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+ verifyTetherOffloadRuleRemove(inOrder,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
+ verifyTetherOffloadRuleRemove(inOrder,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
+ verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES);
+ verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
verifyTetherOffloadRuleAdd(inOrder,
UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
verifyTetherOffloadRuleAdd(inOrder,
@@ -1150,8 +1235,8 @@
// - processMessage CMD_IPV6_TETHER_UPDATE for the IPv6 upstream is lost.
// See dispatchTetherConnectionChanged.
verify(mBpfCoordinator, times(2)).updateAllIpv6Rules(
- mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM);
- verifyStopUpstreamIpv6Forwarding(inOrder);
+ mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES2);
verifyTetherOffloadRuleRemove(null,
UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
verifyTetherOffloadRuleRemove(null,
@@ -1181,7 +1266,7 @@
// with an upstream of NO_UPSTREAM are reapplied.
lp.setInterfaceName(UPSTREAM_IFACE);
dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
- verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
verify(mBpfCoordinator).addIpv6DownstreamRule(
mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macA));
verifyTetherOffloadRuleAdd(null,
@@ -1195,16 +1280,17 @@
// If upstream IPv6 connectivity is lost, rules are removed.
resetNetdBpfMapAndCoordinator();
dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
- verify(mBpfCoordinator).updateAllIpv6Rules(mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM);
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
verifyTetherOffloadRuleRemove(null,
UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
- verifyStopUpstreamIpv6Forwarding(null);
+ verifyStopUpstreamIpv6Forwarding(null, UPSTREAM_PREFIXES);
// When upstream IPv6 connectivity comes back, upstream rules are added and downstream rules
// are reapplied.
lp.setInterfaceName(UPSTREAM_IFACE);
dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
- verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
verify(mBpfCoordinator).addIpv6DownstreamRule(
mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macA));
verifyTetherOffloadRuleAdd(null,
@@ -1219,7 +1305,7 @@
mIpServer.stop();
mLooper.dispatchAll();
verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer);
- verifyStopUpstreamIpv6Forwarding(null);
+ verifyStopUpstreamIpv6Forwarding(null, UPSTREAM_PREFIXES);
verifyTetherOffloadRuleRemove(null,
UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
verifyTetherOffloadRuleRemove(null,
@@ -1244,8 +1330,8 @@
// [1] Enable BPF offload.
// A neighbor that is added or deleted causes the rule to be added or removed.
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- true /* usingBpfOffload */);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+ false /* usingLegacyDhcp */, true /* usingBpfOffload */);
resetNetdBpfMapAndCoordinator();
recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
@@ -1265,15 +1351,17 @@
// Upstream IPv6 connectivity change causes upstream rules change.
LinkProperties lp2 = new LinkProperties();
lp2.setInterfaceName(UPSTREAM_IFACE2);
+ lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, 0);
- verify(mBpfCoordinator).updateAllIpv6Rules(mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2);
- verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX2);
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
resetNetdBpfMapAndCoordinator();
// [2] Disable BPF offload.
// A neighbor that is added or deleted doesn’t cause the rule to be added or removed.
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- false /* usingBpfOffload */);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+ false /* usingLegacyDhcp */, false /* usingBpfOffload */);
resetNetdBpfMapAndCoordinator();
recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
@@ -1293,8 +1381,8 @@
@Test
public void doesNotStartIpNeighborMonitorIfBpfOffloadDisabled() throws Exception {
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- false /* usingBpfOffload */);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+ false /* usingLegacyDhcp */, false /* usingBpfOffload */);
// IP neighbor monitor doesn't start if BPF offload is disabled.
verify(mIpNeighborMonitor, never()).start();
@@ -1576,8 +1664,8 @@
// TODO: move to BpfCoordinatorTest once IpNeighborMonitor is migrated to BpfCoordinator.
@Test
public void addRemoveTetherClient() throws Exception {
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- DEFAULT_USING_BPF_OFFLOAD);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+ false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
final int myIfindex = TEST_IFACE_PARAMS.index;
final int notMyIfindex = myIfindex - 1;
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 601f587..7fbb670 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -55,6 +55,7 @@
import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+import static com.android.testutils.MiscAsserts.assertSameElements;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -73,6 +74,7 @@
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -92,6 +94,7 @@
import android.os.Build;
import android.os.Handler;
import android.os.test.TestLooper;
+import android.util.ArrayMap;
import android.util.SparseArray;
import androidx.annotation.NonNull;
@@ -136,16 +139,20 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
+import org.mockito.verification.VerificationMode;
import java.io.StringWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -159,7 +166,7 @@
private static final int TEST_NET_ID = 24;
private static final int TEST_NET_ID2 = 25;
- private static final int INVALID_IFINDEX = 0;
+ private static final int NO_UPSTREAM = 0;
private static final int UPSTREAM_IFINDEX = 1001;
private static final int UPSTREAM_XLAT_IFINDEX = 1002;
private static final int UPSTREAM_IFINDEX2 = 1003;
@@ -178,8 +185,17 @@
private static final MacAddress MAC_A = MacAddress.fromString("00:00:00:00:00:0a");
private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b");
- private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1");
- private static final InetAddress NEIGH_B = InetAddresses.parseNumericAddress("2001:db8::2");
+ private static final IpPrefix UPSTREAM_PREFIX = new IpPrefix("2001:db8:0:1234::/64");
+ private static final IpPrefix UPSTREAM_PREFIX2 = new IpPrefix("2001:db8:0:abcd::/64");
+ private static final Set<IpPrefix> UPSTREAM_PREFIXES = Set.of(UPSTREAM_PREFIX);
+ private static final Set<IpPrefix> UPSTREAM_PREFIXES2 =
+ Set.of(UPSTREAM_PREFIX, UPSTREAM_PREFIX2);
+ private static final Set<IpPrefix> NO_PREFIXES = Set.of();
+
+ private static final InetAddress NEIGH_A =
+ InetAddresses.parseNumericAddress("2001:db8:0:1234::1");
+ private static final InetAddress NEIGH_B =
+ InetAddresses.parseNumericAddress("2001:db8:0:1234::2");
private static final Inet4Address REMOTE_ADDR =
(Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
@@ -195,7 +211,6 @@
private static final Inet4Address XLAT_LOCAL_IPV4ADDR =
(Inet4Address) InetAddresses.parseNumericAddress("192.0.0.46");
private static final IpPrefix NAT64_IP_PREFIX = new IpPrefix("64:ff9b::/96");
- private static final IpPrefix IPV6_ZERO_PREFIX = new IpPrefix("::/64");
// Generally, public port and private port are the same in the NAT conntrack message.
// TODO: consider using different private port and public port for testing.
@@ -624,10 +639,14 @@
}
private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+ return verifyWithOrder(inOrder, t, times(1));
+ }
+
+ private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t, VerificationMode mode) {
if (inOrder != null) {
- return inOrder.verify(t);
+ return inOrder.verify(t, mode);
} else {
- return verify(t);
+ return verify(t, mode);
}
}
@@ -667,6 +686,28 @@
rule.makeTetherUpstream6Key(), rule.makeTether6Value());
}
+ private void verifyAddUpstreamRules(@Nullable InOrder inOrder,
+ @NonNull Set<Ipv6UpstreamRule> rules) throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ ArrayMap<TetherUpstream6Key, Tether6Value> expected = new ArrayMap<>();
+ for (Ipv6UpstreamRule rule : rules) {
+ expected.put(rule.makeTetherUpstream6Key(), rule.makeTether6Value());
+ }
+ ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+ ArgumentCaptor.forClass(TetherUpstream6Key.class);
+ ArgumentCaptor<Tether6Value> valueCaptor =
+ ArgumentCaptor.forClass(Tether6Value.class);
+ verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).insertEntry(
+ keyCaptor.capture(), valueCaptor.capture());
+ List<TetherUpstream6Key> keys = keyCaptor.getAllValues();
+ List<Tether6Value> values = valueCaptor.getAllValues();
+ ArrayMap<TetherUpstream6Key, Tether6Value> captured = new ArrayMap<>();
+ for (int i = 0; i < keys.size(); i++) {
+ captured.put(keys.get(i), values.get(i));
+ }
+ assertEquals(expected, captured);
+ }
+
private void verifyAddDownstreamRule(@Nullable InOrder inOrder,
@NonNull Ipv6DownstreamRule rule) throws Exception {
if (mDeps.isAtLeastS()) {
@@ -697,6 +738,20 @@
rule.makeTetherUpstream6Key());
}
+ private void verifyRemoveUpstreamRules(@Nullable InOrder inOrder,
+ @NonNull Set<Ipv6UpstreamRule> rules) throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ List<TetherUpstream6Key> expected = new ArrayList<>();
+ for (Ipv6UpstreamRule rule : rules) {
+ expected.add(rule.makeTetherUpstream6Key());
+ }
+ ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+ ArgumentCaptor.forClass(TetherUpstream6Key.class);
+ verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).deleteEntry(
+ keyCaptor.capture());
+ assertSameElements(expected, keyCaptor.getAllValues());
+ }
+
private void verifyRemoveDownstreamRule(@Nullable InOrder inOrder,
@NonNull final Ipv6DownstreamRule rule) throws Exception {
if (mDeps.isAtLeastS()) {
@@ -785,10 +840,11 @@
final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfDownstream6Map, mBpfLimitMap,
mBpfStatsMap);
final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(
- mobileIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(
mobileIfIndex, NEIGH_A, MAC_A);
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
verifyAddUpstreamRule(inOrder, upstreamRule);
@@ -798,7 +854,8 @@
// Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
updateStatsEntryForTetherOffloadGetAndClearStats(
buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, 0);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
verifyRemoveDownstreamRule(inOrder, downstreamRule);
verifyRemoveUpstreamRule(inOrder, upstreamRule);
verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
@@ -998,11 +1055,10 @@
}
@NonNull
- private static Ipv6UpstreamRule buildTestUpstreamRule(
- int upstreamIfindex, int downstreamIfindex, @NonNull MacAddress inDstMac) {
- return new Ipv6UpstreamRule(upstreamIfindex, downstreamIfindex,
- IPV6_ZERO_PREFIX, inDstMac, MacAddress.ALL_ZEROS_ADDRESS,
- MacAddress.ALL_ZEROS_ADDRESS);
+ private static Ipv6UpstreamRule buildTestUpstreamRule(int upstreamIfindex,
+ int downstreamIfindex, @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac) {
+ return new Ipv6UpstreamRule(upstreamIfindex, downstreamIfindex, sourcePrefix, inDstMac,
+ MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS);
}
@NonNull
@@ -1054,9 +1110,10 @@
// Set the unlimited quota as default if the service has never applied a data limit for a
// given upstream. Note that the data limit only be applied on an upstream which has rules.
final Ipv6UpstreamRule rule = buildTestUpstreamRule(
- mobileIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfLimitMap, mBpfStatsMap);
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
verifyAddUpstreamRule(inOrder, rule);
@@ -1104,28 +1161,32 @@
// Adding the first rule on current upstream immediately sends the quota to BPF.
final Ipv6UpstreamRule ruleA = buildTestUpstreamRule(
- mobileIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex);
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */);
verifyAddUpstreamRule(inOrder, ruleA);
inOrder.verifyNoMoreInteractions();
// Adding the second rule on current upstream does not send the quota to BPF.
final Ipv6UpstreamRule ruleB = buildTestUpstreamRule(
- mobileIfIndex, DOWNSTREAM_IFINDEX2, DOWNSTREAM_MAC2);
- coordinator.updateAllIpv6Rules(mIpServer2, DOWNSTREAM_IFACE_PARAMS2, mobileIfIndex);
+ mobileIfIndex, DOWNSTREAM_IFINDEX2, UPSTREAM_PREFIX, DOWNSTREAM_MAC2);
+ coordinator.updateAllIpv6Rules(
+ mIpServer2, DOWNSTREAM_IFACE_PARAMS2, mobileIfIndex, UPSTREAM_PREFIXES);
verifyAddUpstreamRule(inOrder, ruleB);
verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
// Removing the second rule on current upstream does not send the quota to BPF.
- coordinator.updateAllIpv6Rules(mIpServer2, DOWNSTREAM_IFACE_PARAMS2, 0);
+ coordinator.updateAllIpv6Rules(
+ mIpServer2, DOWNSTREAM_IFACE_PARAMS2, NO_UPSTREAM, NO_PREFIXES);
verifyRemoveUpstreamRule(inOrder, ruleB);
verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
// Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
updateStatsEntryForTetherOffloadGetAndClearStats(
buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, 0);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
verifyRemoveUpstreamRule(inOrder, ruleA);
verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
inOrder.verifyNoMoreInteractions();
@@ -1157,13 +1218,14 @@
// [1] Adding rules on the upstream Ethernet.
// Note that the default data limit is applied after the first rule is added.
final Ipv6UpstreamRule ethernetUpstreamRule = buildTestUpstreamRule(
- ethIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ ethIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
final Ipv6DownstreamRule ethernetRuleA = buildTestDownstreamRule(
ethIfIndex, NEIGH_A, MAC_A);
final Ipv6DownstreamRule ethernetRuleB = buildTestDownstreamRule(
ethIfIndex, NEIGH_B, MAC_B);
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, ethIfIndex);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, ethIfIndex, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
verifyAddUpstreamRule(inOrder, ethernetUpstreamRule);
@@ -1174,7 +1236,9 @@
// [2] Update the existing rules from Ethernet to cellular.
final Ipv6UpstreamRule mobileUpstreamRule = buildTestUpstreamRule(
- mobileIfIndex, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+ final Ipv6UpstreamRule mobileUpstreamRule2 = buildTestUpstreamRule(
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX2, DOWNSTREAM_MAC);
final Ipv6DownstreamRule mobileRuleA = buildTestDownstreamRule(
mobileIfIndex, NEIGH_A, MAC_A);
final Ipv6DownstreamRule mobileRuleB = buildTestDownstreamRule(
@@ -1183,15 +1247,16 @@
buildTestTetherStatsParcel(ethIfIndex, 10, 20, 30, 40));
// Update the existing rules for upstream changes. The rules are removed and re-added one
- // by one for updating upstream interface index by #tetherOffloadRuleUpdate.
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex);
+ // by one for updating upstream interface index and prefixes by #tetherOffloadRuleUpdate.
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES2);
verifyRemoveDownstreamRule(inOrder, ethernetRuleA);
verifyRemoveDownstreamRule(inOrder, ethernetRuleB);
verifyRemoveUpstreamRule(inOrder, ethernetUpstreamRule);
verifyTetherOffloadGetAndClearStats(inOrder, ethIfIndex);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
- verifyAddUpstreamRule(inOrder, mobileUpstreamRule);
+ verifyAddUpstreamRules(inOrder, Set.of(mobileUpstreamRule, mobileUpstreamRule2));
verifyAddDownstreamRule(inOrder, mobileRuleA);
verifyAddDownstreamRule(inOrder, mobileRuleB);
@@ -1201,7 +1266,7 @@
coordinator.clearAllIpv6Rules(mIpServer);
verifyRemoveDownstreamRule(inOrder, mobileRuleA);
verifyRemoveDownstreamRule(inOrder, mobileRuleB);
- verifyRemoveUpstreamRule(inOrder, mobileUpstreamRule);
+ verifyRemoveUpstreamRules(inOrder, Set.of(mobileUpstreamRule, mobileUpstreamRule2));
verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
// [4] Force pushing stats update to verify that the last diff of stats is reported on all
@@ -1264,8 +1329,8 @@
assertEquals(1, rules.size());
// The rule can't be updated.
- coordinator.updateAllIpv6Rules(
- mIpServer, DOWNSTREAM_IFACE_PARAMS, rule.upstreamIfindex + 1 /* new */);
+ coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS,
+ rule.upstreamIfindex + 1 /* new */, UPSTREAM_PREFIXES);
verifyNeverRemoveDownstreamRule();
verifyNeverAddDownstreamRule();
rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
@@ -1561,12 +1626,12 @@
//
// @param coordinator BpfCoordinator instance.
// @param upstreamIfindex upstream interface index. can be the following values.
- // INVALID_IFINDEX: no upstream interface
+ // NO_UPSTREAM: no upstream interface
// UPSTREAM_IFINDEX: CELLULAR (raw ip interface)
// UPSTREAM_IFINDEX2: WIFI (ethernet interface)
private void setUpstreamInformationTo(final BpfCoordinator coordinator,
@Nullable Integer upstreamIfindex) {
- if (upstreamIfindex == INVALID_IFINDEX) {
+ if (upstreamIfindex == NO_UPSTREAM) {
coordinator.updateUpstreamNetworkState(null);
return;
}
@@ -1706,7 +1771,8 @@
final BpfCoordinator coordinator = makeBpfCoordinator();
coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
- coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS, UPSTREAM_IFINDEX);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
eq(new TetherDevValue(UPSTREAM_IFINDEX)));
verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)),
@@ -1715,7 +1781,8 @@
// Adding the second downstream, only the second downstream ifindex is added to DevMap,
// the existing upstream ifindex won't be added again.
- coordinator.updateAllIpv6Rules(mIpServer2, DOWNSTREAM_IFACE_PARAMS2, UPSTREAM_IFINDEX);
+ coordinator.updateAllIpv6Rules(
+ mIpServer2, DOWNSTREAM_IFACE_PARAMS2, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX2)),
eq(new TetherDevValue(DOWNSTREAM_IFINDEX2)));
verify(mBpfDevMap, never()).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
@@ -1996,6 +2063,11 @@
100 /* nonzero, CT_NEW */);
}
+ private static long prefixToLong(IpPrefix prefix) {
+ byte[] prefixBytes = Arrays.copyOf(prefix.getRawAddress(), 8);
+ return ByteBuffer.wrap(prefixBytes).getLong();
+ }
+
void checkRule4ExistInUpstreamDownstreamMap() throws Exception {
assertEquals(UPSTREAM4_RULE_VALUE_A, mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_A));
assertEquals(DOWNSTREAM4_RULE_VALUE_A, mBpfDownstream4Map.getValue(
@@ -2113,7 +2185,7 @@
// [3] Switch upstream from the first upstream (rawip, bpf supported) to no upstream. Clear
// all rules.
- setUpstreamInformationTo(coordinator, INVALID_IFINDEX);
+ setUpstreamInformationTo(coordinator, NO_UPSTREAM);
checkRule4NotExistInUpstreamDownstreamMap();
// Client information should be not deleted.
@@ -2180,14 +2252,15 @@
public void testIpv6ForwardingRuleToString() throws Exception {
final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A,
MAC_A);
- assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, address: 2001:db8::1, "
+ assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, address: 2001:db8:0:1234::1, "
+ "srcMac: 12:34:56:78:90:ab, dstMac: 00:00:00:00:00:0a",
downstreamRule.toString());
final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(
- UPSTREAM_IFINDEX, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
- assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, sourcePrefix: ::/64, "
- + "inDstMac: 12:34:56:78:90:ab, outSrcMac: 00:00:00:00:00:00, "
- + "outDstMac: 00:00:00:00:00:00", upstreamRule.toString());
+ UPSTREAM_IFINDEX, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+ assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, "
+ + "sourcePrefix: 2001:db8:0:1234::/64, inDstMac: 12:34:56:78:90:ab, "
+ + "outSrcMac: 00:00:00:00:00:00, outDstMac: 00:00:00:00:00:00",
+ upstreamRule.toString());
}
private void verifyDump(@NonNull final BpfCoordinator coordinator) {
@@ -2237,8 +2310,9 @@
final Ipv6DownstreamRule rule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
mBpfDownstream6Map.insertEntry(rule.makeTetherDownstream6Key(), rule.makeTether6Value());
+ final long prefix64 = prefixToLong(UPSTREAM_PREFIX);
final TetherUpstream6Key upstream6Key = new TetherUpstream6Key(DOWNSTREAM_IFINDEX,
- DOWNSTREAM_MAC, 0);
+ DOWNSTREAM_MAC, prefix64);
final Tether6Value upstream6Value = new Tether6Value(UPSTREAM_IFINDEX,
MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 91b092a..6ebd6ae 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -126,16 +126,17 @@
final LinkAddress newAddress = requestDownstreamAddress(mHotspotIpServer,
CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- final IpPrefix testDupRequest = asIpPrefix(newAddress);
- assertNotEquals(hotspotPrefix, testDupRequest);
- assertNotEquals(bluetoothPrefix, testDupRequest);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
+ final IpPrefix newHotspotPrefix = asIpPrefix(newAddress);
+ assertNotEquals(hotspotPrefix, newHotspotPrefix);
+ assertNotEquals(bluetoothPrefix, newHotspotPrefix);
final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
final IpPrefix usbPrefix = asIpPrefix(usbAddress);
assertNotEquals(usbPrefix, bluetoothPrefix);
- assertNotEquals(usbPrefix, hotspotPrefix);
+ assertNotEquals(usbPrefix, newHotspotPrefix);
+
+ mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer);
}
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 770507e..6eba590 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -142,6 +142,7 @@
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.RouteInfo;
+import android.net.RoutingCoordinatorManager;
import android.net.TetherStatesParcel;
import android.net.TetheredClient;
import android.net.TetheredClient.AddressInfo;
@@ -191,6 +192,7 @@
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.ip.IpNeighborMonitor;
import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -300,7 +302,7 @@
// 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 TestLooper mLooper;
private Vector<Intent> mIntents;
private BroadcastInterceptingContext mServiceContext;
@@ -482,6 +484,12 @@
return mEntitleMgr;
}
+ @Nullable
+ @Override
+ public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(final Context context) {
+ return new LateSdk<>(null);
+ }
+
@Override
public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
int subId) {
@@ -672,7 +680,14 @@
mCm = spy(new TestConnectivityManager(mServiceContext, mock(IConnectivityManager.class)));
- mTethering = makeTethering();
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
+ }
+
+ // In order to interact with syncSM from the test, tethering must be created in test thread.
+ private void initTetheringOnTestThread() throws Exception {
+ mLooper = new TestLooper();
+ mTethering = new Tethering(mTetheringDependencies);
verify(mStatsManager, times(1)).registerNetworkStatsProvider(anyString(), any());
verify(mNetd).registerUnsolicitedEventListener(any());
verifyDefaultNetworkRequestFiled();
@@ -696,9 +711,6 @@
localOnlyCallbackCaptor.capture());
mLocalOnlyHotspotCallback = localOnlyCallbackCaptor.getValue();
}
-
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
}
private void setTetheringSupported(final boolean supported) {
@@ -730,10 +742,6 @@
doReturn(upstreamState).when(mUpstreamNetworkMonitor).selectPreferredUpstreamType(any());
}
- private Tethering makeTethering() {
- return new Tethering(mTetheringDependencies);
- }
-
private TetheringRequestParcel createTetheringRequestParcel(final int type) {
return createTetheringRequestParcel(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL);
}
@@ -877,6 +885,7 @@
public void failingLocalOnlyHotspotLegacyApBroadcast(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
// Emulate externally-visible WifiManager effects, causing the
// per-interface state machine to start up, and telling us that
// hotspot mode is to be started.
@@ -928,6 +937,7 @@
@Test
public void testUsbConfiguredBroadcastStartsTethering() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
initTetheringUpstream(upstreamState);
prepareUsbTethering();
@@ -1004,6 +1014,7 @@
public void workingLocalOnlyHotspotEnrichedApBroadcast(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
// Emulate externally-visible WifiManager effects, causing the
// per-interface state machine to start up, and telling us that
// hotspot mode is to be started.
@@ -1067,6 +1078,7 @@
@Test
public void workingMobileUsbTethering_IPv4() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
runUsbTethering(upstreamState);
@@ -1081,7 +1093,8 @@
}
@Test
- public void workingMobileUsbTethering_IPv4LegacyDhcp() {
+ public void workingMobileUsbTethering_IPv4LegacyDhcp() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
true);
sendConfigurationChanged();
@@ -1094,6 +1107,7 @@
@Test
public void workingMobileUsbTethering_IPv6() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
runUsbTethering(upstreamState);
@@ -1109,6 +1123,7 @@
@Test
public void workingMobileUsbTethering_DualStack() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
runUsbTethering(upstreamState);
@@ -1126,6 +1141,7 @@
@Test
public void workingMobileUsbTethering_MultipleUpstreams() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobile464xlatUpstreamState();
runUsbTethering(upstreamState);
@@ -1145,6 +1161,7 @@
@Test
public void workingMobileUsbTethering_v6Then464xlat() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
when(mResources.getStringArray(R.array.config_tether_usb_regexs))
@@ -1186,6 +1203,7 @@
@Test
public void configTetherUpstreamAutomaticIgnoresConfigTetherUpstreamTypes() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true);
sendConfigurationChanged();
@@ -1234,6 +1252,7 @@
}
private void verifyAutomaticUpstreamSelection(boolean configAutomatic) throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1333,6 +1352,7 @@
@Test
@IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
public void testLegacyUpstreamSelection() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1483,6 +1503,7 @@
// +-------+-------+-------+-------+-------+
//
private void verifyChooseDunUpstreamByAutomaticMode(boolean configAutomatic) throws Exception {
+ initTetheringOnTestThread();
// Enable automatic upstream selection.
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
@@ -1543,6 +1564,7 @@
//
@Test
public void testChooseDunUpstreamByAutomaticMode_defaultNetworkWifi() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
@@ -1594,6 +1616,7 @@
//
@Test
public void testChooseDunUpstreamByAutomaticMode_loseDefaultNetworkWifi() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
final InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1635,6 +1658,7 @@
//
@Test
public void testChooseDunUpstreamByAutomaticMode_defaultNetworkCell() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
final InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1679,6 +1703,7 @@
//
@Test
public void testChooseDunUpstreamByAutomaticMode_loseAndRegainDun() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
final InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
setupDunUpstreamTest(true /* configAutomatic */, inOrder);
@@ -1720,6 +1745,7 @@
@Test
public void testChooseDunUpstreamByAutomaticMode_switchDefaultFromWifiToCell()
throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
@@ -1757,6 +1783,7 @@
@Test
@IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
public void testChooseDunUpstreamByLegacyMode() throws Exception {
+ initTetheringOnTestThread();
// Enable Legacy upstream selection.
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
@@ -1849,6 +1876,7 @@
@Test
public void workingNcmTethering() throws Exception {
+ initTetheringOnTestThread();
runNcmTethering();
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
@@ -1856,7 +1884,8 @@
}
@Test
- public void workingNcmTethering_LegacyDhcp() {
+ public void workingNcmTethering_LegacyDhcp() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
true);
sendConfigurationChanged();
@@ -1878,6 +1907,7 @@
// TODO: Test with and without interfaceStatusChanged().
@Test
public void failingWifiTetheringLegacyApBroadcast() throws Exception {
+ initTetheringOnTestThread();
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
// Emulate pressing the WiFi tethering button.
@@ -1906,6 +1936,7 @@
// TODO: Test with and without interfaceStatusChanged().
@Test
public void workingWifiTetheringEnrichedApBroadcast() throws Exception {
+ initTetheringOnTestThread();
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
// Emulate pressing the WiFi tethering button.
@@ -1954,6 +1985,7 @@
// TODO: Test with and without interfaceStatusChanged().
@Test
public void failureEnablingIpForwarding() throws Exception {
+ initTetheringOnTestThread();
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
@@ -2101,7 +2133,8 @@
}
@Test
- public void testUntetherUsbWhenRestrictionIsOn() {
+ public void testUntetherUsbWhenRestrictionIsOn() throws Exception {
+ initTetheringOnTestThread();
// Start usb tethering and check that usb interface is tethered.
final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
runUsbTethering(upstreamState);
@@ -2278,6 +2311,7 @@
@Test
public void testRegisterTetheringEventCallback() throws Exception {
+ initTetheringOnTestThread();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
final TetheringInterface wifiIface = new TetheringInterface(
@@ -2342,6 +2376,7 @@
@Test
public void testReportFailCallbackIfOffloadNotSupported() throws Exception {
+ initTetheringOnTestThread();
final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
mTethering.registerTetheringEventCallback(callback);
@@ -2381,6 +2416,7 @@
@Test
public void testMultiSimAware() throws Exception {
+ initTetheringOnTestThread();
final TetheringConfiguration initailConfig = mTethering.getTetheringConfiguration();
assertEquals(INVALID_SUBSCRIPTION_ID, initailConfig.activeDataSubId);
@@ -2393,6 +2429,7 @@
@Test
public void testNoDuplicatedEthernetRequest() throws Exception {
+ initTetheringOnTestThread();
final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class);
when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
@@ -2413,6 +2450,7 @@
private void workingWifiP2pGroupOwner(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
if (emulateInterfaceStatusChanged) {
mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
}
@@ -2452,6 +2490,7 @@
private void workingWifiP2pGroupClient(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
if (emulateInterfaceStatusChanged) {
mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
}
@@ -2492,6 +2531,7 @@
private void workingWifiP2pGroupOwnerLegacyMode(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
// change to legacy mode and update tethering information by chaning SIM
when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs))
.thenReturn(new String[]{});
@@ -2541,7 +2581,8 @@
}
@Test
- public void testDataSaverChanged() {
+ public void testDataSaverChanged() throws Exception {
+ initTetheringOnTestThread();
// Start Tethering.
final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
runUsbTethering(upstreamState);
@@ -2596,6 +2637,7 @@
@Test
public void testMultipleStartTethering() throws Exception {
+ initTetheringOnTestThread();
final LinkAddress serverLinkAddr = new LinkAddress("192.168.20.1/24");
final LinkAddress clientLinkAddr = new LinkAddress("192.168.20.42/24");
final String serverAddr = "192.168.20.1";
@@ -2639,6 +2681,7 @@
@Test
public void testRequestStaticIp() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
when(mResources.getStringArray(R.array.config_tether_usb_regexs))
@@ -2668,7 +2711,8 @@
}
@Test
- public void testUpstreamNetworkChanged() {
+ public void testUpstreamNetworkChanged() throws Exception {
+ initTetheringOnTestThread();
final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM)
mTetheringDependencies.mUpstreamNetworkMonitorSM;
final InOrder inOrder = inOrder(mNotificationUpdater);
@@ -2710,7 +2754,8 @@
}
@Test
- public void testUpstreamCapabilitiesChanged() {
+ public void testUpstreamCapabilitiesChanged() throws Exception {
+ initTetheringOnTestThread();
final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM)
mTetheringDependencies.mUpstreamNetworkMonitorSM;
final InOrder inOrder = inOrder(mNotificationUpdater);
@@ -2745,6 +2790,7 @@
@Test
public void testUpstreamCapabilitiesChanged_startStopTethering() throws Exception {
+ initTetheringOnTestThread();
final TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
// Start USB tethering with no current upstream.
@@ -2766,6 +2812,7 @@
@Test
public void testDumpTetheringLog() throws Exception {
+ initTetheringOnTestThread();
final FileDescriptor mockFd = mock(FileDescriptor.class);
final PrintWriter mockPw = mock(PrintWriter.class);
runUsbTethering(null);
@@ -2779,6 +2826,7 @@
@Test
public void testExemptFromEntitlementCheck() throws Exception {
+ initTetheringOnTestThread();
setupForRequiredProvisioning();
final TetheringRequestParcel wifiNotExemptRequest =
createTetheringRequestParcel(TETHERING_WIFI, null, null, false,
@@ -2869,31 +2917,42 @@
@Test
public void testHandleIpConflict() throws Exception {
+ initTetheringOnTestThread();
final Network wifiNetwork = new Network(200);
final Network[] allNetworks = { wifiNetwork };
doReturn(allNetworks).when(mCm).getAllNetworks();
+ InOrder inOrder = inOrder(mUsbManager, mNetd);
runUsbTethering(null);
+
+ inOrder.verify(mNetd).tetherInterfaceAdd(TEST_RNDIS_IFNAME);
+
final ArgumentCaptor<InterfaceConfigurationParcel> ifaceConfigCaptor =
ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
verify(mNetd).interfaceSetCfg(ifaceConfigCaptor.capture());
final String ipv4Address = ifaceConfigCaptor.getValue().ipv4Addr;
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
any(), any());
- reset(mUsbManager);
// Cause a prefix conflict by assigning a /30 out of the downstream's /24 to the upstream.
updateV4Upstream(new LinkAddress(InetAddresses.parseNumericAddress(ipv4Address), 30),
wifiNetwork, TEST_WIFI_IFNAME, TRANSPORT_WIFI);
// verify turn off usb tethering
- verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ inOrder.verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
sendUsbBroadcast(true, true, -1 /* function */);
mLooper.dispatchAll();
+ inOrder.verify(mNetd).tetherInterfaceRemove(TEST_RNDIS_IFNAME);
+
// verify restart usb tethering
- verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+ inOrder.verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+
+ sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
+ mLooper.dispatchAll();
+ inOrder.verify(mNetd).tetherInterfaceAdd(TEST_RNDIS_IFNAME);
}
@Test
public void testNoAddressAvailable() throws Exception {
+ initTetheringOnTestThread();
final Network wifiNetwork = new Network(200);
final Network btNetwork = new Network(201);
final Network mobileNetwork = new Network(202);
@@ -2955,6 +3014,7 @@
@Test
public void testProvisioningNeededButUnavailable() throws Exception {
+ initTetheringOnTestThread();
assertTrue(mTethering.isTetheringSupported());
verify(mPackageManager, never()).getPackageInfo(PROVISIONING_APP_NAME[0], GET_ACTIVITIES);
@@ -2972,6 +3032,7 @@
@Test
public void testUpdateConnectedClients() throws Exception {
+ initTetheringOnTestThread();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
runAsShell(NETWORK_SETTINGS, () -> {
mTethering.registerTetheringEventCallback(callback);
@@ -3021,6 +3082,7 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testUpdateConnectedClientsForLocalOnlyHotspot() throws Exception {
+ initTetheringOnTestThread();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
runAsShell(NETWORK_SETTINGS, () -> {
mTethering.registerTetheringEventCallback(callback);
@@ -3053,6 +3115,7 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testConnectedClientsForSapAndLohsConcurrency() throws Exception {
+ initTetheringOnTestThread();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
runAsShell(NETWORK_SETTINGS, () -> {
mTethering.registerTetheringEventCallback(callback);
@@ -3178,6 +3241,7 @@
@Test
public void testBluetoothTethering() throws Exception {
+ initTetheringOnTestThread();
// Switch to @IgnoreUpTo(Build.VERSION_CODES.S_V2) when it is available for AOSP.
assumeTrue(isAtLeastT());
@@ -3214,6 +3278,7 @@
@Test
public void testBluetoothTetheringBeforeT() throws Exception {
+ initTetheringOnTestThread();
// Switch to @IgnoreAfter(Build.VERSION_CODES.S_V2) when it is available for AOSP.
assumeFalse(isAtLeastT());
@@ -3261,6 +3326,7 @@
@Test
public void testBluetoothServiceDisconnects() throws Exception {
+ initTetheringOnTestThread();
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
@@ -3415,6 +3481,7 @@
@Test
public void testUsbFunctionConfigurationChange() throws Exception {
+ initTetheringOnTestThread();
// Run TETHERING_NCM.
runNcmTethering();
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
@@ -3473,6 +3540,7 @@
@Test
public void testTetheringSupported() throws Exception {
+ initTetheringOnTestThread();
final ArraySet<Integer> expectedTypes = getAllSupportedTetheringTypes();
// Check tethering is supported after initialization.
TestTetheringEventCallback callback = new TestTetheringEventCallback();
@@ -3545,6 +3613,7 @@
@Test
public void testIpv4AddressForSapAndLohsConcurrency() throws Exception {
+ initTetheringOnTestThread();
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
new file mode 100644
index 0000000..f8e98e3
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
@@ -0,0 +1,135 @@
+/**
+ * Copyright (C) 2023 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.util
+
+import android.os.Looper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.util.State
+import com.android.networkstack.tethering.util.StateMachineShim.AsyncStateMachine
+import com.android.networkstack.tethering.util.StateMachineShim.Dependencies
+import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class StateMachineShimTest {
+ private val mSyncSM = mock(SyncStateMachine::class.java)
+ private val mAsyncSM = mock(AsyncStateMachine::class.java)
+ private val mState1 = mock(State::class.java)
+ private val mState2 = mock(State::class.java)
+
+ inner class MyDependencies() : Dependencies() {
+
+ override fun makeSyncStateMachine(name: String, thread: Thread) = mSyncSM
+
+ override fun makeAsyncStateMachine(name: String, looper: Looper) = mAsyncSM
+ }
+
+ @Test
+ fun testUsingSyncStateMachine() {
+ val inOrder = inOrder(mSyncSM, mAsyncSM)
+ val shimUsingSyncSM = StateMachineShim("ShimTest", null, MyDependencies())
+ shimUsingSyncSM.start(mState1)
+ inOrder.verify(mSyncSM).start(mState1)
+
+ val allStates = ArrayList<StateInfo>()
+ allStates.add(StateInfo(mState1, null))
+ allStates.add(StateInfo(mState2, mState1))
+ shimUsingSyncSM.addAllStates(allStates)
+ inOrder.verify(mSyncSM).addAllStates(allStates)
+
+ shimUsingSyncSM.transitionTo(mState1)
+ inOrder.verify(mSyncSM).transitionTo(mState1)
+
+ val what = 10
+ shimUsingSyncSM.sendMessage(what)
+ inOrder.verify(mSyncSM).processMessage(what, 0, 0, null)
+ val obj = Object()
+ shimUsingSyncSM.sendMessage(what, obj)
+ inOrder.verify(mSyncSM).processMessage(what, 0, 0, obj)
+ val arg1 = 11
+ shimUsingSyncSM.sendMessage(what, arg1)
+ inOrder.verify(mSyncSM).processMessage(what, arg1, 0, null)
+ val arg2 = 12
+ shimUsingSyncSM.sendMessage(what, arg1, arg2, obj)
+ inOrder.verify(mSyncSM).processMessage(what, arg1, arg2, obj)
+
+ assertFailsWith(IllegalStateException::class) {
+ shimUsingSyncSM.sendMessageDelayedToAsyncSM(what, 1000 /* delayMillis */)
+ }
+
+ assertFailsWith(IllegalStateException::class) {
+ shimUsingSyncSM.sendMessageAtFrontOfQueueToAsyncSM(what, arg1)
+ }
+
+ shimUsingSyncSM.sendSelfMessageToSyncSM(what, obj)
+ inOrder.verify(mSyncSM).sendSelfMessage(what, 0, 0, obj)
+
+ verifyNoMoreInteractions(mSyncSM, mAsyncSM)
+ }
+
+ @Test
+ fun testUsingAsyncStateMachine() {
+ val inOrder = inOrder(mSyncSM, mAsyncSM)
+ val shimUsingAsyncSM = StateMachineShim("ShimTest", mock(Looper::class.java),
+ MyDependencies())
+ shimUsingAsyncSM.start(mState1)
+ inOrder.verify(mAsyncSM).setInitialState(mState1)
+ inOrder.verify(mAsyncSM).start()
+
+ val allStates = ArrayList<StateInfo>()
+ allStates.add(StateInfo(mState1, null))
+ allStates.add(StateInfo(mState2, mState1))
+ shimUsingAsyncSM.addAllStates(allStates)
+ inOrder.verify(mAsyncSM).addState(mState1, null)
+ inOrder.verify(mAsyncSM).addState(mState2, mState1)
+
+ shimUsingAsyncSM.transitionTo(mState1)
+ inOrder.verify(mAsyncSM).transitionTo(mState1)
+
+ val what = 10
+ shimUsingAsyncSM.sendMessage(what)
+ inOrder.verify(mAsyncSM).sendMessage(what, 0, 0, null)
+ val obj = Object()
+ shimUsingAsyncSM.sendMessage(what, obj)
+ inOrder.verify(mAsyncSM).sendMessage(what, 0, 0, obj)
+ val arg1 = 11
+ shimUsingAsyncSM.sendMessage(what, arg1)
+ inOrder.verify(mAsyncSM).sendMessage(what, arg1, 0, null)
+ val arg2 = 12
+ shimUsingAsyncSM.sendMessage(what, arg1, arg2, obj)
+ inOrder.verify(mAsyncSM).sendMessage(what, arg1, arg2, obj)
+
+ shimUsingAsyncSM.sendMessageDelayedToAsyncSM(what, 1000 /* delayMillis */)
+ inOrder.verify(mAsyncSM).sendMessageDelayed(what, 1000)
+
+ shimUsingAsyncSM.sendMessageAtFrontOfQueueToAsyncSM(what, arg1)
+ inOrder.verify(mAsyncSM).sendMessageAtFrontOfQueueToAsyncSM(what, arg1)
+
+ assertFailsWith(IllegalStateException::class) {
+ shimUsingAsyncSM.sendSelfMessageToSyncSM(what, obj)
+ }
+
+ verifyNoMoreInteractions(mSyncSM, mAsyncSM)
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt
new file mode 100644
index 0000000..3a57fdd
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt
@@ -0,0 +1,294 @@
+/**
+ * Copyright (C) 2023 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.util
+
+import android.os.Message
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.util.State
+import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo
+import java.util.ArrayDeque
+import java.util.ArrayList
+import kotlin.test.assertFailsWith
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+private const val MSG_INVALID = -1
+private const val MSG_1 = 1
+private const val MSG_2 = 2
+private const val MSG_3 = 3
+private const val MSG_4 = 4
+private const val MSG_5 = 5
+private const val MSG_6 = 6
+private const val MSG_7 = 7
+private const val ARG_1 = 100
+private const val ARG_2 = 200
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class SynStateMachineTest {
+ private val mState1 = spy(object : TestState(MSG_1) {})
+ private val mState2 = spy(object : TestState(MSG_2) {})
+ private val mState3 = spy(object : TestState(MSG_3) {})
+ private val mState4 = spy(object : TestState(MSG_4) {})
+ private val mState5 = spy(object : TestState(MSG_5) {})
+ private val mState6 = spy(object : TestState(MSG_6) {})
+ private val mState7 = spy(object : TestState(MSG_7) {})
+ private val mInOrder = inOrder(mState1, mState2, mState3, mState4, mState5, mState6, mState7)
+ // Lazy initialize to make sure running in test thread.
+ private val mSM by lazy {
+ SyncStateMachine("TestSyncStateMachine", Thread.currentThread(), true /* debug */)
+ }
+ private val mAllStates = ArrayList<StateInfo>()
+
+ private val mMsgProcessedResults = ArrayDeque<Pair<State, Int>>()
+
+ open inner class TestState(val expected: Int) : State() {
+ // Control destination state in obj field for testing.
+ override fun processMessage(msg: Message): Boolean {
+ mMsgProcessedResults.add(this to msg.what)
+ assertEquals(ARG_1, msg.arg1)
+ assertEquals(ARG_2, msg.arg2)
+
+ if (msg.what == expected) {
+ msg.obj?.let { mSM.transitionTo(it as State) }
+ return true
+ }
+
+ return false
+ }
+ }
+
+ private fun verifyNoMoreInteractions() {
+ verifyNoMoreInteractions(mState1, mState2, mState3, mState4, mState5, mState6)
+ }
+
+ private fun processMessage(what: Int, toState: State?) {
+ mSM.processMessage(what, ARG_1, ARG_2, toState)
+ }
+
+ private fun verifyMessageProcessedBy(what: Int, vararg processedStates: State) {
+ for (state in processedStates) {
+ // InOrder.verify can't check the Message content here because SyncSM will recycle the
+ // message after it's been processed. SyncSM reuses the same Message instance for all
+ // messages it processes. So, if using InOrder.verify to verify the content of a message
+ // after SyncSM has processed it, the content would be wrong.
+ mInOrder.verify(state).processMessage(any())
+ val (processedState, msgWhat) = mMsgProcessedResults.remove()
+ assertEquals(state, processedState)
+ assertEquals(what, msgWhat)
+ }
+ assertTrue(mMsgProcessedResults.isEmpty())
+ }
+
+ @Test
+ fun testInitialState() {
+ // mState1 -> initial
+ // |
+ // mState2
+ mAllStates.add(StateInfo(mState1, null))
+ mAllStates.add(StateInfo(mState2, mState1))
+ mSM.addAllStates(mAllStates)
+
+ mSM.start(mState1)
+ mInOrder.verify(mState1).enter()
+ verifyNoMoreInteractions()
+ }
+
+ @Test
+ fun testStartFromLeafState() {
+ // mState1 -> initial
+ // |
+ // mState2
+ // |
+ // mState3
+ mAllStates.add(StateInfo(mState1, null))
+ mAllStates.add(StateInfo(mState2, mState1))
+ mAllStates.add(StateInfo(mState3, mState2))
+ mSM.addAllStates(mAllStates)
+
+ mSM.start(mState3)
+ mInOrder.verify(mState1).enter()
+ mInOrder.verify(mState2).enter()
+ mInOrder.verify(mState3).enter()
+ verifyNoMoreInteractions()
+ }
+
+ private fun verifyStart() {
+ mSM.addAllStates(mAllStates)
+ mSM.start(mState1)
+ mInOrder.verify(mState1).enter()
+ verifyNoMoreInteractions()
+ }
+
+ fun addState(state: State, parent: State? = null) {
+ mAllStates.add(StateInfo(state, parent))
+ }
+
+ @Test
+ fun testAddState() {
+ // Add duplicated states.
+ mAllStates.add(StateInfo(mState1, null))
+ mAllStates.add(StateInfo(mState1, null))
+ assertFailsWith(IllegalStateException::class) {
+ mSM.addAllStates(mAllStates)
+ }
+ }
+
+ @Test
+ fun testProcessMessage() {
+ // mState1
+ // |
+ // mState2
+ addState(mState1)
+ addState(mState2, mState1)
+ verifyStart()
+
+ processMessage(MSG_1, null)
+ verifyMessageProcessedBy(MSG_1, mState1)
+ verifyNoMoreInteractions()
+ }
+
+ @Test
+ fun testTwoStates() {
+ // mState1 <-initial, mState2
+ addState(mState1)
+ addState(mState2)
+ verifyStart()
+
+ // Test transition to mState2
+ processMessage(MSG_1, mState2)
+ verifyMessageProcessedBy(MSG_1, mState1)
+ mInOrder.verify(mState1).exit()
+ mInOrder.verify(mState2).enter()
+ verifyNoMoreInteractions()
+
+ // If set destState to mState2 (current state), no state transition.
+ processMessage(MSG_2, mState2)
+ verifyMessageProcessedBy(MSG_2, mState2)
+ verifyNoMoreInteractions()
+ }
+
+ @Test
+ fun testTwoStateTrees() {
+ // mState1 -> initial mState4
+ // / \ / \
+ // mState2 mState3 mState5 mState6
+ addState(mState1)
+ addState(mState2, mState1)
+ addState(mState3, mState1)
+ addState(mState4)
+ addState(mState5, mState4)
+ addState(mState6, mState4)
+ verifyStart()
+
+ // mState1 -> current mState4
+ // / \ / \
+ // mState2 mState3 -> dest mState5 mState6
+ processMessage(MSG_1, mState3)
+ verifyMessageProcessedBy(MSG_1, mState1)
+ mInOrder.verify(mState3).enter()
+ verifyNoMoreInteractions()
+
+ // mState1 mState4
+ // / \ / \
+ // dest <- mState2 mState3 -> current mState5 mState6
+ processMessage(MSG_1, mState2)
+ verifyMessageProcessedBy(MSG_1, mState3, mState1)
+ mInOrder.verify(mState3).exit()
+ mInOrder.verify(mState2).enter()
+ verifyNoMoreInteractions()
+
+ // mState1 mState4
+ // / \ / \
+ // current <- mState2 mState3 mState5 mState6 -> dest
+ processMessage(MSG_2, mState6)
+ verifyMessageProcessedBy(MSG_2, mState2)
+ mInOrder.verify(mState2).exit()
+ mInOrder.verify(mState1).exit()
+ mInOrder.verify(mState4).enter()
+ mInOrder.verify(mState6).enter()
+ verifyNoMoreInteractions()
+ }
+
+ @Test
+ fun testMultiDepthTransition() {
+ // mState1 -> current
+ // | \
+ // mState2 mState6
+ // | \ |
+ // mState3 mState5 mState7
+ // |
+ // mState4
+ addState(mState1)
+ addState(mState2, mState1)
+ addState(mState6, mState1)
+ addState(mState3, mState2)
+ addState(mState5, mState2)
+ addState(mState7, mState6)
+ addState(mState4, mState3)
+ verifyStart()
+
+ // mState1 -> current
+ // | \
+ // mState2 mState6
+ // | \ |
+ // mState3 mState5 mState7
+ // |
+ // mState4 -> dest
+ processMessage(MSG_1, mState4)
+ verifyMessageProcessedBy(MSG_1, mState1)
+ mInOrder.verify(mState2).enter()
+ mInOrder.verify(mState3).enter()
+ mInOrder.verify(mState4).enter()
+ verifyNoMoreInteractions()
+
+ // mState1
+ // / \
+ // mState2 mState6
+ // | \ \
+ // mState3 mState5 -> dest mState7
+ // |
+ // mState4 -> current
+ processMessage(MSG_1, mState5)
+ verifyMessageProcessedBy(MSG_1, mState4, mState3, mState2, mState1)
+ mInOrder.verify(mState4).exit()
+ mInOrder.verify(mState3).exit()
+ mInOrder.verify(mState5).enter()
+ verifyNoMoreInteractions()
+
+ // mState1
+ // / \
+ // mState2 mState6
+ // | \ \
+ // mState3 mState5 -> current mState7 -> dest
+ // |
+ // mState4
+ processMessage(MSG_2, mState7)
+ verifyMessageProcessedBy(MSG_2, mState5, mState2)
+ mInOrder.verify(mState5).exit()
+ mInOrder.verify(mState2).exit()
+ mInOrder.verify(mState6).enter()
+ mInOrder.verify(mState7).enter()
+ verifyNoMoreInteractions()
+ }
+}
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
index d734b74..0a2b0b8 100644
--- a/bpf_progs/block.c
+++ b/bpf_progs/block.c
@@ -24,8 +24,8 @@
#include "bpf_helpers.h"
-#define ALLOW 1
-#define DISALLOW 0
+static const int ALLOW = 1;
+static const int DISALLOW = 0;
DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
@@ -57,14 +57,18 @@
return ALLOW;
}
-DEFINE_BPF_PROG_KVER("bind4/block_port", AID_ROOT, AID_SYSTEM,
- bind4_block_port, KVER(5, 4, 0))
+// the program need to be accessible/loadable by netd (from netd updatable plugin)
+#define DEFINE_NETD_RO_BPF_PROG(SECTION_NAME, the_prog, min_kver) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, min_kver, KVER_INF, \
+ BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
+ "", "netd_readonly/", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
+DEFINE_NETD_RO_BPF_PROG("bind4/block_port", bind4_block_port, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return block_port(ctx);
}
-DEFINE_BPF_PROG_KVER("bind6/block_port", AID_ROOT, AID_SYSTEM,
- bind6_block_port, KVER(5, 4, 0))
+DEFINE_NETD_RO_BPF_PROG("bind6/block_port", bind6_block_port, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return block_port(ctx);
}
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf_progs/bpf_net_helpers.h
index bb5e330..f3c7de5 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf_progs/bpf_net_helpers.h
@@ -87,36 +87,18 @@
if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
}
-// constants for passing in to 'bool shared' (for maps)
-static const bool PRIVATE = false;
-static const bool SHARED = true;
+struct egress_bool { bool egress; };
+#define INGRESS ((struct egress_bool){ .egress = false })
+#define EGRESS ((struct egress_bool){ .egress = true })
-// constants for passing in to 'bool optional' (for programs)
-static const bool MANDATORY = false;
-static const bool OPTIONAL = true;
+struct stream_bool { bool down; };
+#define UPSTREAM ((struct stream_bool){ .down = false })
+#define DOWNSTREAM ((struct stream_bool){ .down = true })
-// constants for passing in to 'bool egress'
-static const bool INGRESS = false;
-static const bool EGRESS = true;
+struct rawip_bool { bool rawip; };
+#define ETHER ((struct rawip_bool){ .rawip = false })
+#define RAWIP ((struct rawip_bool){ .rawip = true })
-// constants for passing in to 'bool downstream'
-static const bool UPSTREAM = false;
-static const bool DOWNSTREAM = true;
-
-// constants for passing in to 'bool is_ethernet'
-static const bool RAWIP = false;
-static const bool ETHER = true;
-
-// constants for passing in to 'bool updatetime'
-static const bool NO_UPDATETIME = false;
-static const bool UPDATETIME = true;
-
-// constants for passing in to ignore_on_eng / ignore_on_user / ignore_on_userdebug
-static const bool LOAD_ON_ENG = false;
-static const bool LOAD_ON_USER = false;
-static const bool LOAD_ON_USERDEBUG = false;
-static const bool IGNORE_ON_ENG = true;
-static const bool IGNORE_ON_USER = true;
-static const bool IGNORE_ON_USERDEBUG = true;
-
-#define KVER_4_14 KVER(4, 14, 0)
+struct updatetime_bool { bool updatetime; };
+#define NO_UPDATETIME ((struct updatetime_bool){ .updatetime = false })
+#define UPDATETIME ((struct updatetime_bool){ .updatetime = true })
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index 8f0ff84..addb02f 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -55,8 +55,10 @@
DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
static inline __always_inline int nat64(struct __sk_buff* skb,
- const bool is_ethernet,
- const unsigned kver) {
+ const struct rawip_bool rawip,
+ const struct kver_uint kver) {
+ const bool is_ethernet = !rawip.rawip;
+
// Require ethernet dst mac address to be our unicast address.
if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
@@ -115,7 +117,7 @@
if (proto == IPPROTO_FRAGMENT) {
// Fragment handling requires bpf_skb_adjust_room which is 4.14+
- if (kver < KVER_4_14) return TC_ACT_PIPE;
+ if (!KVER_IS_AT_LEAST(kver, 4, 14, 0)) return TC_ACT_PIPE;
// Must have (ethernet and) ipv6 header and ipv6 fragment extension header
if (data + l2_header_size + sizeof(*ip6) + sizeof(struct frag_hdr) > data_end)
@@ -233,7 +235,7 @@
//
// Note: we currently have no TreeHugger coverage for 4.9-T devices (there are no such
// Pixel or cuttlefish devices), so likely you won't notice for months if this breaks...
- if (kver >= KVER_4_14 && frag_off != htons(IP_DF)) {
+ if (KVER_IS_AT_LEAST(kver, 4, 14, 0) && frag_off != htons(IP_DF)) {
// If we're converting an IPv6 Fragment, we need to trim off 8 more bytes
// We're beyond recovery on error here... but hard to imagine how this could fail.
if (bpf_skb_adjust_room(skb, -(__s32)sizeof(struct frag_hdr), BPF_ADJ_ROOM_NET, /*flags*/0))
diff --git a/bpf_progs/dscpPolicy.c b/bpf_progs/dscpPolicy.c
index 88b50b5..e845a69 100644
--- a/bpf_progs/dscpPolicy.c
+++ b/bpf_progs/dscpPolicy.c
@@ -222,7 +222,7 @@
}
DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM, schedcls_set_dscp_ether,
- KVER(5, 15, 0))
+ KVER_5_15)
(struct __sk_buff* skb) {
if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 7a48e8c..5759df7 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -112,6 +112,9 @@
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
LOAD_ON_USER, LOAD_ON_USERDEBUG);
+DEFINE_BPF_MAP_RO_NETD(data_saver_enabled_map, ARRAY, uint32_t, bool,
+ DATA_SAVER_ENABLED_MAP_SIZE)
+
// iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
// selinux contexts, because even non-xt_bpf iptables mutations are implemented as
// a full table dump, followed by an update in userspace, and then a reload into the kernel,
@@ -142,12 +145,6 @@
BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
"fs_bpf_net_shared", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
-static __always_inline int is_system_uid(uint32_t uid) {
- // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
- // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
- return (uid < AID_APP_START);
-}
-
/*
* Note: this blindly assumes an MTU of 1500, and that packets > MTU are always TCP,
* and that TCP is using the Linux default settings with TCP timestamp option enabled
@@ -178,8 +175,8 @@
#define DEFINE_UPDATE_STATS(the_stats_map, TypeOfKey) \
static __always_inline inline void update_##the_stats_map(const struct __sk_buff* const skb, \
const TypeOfKey* const key, \
- const bool egress, \
- const unsigned kver) { \
+ const struct egress_bool egress, \
+ const struct kver_uint kver) { \
StatsValue* value = bpf_##the_stats_map##_lookup_elem(key); \
if (!value) { \
StatsValue newValue = {}; \
@@ -199,7 +196,7 @@
packets = (payload + mss - 1) / mss; \
bytes = tcp_overhead * packets + payload; \
} \
- if (egress) { \
+ if (egress.egress) { \
__sync_fetch_and_add(&value->txPackets, packets); \
__sync_fetch_and_add(&value->txBytes, bytes); \
} else { \
@@ -219,7 +216,7 @@
const int L3_off,
void* const to,
const int len,
- const unsigned kver) {
+ const struct kver_uint kver) {
// 'kver' (here and throughout) is the compile time guaranteed minimum kernel version,
// ie. we're building (a version of) the bpf program for kver (or newer!) kernels.
//
@@ -236,16 +233,16 @@
//
// For similar reasons this will fail with non-offloaded VLAN tags on < 4.19 kernels,
// since those extend the ethernet header from 14 to 18 bytes.
- return kver >= KVER(4, 19, 0)
+ return KVER_IS_AT_LEAST(kver, 4, 19, 0)
? bpf_skb_load_bytes_relative(skb, L3_off, to, len, BPF_HDR_START_NET)
: bpf_skb_load_bytes(skb, L3_off, to, len);
}
static __always_inline inline void do_packet_tracing(
- const struct __sk_buff* const skb, const bool egress, const uint32_t uid,
- const uint32_t tag, const bool enable_tracing, const unsigned kver) {
+ const struct __sk_buff* const skb, const struct egress_bool egress, const uint32_t uid,
+ const uint32_t tag, const bool enable_tracing, const struct kver_uint kver) {
if (!enable_tracing) return;
- if (kver < KVER(5, 8, 0)) return;
+ if (!KVER_IS_AT_LEAST(kver, 5, 8, 0)) return;
uint32_t mapKey = 0;
bool* traceConfig = bpf_packet_trace_enabled_map_lookup_elem(&mapKey);
@@ -317,8 +314,8 @@
pkt->sport = sport;
pkt->dport = dport;
- pkt->egress = egress;
- pkt->wakeup = !egress && (skb->mark & 0x80000000); // Fwmark.ingress_cpu_wakeup
+ pkt->egress = egress.egress;
+ pkt->wakeup = !egress.egress && (skb->mark & 0x80000000); // Fwmark.ingress_cpu_wakeup
pkt->ipProto = proto;
pkt->tcpFlags = flags;
pkt->ipVersion = ipVersion;
@@ -326,8 +323,9 @@
bpf_packet_trace_ringbuf_submit(pkt);
}
-static __always_inline inline bool skip_owner_match(struct __sk_buff* skb, bool egress,
- const unsigned kver) {
+static __always_inline inline bool skip_owner_match(struct __sk_buff* skb,
+ const struct egress_bool egress,
+ const struct kver_uint kver) {
uint32_t flag = 0;
if (skb->protocol == htons(ETH_P_IP)) {
uint8_t proto;
@@ -358,7 +356,7 @@
return false;
}
// Always allow RST's, and additionally allow ingress FINs
- return flag & (TCP_FLAG_RST | (egress ? 0 : TCP_FLAG_FIN)); // false on read failure
+ return flag & (TCP_FLAG_RST | (egress.egress ? 0 : TCP_FLAG_FIN)); // false on read failure
}
static __always_inline inline BpfConfig getConfig(uint32_t configKey) {
@@ -372,11 +370,11 @@
}
static __always_inline inline bool ingress_should_discard(struct __sk_buff* skb,
- const unsigned kver) {
+ const struct kver_uint kver) {
// Require 4.19, since earlier kernels don't have bpf_skb_load_bytes_relative() which
// provides relative to L3 header reads. Without that we could fetch the wrong bytes.
// Additionally earlier bpf verifiers are much harder to please.
- if (kver < KVER(4, 19, 0)) return false;
+ if (!KVER_IS_AT_LEAST(kver, 4, 19, 0)) return false;
IngressDiscardKey k = {};
if (skb->protocol == htons(ETH_P_IP)) {
@@ -401,7 +399,8 @@
}
static __always_inline inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid,
- bool egress, const unsigned kver) {
+ const struct egress_bool egress,
+ const struct kver_uint kver) {
if (is_system_uid(uid)) return PASS;
if (skip_owner_match(skb, egress, kver)) return PASS;
@@ -414,7 +413,7 @@
if (isBlockedByUidRules(enabledRules, uidRules)) return DROP;
- if (!egress && skb->ifindex != 1) {
+ if (!egress.egress && skb->ifindex != 1) {
if (ingress_should_discard(skb, kver)) return DROP;
if (uidRules & IIF_MATCH) {
if (allowed_iif && skb->ifindex != allowed_iif) {
@@ -434,8 +433,8 @@
static __always_inline inline void update_stats_with_config(const uint32_t selectedMap,
const struct __sk_buff* const skb,
const StatsKey* const key,
- const bool egress,
- const unsigned kver) {
+ const struct egress_bool egress,
+ const struct kver_uint kver) {
if (selectedMap == SELECT_MAP_A) {
update_stats_map_A(skb, key, egress, kver);
} else {
@@ -443,9 +442,10 @@
}
}
-static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb, bool egress,
+static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb,
+ const struct egress_bool egress,
const bool enable_tracing,
- const unsigned kver) {
+ const struct kver_uint kver) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
uint64_t cookie = bpf_get_socket_cookie(skb);
UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
@@ -462,7 +462,7 @@
// interface is accounted for and subject to usage restrictions.
// CLAT IPv6 TX sockets are *always* tagged with CLAT uid, see tagSocketAsClat()
// CLAT daemon receives via an untagged AF_PACKET socket.
- if (egress && uid == AID_CLAT) return PASS;
+ if (egress.egress && uid == AID_CLAT) return PASS;
int match = bpf_owner_match(skb, sock_uid, egress, kver);
@@ -478,7 +478,7 @@
}
// If an outbound packet is going to be dropped, we do not count that traffic.
- if (egress && (match == DROP)) return DROP;
+ if (egress.egress && (match == DROP)) return DROP;
StatsKey key = {.uid = uid, .tag = tag, .counterSet = 0, .ifaceIndex = skb->ifindex};
@@ -505,64 +505,64 @@
// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace_user", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_trace_user, KVER(5, 8, 0), KVER_INF,
+ bpf_cgroup_ingress_trace_user, KVER_5_8, KVER_INF,
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
"fs_bpf_netd_readonly", "",
IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER(5, 8, 0));
+ return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
}
// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_trace, KVER(5, 8, 0), KVER_INF,
+ bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, MANDATORY,
"fs_bpf_netd_readonly", "",
LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER(5, 8, 0));
+ return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_4_19, KVER(4, 19, 0), KVER_INF)
+ bpf_cgroup_ingress_4_19, KVER_4_19, KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER(4, 19, 0));
+ return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_4_19);
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_14", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_4_14, KVER_NONE, KVER(4, 19, 0))
+ bpf_cgroup_ingress_4_14, KVER_NONE, KVER_4_19)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
}
// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace_user", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_trace_user, KVER(5, 8, 0), KVER_INF,
+ bpf_cgroup_egress_trace_user, KVER_5_8, KVER_INF,
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
"fs_bpf_netd_readonly", "",
LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER(5, 8, 0));
+ return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
}
// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_trace, KVER(5, 8, 0), KVER_INF,
+ bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, MANDATORY,
"fs_bpf_netd_readonly", "",
LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER(5, 8, 0));
+ return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_4_19, KVER(4, 19, 0), KVER_INF)
+ bpf_cgroup_egress_4_19, KVER_4_19, KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER(4, 19, 0));
+ return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_4_19);
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_14", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_4_14, KVER_NONE, KVER(4, 19, 0))
+ bpf_cgroup_egress_4_14, KVER_NONE, KVER_4_19)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_NONE);
}
@@ -637,9 +637,7 @@
return BPF_NOMATCH;
}
-DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
- KVER(4, 14, 0))
-(struct bpf_sock* sk) {
+static __always_inline inline uint8_t get_app_permissions() {
uint64_t gid_uid = bpf_get_current_uid_gid();
/*
* A given app is guaranteed to have the same app ID in all the profiles in
@@ -649,13 +647,15 @@
*/
uint32_t appId = (gid_uid & 0xffffffff) % AID_USER_OFFSET; // == PER_USER_RANGE == 100000
uint8_t* permissions = bpf_uid_permission_map_lookup_elem(&appId);
- if (!permissions) {
- // UID not in map. Default to just INTERNET permission.
- return 1;
- }
+ // if UID not in map, then default to just INTERNET permission.
+ return permissions ? *permissions : BPF_PERMISSION_INTERNET;
+}
+DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
+ KVER_4_14)
+(struct bpf_sock* sk) {
// A return value of 1 means allow, everything else means deny.
- return (*permissions & BPF_PERMISSION_INTERNET) == BPF_PERMISSION_INTERNET;
+ return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? 1 : 0;
}
LICENSE("Apache 2.0");
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index dd27bf9..f960dcf 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -16,6 +16,7 @@
#pragma once
+#include <cutils/android_filesystem_config.h>
#include <linux/if.h>
#include <linux/if_ether.h>
#include <linux/in.h>
@@ -125,6 +126,7 @@
static const int UID_OWNER_MAP_SIZE = 4000;
static const int INGRESS_DISCARD_MAP_SIZE = 100;
static const int PACKET_TRACE_BUF_SIZE = 32 * 1024;
+static const int DATA_SAVER_ENABLED_MAP_SIZE = 1;
#ifdef __cplusplus
@@ -171,6 +173,7 @@
#define INGRESS_DISCARD_MAP_PATH BPF_NETD_PATH "map_netd_ingress_discard_map"
#define PACKET_TRACE_RINGBUF_PATH BPF_NETD_PATH "map_netd_packet_trace_ringbuf"
#define PACKET_TRACE_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_packet_trace_enabled_map"
+#define DATA_SAVER_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_data_saver_enabled_map"
#endif // __cplusplus
@@ -190,7 +193,7 @@
OEM_DENY_2_MATCH = (1 << 10),
OEM_DENY_3_MATCH = (1 << 11),
};
-// LINT.ThenChange(packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java)
+// LINT.ThenChange(../framework/src/android/net/BpfNetMapsConstants.java)
enum BpfPermissionMatch {
BPF_PERMISSION_INTERNET = 1 << 2,
@@ -249,3 +252,9 @@
static inline bool isBlockedByUidRules(BpfConfig enabledRules, uint32_t uidRules) {
return enabledRules & (DROP_IF_SET | DROP_IF_UNSET) & (uidRules ^ DROP_IF_UNSET);
}
+
+static inline bool is_system_uid(uint32_t uid) {
+ // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
+ // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
+ return (uid < AID_APP_START);
+}
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index c752779..90f96a1 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -124,8 +124,12 @@
DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
TETHERING_GID)
-static inline __always_inline int do_forward6(struct __sk_buff* skb, const bool is_ethernet,
- const bool downstream, const unsigned kver) {
+static inline __always_inline int do_forward6(struct __sk_buff* skb,
+ const struct rawip_bool rawip,
+ const struct stream_bool stream,
+ const struct kver_uint kver) {
+ const bool is_ethernet = !rawip.rawip;
+
// Must be meta-ethernet IPv6 frame
if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_PIPE;
@@ -184,7 +188,7 @@
TC_PUNT(NON_GLOBAL_DST);
// In the upstream direction do not forward traffic within the same /64 subnet.
- if (!downstream && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1]))
+ if (!stream.down && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1]))
TC_PUNT(LOCAL_SRC_DST);
TetherDownstream6Key kd = {
@@ -194,17 +198,18 @@
TetherUpstream6Key ku = {
.iif = skb->ifindex,
- .src64 = 0,
+ // Retrieve the first 64 bits of the source IPv6 address in network order
+ .src64 = *(uint64_t*)&(ip6->saddr.s6_addr32[0]),
};
- if (is_ethernet) __builtin_memcpy(downstream ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
+ if (is_ethernet) __builtin_memcpy(stream.down ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
- Tether6Value* v = downstream ? bpf_tether_downstream6_map_lookup_elem(&kd)
- : bpf_tether_upstream6_map_lookup_elem(&ku);
+ Tether6Value* v = stream.down ? bpf_tether_downstream6_map_lookup_elem(&kd)
+ : bpf_tether_upstream6_map_lookup_elem(&ku);
// If we don't find any offload information then simply let the core stack handle it...
if (!v) return TC_ACT_PIPE;
- uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+ uint32_t stat_and_limit_k = stream.down ? skb->ifindex : v->oif;
TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
@@ -249,7 +254,7 @@
// We do this even if TX interface is RAWIP and thus does not need an ethernet header,
// because this is easier and the kernel will strip extraneous ethernet header.
if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
- __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
TC_PUNT(CHANGE_HEAD_FAILED);
}
@@ -261,7 +266,7 @@
// I do not believe this can ever happen, but keep the verifier happy...
if (data + sizeof(struct ethhdr) + sizeof(*ip6) > data_end) {
- __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
TC_DROP(TOO_SHORT);
}
};
@@ -281,8 +286,8 @@
// (-ENOTSUPP) if it isn't.
bpf_csum_update(skb, 0xFFFF - ntohs(old_hl) + ntohs(new_hl));
- __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
- __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
// Overwrite any mac header with the new one
// For a rawip tx interface it will simply be a bunch of zeroes and later stripped.
@@ -324,26 +329,26 @@
//
// Hence, these mandatory (must load successfully) implementations for 4.14+ kernels:
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream6_rawip_4_14, KVER(4, 14, 0))
+ sched_cls_tether_downstream6_rawip_4_14, KVER_4_14)
(struct __sk_buff* skb) {
- return do_forward6(skb, RAWIP, DOWNSTREAM, KVER(4, 14, 0));
+ return do_forward6(skb, RAWIP, DOWNSTREAM, KVER_4_14);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream6_rawip_4_14, KVER(4, 14, 0))
+ sched_cls_tether_upstream6_rawip_4_14, KVER_4_14)
(struct __sk_buff* skb) {
- return do_forward6(skb, RAWIP, UPSTREAM, KVER(4, 14, 0));
+ return do_forward6(skb, RAWIP, UPSTREAM, KVER_4_14);
}
// and define no-op stubs for pre-4.14 kernels.
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER(4, 14, 0))
+ sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER_4_14)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER(4, 14, 0))
+ sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER_4_14)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
@@ -356,9 +361,10 @@
static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
const int l2_header_size, void* data, const void* data_end,
- struct ethhdr* eth, struct iphdr* ip, const bool is_ethernet,
- const bool downstream, const bool updatetime, const bool is_tcp,
- const unsigned kver) {
+ struct ethhdr* eth, struct iphdr* ip, const struct rawip_bool rawip,
+ const struct stream_bool stream, const struct updatetime_bool updatetime,
+ const bool is_tcp, const struct kver_uint kver) {
+ const bool is_ethernet = !rawip.rawip;
struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
@@ -416,13 +422,13 @@
};
if (is_ethernet) __builtin_memcpy(k.dstMac, eth->h_dest, ETH_ALEN);
- Tether4Value* v = downstream ? bpf_tether_downstream4_map_lookup_elem(&k)
- : bpf_tether_upstream4_map_lookup_elem(&k);
+ Tether4Value* v = stream.down ? bpf_tether_downstream4_map_lookup_elem(&k)
+ : bpf_tether_upstream4_map_lookup_elem(&k);
// If we don't find any offload information then simply let the core stack handle it...
if (!v) return TC_ACT_PIPE;
- uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+ uint32_t stat_and_limit_k = stream.down ? skb->ifindex : v->oif;
TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
@@ -467,7 +473,7 @@
// We do this even if TX interface is RAWIP and thus does not need an ethernet header,
// because this is easier and the kernel will strip extraneous ethernet header.
if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
- __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
TC_PUNT(CHANGE_HEAD_FAILED);
}
@@ -481,7 +487,7 @@
// I do not believe this can ever happen, but keep the verifier happy...
if (data + sizeof(struct ethhdr) + sizeof(*ip) + (is_tcp ? sizeof(*tcph) : sizeof(*udph)) > data_end) {
- __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
TC_DROP(TOO_SHORT);
}
};
@@ -533,10 +539,10 @@
// This requires the bpf_ktime_get_boot_ns() helper which was added in 5.8,
// and backported to all Android Common Kernel 4.14+ trees.
- if (updatetime) v->last_used = bpf_ktime_get_boot_ns();
+ if (updatetime.updatetime) v->last_used = bpf_ktime_get_boot_ns();
- __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
- __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
// Redirect to forwarded interface.
//
@@ -547,8 +553,13 @@
return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
}
-static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
- const bool downstream, const bool updatetime, const unsigned kver) {
+static inline __always_inline int do_forward4(struct __sk_buff* skb,
+ const struct rawip_bool rawip,
+ const struct stream_bool stream,
+ const struct updatetime_bool updatetime,
+ const struct kver_uint kver) {
+ const bool is_ethernet = !rawip.rawip;
+
// Require ethernet dst mac address to be our unicast address.
if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
@@ -606,16 +617,16 @@
// in such a situation we can only support TCP. This also has the added nice benefit of
// using a separate error counter, and thus making it obvious which version of the program
// is loaded.
- if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
+ if (!updatetime.updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
// We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
// but no need to check this if !updatetime due to check immediately above.
- if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
+ if (updatetime.updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
TC_PUNT(NON_TCP_UDP);
// We want to make sure that the compiler will, in the !updatetime case, entirely optimize
// out all the non-tcp logic. Also note that at this point is_udp === !is_tcp.
- const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
+ const bool is_tcp = !updatetime.updatetime || (ip->protocol == IPPROTO_TCP);
// This is a bit of a hack to make things easier on the bpf verifier.
// (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
@@ -636,37 +647,37 @@
// if the underlying requisite kernel support (bpf_ktime_get_boot_ns) was backported.
if (is_tcp) {
return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
- is_ethernet, downstream, updatetime, /* is_tcp */ true, kver);
+ rawip, stream, updatetime, /* is_tcp */ true, kver);
} else {
return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
- is_ethernet, downstream, updatetime, /* is_tcp */ false, kver);
+ rawip, stream, updatetime, /* is_tcp */ false, kver);
}
}
// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_rawip_5_8, KVER(5, 8, 0))
+ sched_cls_tether_downstream4_rawip_5_8, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER(5, 8, 0));
+ return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_rawip_5_8, KVER(5, 8, 0))
+ sched_cls_tether_upstream4_rawip_5_8, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER(5, 8, 0));
+ return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_ether_5_8, KVER(5, 8, 0))
+ sched_cls_tether_downstream4_ether_5_8, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER(5, 8, 0));
+ return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_ether_5_8, KVER(5, 8, 0))
+ sched_cls_tether_upstream4_ether_5_8, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER(5, 8, 0));
+ return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
}
// Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels
@@ -675,33 +686,33 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_downstream4_rawip_opt,
- KVER(4, 14, 0), KVER(5, 8, 0))
+ KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_4_14);
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_upstream4_rawip_opt,
- KVER(4, 14, 0), KVER(5, 8, 0))
+ KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_4_14);
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_downstream4_ether_opt,
- KVER(4, 14, 0), KVER(5, 8, 0))
+ KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_4_14);
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_upstream4_ether_opt,
- KVER(4, 14, 0), KVER(5, 8, 0))
+ KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_4_14);
}
// Partial (TCP-only: will not update 'last_used' field) implementations for 4.14+ kernels.
@@ -719,15 +730,15 @@
// RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+ sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER(5, 4, 0));
+ return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_5_4);
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+ sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER(5, 4, 0));
+ return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
}
// RAWIP: Optional for 4.14/4.19 (R) kernels -- which support bpf_skb_change_head().
@@ -736,31 +747,31 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_downstream4_rawip_4_14,
- KVER(4, 14, 0), KVER(5, 4, 0))
+ KVER_4_14, KVER_5_4)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_upstream4_rawip_4_14,
- KVER(4, 14, 0), KVER(5, 4, 0))
+ KVER_4_14, KVER_5_4)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_4_14);
}
// ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+ sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+ sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
}
// Placeholder (no-op) implementations for older Q kernels
@@ -768,13 +779,13 @@
// RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+ sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER_5_4)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+ sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER_5_4)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
@@ -782,13 +793,13 @@
// ETHER: 4.9-P/Q kernel
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
+ sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER_4_14)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
+ sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER_4_14)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
@@ -797,17 +808,18 @@
DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, TETHERING_GID)
-static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const bool is_ethernet,
- const bool downstream) {
+static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const struct rawip_bool rawip,
+ const struct stream_bool stream) {
return XDP_PASS;
}
-static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const bool is_ethernet,
- const bool downstream) {
+static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const struct rawip_bool rawip,
+ const struct stream_bool stream) {
return XDP_PASS;
}
-static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx, const bool downstream) {
+static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx,
+ const struct stream_bool stream) {
const void* data = (void*)(long)ctx->data;
const void* data_end = (void*)(long)ctx->data_end;
const struct ethhdr* eth = data;
@@ -816,15 +828,16 @@
if ((void*)(eth + 1) > data_end) return XDP_PASS;
if (eth->h_proto == htons(ETH_P_IPV6))
- return do_xdp_forward6(ctx, ETHER, downstream);
+ return do_xdp_forward6(ctx, ETHER, stream);
if (eth->h_proto == htons(ETH_P_IP))
- return do_xdp_forward4(ctx, ETHER, downstream);
+ return do_xdp_forward4(ctx, ETHER, stream);
// Anything else we don't know how to handle...
return XDP_PASS;
}
-static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx, const bool downstream) {
+static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx,
+ const struct stream_bool stream) {
const void* data = (void*)(long)ctx->data;
const void* data_end = (void*)(long)ctx->data_end;
@@ -832,15 +845,15 @@
if (data_end - data < 1) return XDP_PASS;
const uint8_t v = (*(uint8_t*)data) >> 4;
- if (v == 6) return do_xdp_forward6(ctx, RAWIP, downstream);
- if (v == 4) return do_xdp_forward4(ctx, RAWIP, downstream);
+ if (v == 6) return do_xdp_forward6(ctx, RAWIP, stream);
+ if (v == 4) return do_xdp_forward4(ctx, RAWIP, stream);
// Anything else we don't know how to handle...
return XDP_PASS;
}
#define DEFINE_XDP_PROG(str, func) \
- DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER(5, 9, 0))(struct xdp_md *ctx)
+ DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER_5_9)(struct xdp_md *ctx)
DEFINE_XDP_PROG("xdp/tether_downstream_ether",
xdp_tether_downstream_ether) {
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index 68469c8..70b08b7 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -49,7 +49,7 @@
DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, TETHERING_GID)
DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", TETHERING_UID, TETHERING_GID,
- xdp_test, KVER(5, 9, 0))
+ xdp_test, KVER_5_9)
(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
diff --git a/common/Android.bp b/common/Android.bp
index e10ede8..1d73a46 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -19,10 +19,13 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+build = ["TrunkStable.bp"]
+
// This is a placeholder comment to avoid merge conflicts
// as the above target may not exist
// depending on the branch
+// The library requires the final artifact to contain net-utils-device-common-struct.
java_library {
name: "connectivity-net-module-utils-bpf",
srcs: [
@@ -38,8 +41,9 @@
libs: [
"androidx.annotation_annotation",
"framework-connectivity.stubs.module_lib",
- ],
- static_libs: [
+ // For libraries which are statically linked in framework-connectivity, do not
+ // statically link here because callers of this library might already have a static
+ // version linked.
"net-utils-device-common-struct",
],
apex_available: [
diff --git a/common/TrunkStable.bp b/common/TrunkStable.bp
new file mode 100644
index 0000000..56938fc
--- /dev/null
+++ b/common/TrunkStable.bp
@@ -0,0 +1,26 @@
+//
+// Copyright (C) 2023 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.
+//
+
+aconfig_declarations {
+ name: "com.android.net.flags-aconfig",
+ package: "com.android.net.flags",
+ srcs: ["flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "connectivity_flags_aconfig_lib",
+ aconfig_declarations: "com.android.net.flags-aconfig",
+}
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 4926503..7235202 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -6,3 +6,24 @@
description: "NetworkActivityTracker tracks multiple networks including non default networks"
bug: "267870186"
}
+
+flag {
+ name: "forbidden_capability"
+ namespace: "android_core_networking"
+ description: "This flag controls the forbidden capability API"
+ bug: "302997505"
+}
+
+flag {
+ name: "nsd_expired_services_removal"
+ namespace: "android_core_networking"
+ description: "Remove expired services from MdnsServiceCache"
+ bug: "304649384"
+}
+
+flag {
+ name: "set_data_saver_via_cm"
+ namespace: "android_core_networking"
+ description: "Set data saver through ConnectivityManager API"
+ bug: "297836825"
+}
diff --git a/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java b/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java
new file mode 100644
index 0000000..eabcf3c
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 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.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+
+import java.net.Inet6Address;
+
+/** Key type for ingress discard map */
+public class IngressDiscardKey extends Struct {
+ // The destination ip of the incoming packet. IPv4 uses IPv4-mapped IPv6 address.
+ @Field(order = 0, type = Type.Ipv6Address)
+ public final Inet6Address dstAddr;
+
+ public IngressDiscardKey(final Inet6Address dstAddr) {
+ this.dstAddr = dstAddr;
+ }
+}
diff --git a/common/src/com/android/net/module/util/bpf/IngressDiscardValue.java b/common/src/com/android/net/module/util/bpf/IngressDiscardValue.java
new file mode 100644
index 0000000..7df3620
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/IngressDiscardValue.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 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.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+
+/** Value type for ingress discard map */
+public class IngressDiscardValue extends Struct {
+ // Allowed interface indexes.
+ // Use the same value for iif1 and iif2 if there is only a single allowed interface index.
+ @Field(order = 0, type = Type.S32)
+ public final int iif1;
+ @Field(order = 1, type = Type.S32)
+ public final int iif2;
+
+ public IngressDiscardValue(final int iif1, final int iif2) {
+ this.iif1 = iif1;
+ this.iif2 = iif2;
+ }
+}
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 53ad834..23510e1 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -305,6 +305,7 @@
ctor public NetworkStats(long, int);
method @NonNull public android.net.NetworkStats add(@NonNull android.net.NetworkStats);
method @NonNull public android.net.NetworkStats addEntry(@NonNull android.net.NetworkStats.Entry);
+ method public android.net.NetworkStats clone();
method public int describeContents();
method @NonNull public java.util.Iterator<android.net.NetworkStats.Entry> iterator();
method @NonNull public android.net.NetworkStats subtract(@NonNull android.net.NetworkStats);
@@ -416,12 +417,87 @@
package android.net.thread {
- public class ThreadNetworkController {
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ActiveOperationalDataset implements android.os.Parcelable {
+ method @NonNull public static android.net.thread.ActiveOperationalDataset createRandomDataset();
+ method public int describeContents();
+ method @NonNull public static android.net.thread.ActiveOperationalDataset fromThreadTlvs(@NonNull byte[]);
+ method @NonNull public android.net.thread.OperationalDatasetTimestamp getActiveTimestamp();
+ method @IntRange(from=0, to=65535) public int getChannel();
+ method @NonNull @Size(min=1) public android.util.SparseArray<byte[]> getChannelMask();
+ method @IntRange(from=0, to=255) public int getChannelPage();
+ method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) public byte[] getExtendedPanId();
+ method @NonNull public android.net.IpPrefix getMeshLocalPrefix();
+ method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) public byte[] getNetworkKey();
+ method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) public String getNetworkName();
+ method @IntRange(from=0, to=65534) public int getPanId();
+ method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) public byte[] getPskc();
+ method @NonNull public android.net.thread.ActiveOperationalDataset.SecurityPolicy getSecurityPolicy();
+ method @NonNull public byte[] toThreadTlvs();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field public static final int CHANNEL_MAX_24_GHZ = 26; // 0x1a
+ field public static final int CHANNEL_MIN_24_GHZ = 11; // 0xb
+ field public static final int CHANNEL_PAGE_24_GHZ = 0; // 0x0
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ActiveOperationalDataset> CREATOR;
+ field public static final int LENGTH_EXTENDED_PAN_ID = 8; // 0x8
+ field public static final int LENGTH_MAX_DATASET_TLVS = 254; // 0xfe
+ field public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16; // 0x10
+ field public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64; // 0x40
+ field public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1; // 0x1
+ field public static final int LENGTH_NETWORK_KEY = 16; // 0x10
+ field public static final int LENGTH_PSKC = 16; // 0x10
+ }
+
+ public static final class ActiveOperationalDataset.Builder {
+ ctor public ActiveOperationalDataset.Builder(@NonNull android.net.thread.ActiveOperationalDataset);
+ ctor public ActiveOperationalDataset.Builder();
+ method @NonNull public android.net.thread.ActiveOperationalDataset build();
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setActiveTimestamp(@NonNull android.net.thread.OperationalDatasetTimestamp);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannel(@IntRange(from=0, to=255) int, @IntRange(from=0, to=65535) int);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannelMask(@NonNull @Size(min=1) android.util.SparseArray<byte[]>);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setExtendedPanId(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) byte[]);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setMeshLocalPrefix(@NonNull android.net.IpPrefix);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkKey(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) byte[]);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkName(@NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) String);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPanId(@IntRange(from=0, to=65534) int);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPskc(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) byte[]);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setSecurityPolicy(@NonNull android.net.thread.ActiveOperationalDataset.SecurityPolicy);
+ }
+
+ public static final class ActiveOperationalDataset.SecurityPolicy {
+ ctor public ActiveOperationalDataset.SecurityPolicy(@IntRange(from=1, to=65535) int, @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[]);
+ method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) public byte[] getFlags();
+ method @IntRange(from=1, to=65535) public int getRotationTimeHours();
+ field public static final int DEFAULT_ROTATION_TIME_HOURS = 672; // 0x2a0
+ field public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1; // 0x1
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class OperationalDatasetTimestamp {
+ ctor public OperationalDatasetTimestamp(@IntRange(from=0, to=281474976710655L) long, @IntRange(from=0, to=32767) int, boolean);
+ method @NonNull public static android.net.thread.OperationalDatasetTimestamp fromInstant(@NonNull java.time.Instant);
+ method @IntRange(from=0, to=281474976710655L) public long getSeconds();
+ method @IntRange(from=0, to=32767) public int getTicks();
+ method public boolean isAuthoritativeSource();
+ method @NonNull public java.time.Instant toInstant();
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class PendingOperationalDataset implements android.os.Parcelable {
+ ctor public PendingOperationalDataset(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull android.net.thread.OperationalDatasetTimestamp, @NonNull java.time.Duration);
+ method public int describeContents();
+ method @NonNull public static android.net.thread.PendingOperationalDataset fromThreadTlvs(@NonNull byte[]);
+ method @NonNull public android.net.thread.ActiveOperationalDataset getActiveOperationalDataset();
+ method @NonNull public java.time.Duration getDelayTimer();
+ method @NonNull public android.net.thread.OperationalDatasetTimestamp getPendingTimestamp();
+ method @NonNull public byte[] toThreadTlvs();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.PendingOperationalDataset> CREATOR;
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
method public int getThreadVersion();
field public static final int THREAD_VERSION_1_3 = 4; // 0x4
}
- public class ThreadNetworkManager {
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkManager {
method @NonNull public java.util.List<android.net.thread.ThreadNetworkController> getAllThreadNetworkControllers();
}
diff --git a/framework-t/src/android/net/EthernetNetworkSpecifier.java b/framework-t/src/android/net/EthernetNetworkSpecifier.java
index e4d6e24..90c0361 100644
--- a/framework-t/src/android/net/EthernetNetworkSpecifier.java
+++ b/framework-t/src/android/net/EthernetNetworkSpecifier.java
@@ -26,8 +26,6 @@
/**
* A {@link NetworkSpecifier} used to identify ethernet interfaces.
- *
- * @see EthernetManager
*/
public final class EthernetNetworkSpecifier extends NetworkSpecifier implements Parcelable {
diff --git a/framework/Android.bp b/framework/Android.bp
index 182c558..103083f 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -101,7 +101,7 @@
"framework-connectivity-javastream-protos",
],
impl_only_static_libs: [
- "net-utils-device-common-struct",
+ "net-utils-device-common-bpf",
],
libs: [
"androidx.annotation_annotation",
@@ -130,7 +130,7 @@
// to generate the SDK stubs.
// Even if the library is included in "impl_only_static_libs" of defaults. This is still
// needed because java_library which doesn't understand "impl_only_static_libs".
- "net-utils-device-common-struct",
+ "net-utils-device-common-bpf",
],
libs: [
// This cannot be in the defaults clause above because if it were, it would be used
@@ -292,17 +292,20 @@
// Library providing limited APIs within the connectivity module, so that R+ components like
// Tethering have a controlled way to depend on newer components like framework-connectivity that
// are not loaded on R.
+// Note that this target needs to have access to hidden classes, and as such needs to list
+// the full libraries instead of the .impl lib (which only expose API classes).
java_library {
name: "connectivity-internal-api-util",
sdk_version: "module_current",
libs: [
"androidx.annotation_annotation",
- "framework-connectivity.impl",
+ "framework-connectivity-pre-jarjar",
],
jarjar_rules: ":framework-connectivity-jarjar-rules",
srcs: [
- // Files listed here MUST all be annotated with @RequiresApi(Build.VERSION_CODES.TIRAMISU),
- // so that API checks are enforced for R+ users of this library
+ // Files listed here MUST all be annotated with @RequiresApi(Build.VERSION_CODES.S)
+ // or above as appropriate so that API checks are enforced for R+ users of this library
+ "src/android/net/RoutingCoordinatorManager.java",
"src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java",
],
visibility: [
diff --git a/framework/aidl-export/android/net/LocalNetworkConfig.aidl b/framework/aidl-export/android/net/LocalNetworkConfig.aidl
new file mode 100644
index 0000000..e2829a5
--- /dev/null
+++ b/framework/aidl-export/android/net/LocalNetworkConfig.aidl
@@ -0,0 +1,20 @@
+/**
+ *
+ * Copyright (C) 2023 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;
+
+@JavaOnlyStableParcelable parcelable LocalNetworkConfig;
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 193bd92..782e20a 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -24,6 +24,7 @@
method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptPartialConnectivity(@NonNull android.net.Network, boolean, boolean);
method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptUnvalidated(@NonNull android.net.Network, boolean, boolean);
method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAvoidUnvalidated(@NonNull android.net.Network);
+ method @FlaggedApi("com.android.net.flags.set_data_saver_via_cm") @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setDataSaverEnabled(boolean);
method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setFirewallChainEnabled(int, boolean);
method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setGlobalProxy(@Nullable android.net.ProxyInfo);
method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setLegacyLockdownVpnEnabled(boolean);
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 4a2ed8a..e812024 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -94,6 +94,7 @@
}
public final class DscpPolicy implements android.os.Parcelable {
+ method public int describeContents();
method @Nullable public java.net.InetAddress getDestinationAddress();
method @Nullable public android.util.Range<java.lang.Integer> getDestinationPortRange();
method public int getDscpValue();
@@ -101,6 +102,7 @@
method public int getProtocol();
method @Nullable public java.net.InetAddress getSourceAddress();
method public int getSourcePort();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.DscpPolicy> CREATOR;
field public static final int PROTOCOL_ANY = -1; // 0xffffffff
field public static final int SOURCE_PORT_ANY = -1; // 0xffffffff
diff --git a/framework/jarjar-excludes.txt b/framework/jarjar-excludes.txt
index 1ac5e8e..bc3c8d1 100644
--- a/framework/jarjar-excludes.txt
+++ b/framework/jarjar-excludes.txt
@@ -14,6 +14,15 @@
# TODO: move files to android.net.connectivity.visiblefortesting
android\.net\.IConnectivityDiagnosticsCallback(\$.+)?
+# Classes used by tethering as a hidden API are compiled as a lib in target
+# connectivity-internal-api-util. Because it's used by tethering, it can't
+# be jarjared. Classes in android.net.connectivity are exempt from being
+# listed here because they are already in the target package and as such
+# are already not jarjared.
+# Because Tethering can be installed on R without Connectivity, any use
+# of these classes must be protected by a check for >= S SDK.
+# It's unlikely anybody else declares a hidden class with this name ?
+android\.net\.RoutingCoordinatorManager(\$.+)?
# KeepaliveUtils is used by ConnectivityManager CTS
# TODO: move into service-connectivity so framework-connectivity stops using
diff --git a/framework/src/android/net/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java
index 2191682..36848e7 100644
--- a/framework/src/android/net/BpfNetMapsConstants.java
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -43,8 +43,14 @@
"/sys/fs/bpf/netd_shared/map_netd_uid_permission_map";
public static final String COOKIE_TAG_MAP_PATH =
"/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map";
+ public static final String DATA_SAVER_ENABLED_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_data_saver_enabled_map";
public static final Struct.S32 UID_RULES_CONFIGURATION_KEY = new Struct.S32(0);
public static final Struct.S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new Struct.S32(1);
+ public static final Struct.S32 DATA_SAVER_ENABLED_KEY = new Struct.S32(0);
+
+ public static final short DATA_SAVER_DISABLED = 0;
+ public static final short DATA_SAVER_ENABLED = 1;
// LINT.IfChange(match_type)
public static final long NO_MATCH = 0;
@@ -60,7 +66,7 @@
public static final long OEM_DENY_1_MATCH = (1 << 9);
public static final long OEM_DENY_2_MATCH = (1 << 10);
public static final long OEM_DENY_3_MATCH = (1 << 11);
- // LINT.ThenChange(packages/modules/Connectivity/bpf_progs/netd.h)
+ // LINT.ThenChange(../../../../bpf_progs/netd.h)
public static final List<Pair<Long, String>> MATCH_LIST = Arrays.asList(
Pair.create(HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"),
diff --git a/framework/src/android/net/BpfNetMapsReader.java b/framework/src/android/net/BpfNetMapsReader.java
new file mode 100644
index 0000000..49e874a
--- /dev/null
+++ b/framework/src/android/net/BpfNetMapsReader.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2023 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;
+
+import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
+import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
+import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
+import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
+import static android.net.BpfNetMapsUtils.isFirewallAllowList;
+import static android.net.BpfNetMapsUtils.throwIfPreT;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.os.Build;
+import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.S32;
+import com.android.net.module.util.Struct.U32;
+
+/**
+ * A helper class to *read* java BpfMaps.
+ * @hide
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU) // BPF maps were only mainlined in T
+public class BpfNetMapsReader {
+ // Locally store the handle of bpf maps. The FileDescriptors are statically cached inside the
+ // BpfMap implementation.
+
+ // Bpf map to store various networking configurations, the format of the value is different
+ // for different keys. See BpfNetMapsConstants#*_CONFIGURATION_KEY for keys.
+ private final IBpfMap<S32, U32> mConfigurationMap;
+ // Bpf map to store per uid traffic control configurations.
+ // See {@link UidOwnerValue} for more detail.
+ private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap;
+ private final Dependencies mDeps;
+
+ public BpfNetMapsReader() {
+ this(new Dependencies());
+ }
+
+ @VisibleForTesting
+ public BpfNetMapsReader(@NonNull Dependencies deps) {
+ if (!SdkLevel.isAtLeastT()) {
+ throw new UnsupportedOperationException(
+ BpfNetMapsReader.class.getSimpleName() + " is not supported below Android T");
+ }
+ mDeps = deps;
+ mConfigurationMap = mDeps.getConfigurationMap();
+ mUidOwnerMap = mDeps.getUidOwnerMap();
+ }
+
+ /**
+ * Dependencies of BpfNetMapReader, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Get the configuration map. */
+ public IBpfMap<S32, U32> getConfigurationMap() {
+ try {
+ return new BpfMap<>(CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDONLY,
+ S32.class, U32.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open configuration map", e);
+ }
+ }
+
+ /** Get the uid owner map. */
+ public IBpfMap<S32, UidOwnerValue> getUidOwnerMap() {
+ try {
+ return new BpfMap<>(UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDONLY,
+ S32.class, UidOwnerValue.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open uid owner map", e);
+ }
+ }
+ }
+
+ /**
+ * Get the specified firewall chain's status.
+ *
+ * @param chain target chain
+ * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public boolean isChainEnabled(final int chain) {
+ return isChainEnabled(mConfigurationMap, chain);
+ }
+
+ /**
+ * Get firewall rule of specified firewall chain on specified uid.
+ *
+ * @param chain target chain
+ * @param uid target uid
+ * @return either {@link ConnectivityManager#FIREWALL_RULE_ALLOW} or
+ * {@link ConnectivityManager#FIREWALL_RULE_DENY}.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public int getUidRule(final int chain, final int uid) {
+ return getUidRule(mUidOwnerMap, chain, uid);
+ }
+
+ /**
+ * Get the specified firewall chain's status.
+ *
+ * @param configurationMap target configurationMap
+ * @param chain target chain
+ * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public static boolean isChainEnabled(
+ final IBpfMap<Struct.S32, Struct.U32> configurationMap, final int chain) {
+ throwIfPreT("isChainEnabled is not available on pre-T devices");
+
+ final long match = getMatchByFirewallChain(chain);
+ try {
+ final Struct.U32 config = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+ return (config.val & match) != 0;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to get firewall chain status: " + Os.strerror(e.errno));
+ }
+ }
+
+ /**
+ * Get firewall rule of specified firewall chain on specified uid.
+ *
+ * @param uidOwnerMap target uidOwnerMap.
+ * @param chain target chain.
+ * @param uid target uid.
+ * @return either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public static int getUidRule(final IBpfMap<Struct.S32, UidOwnerValue> uidOwnerMap,
+ final int chain, final int uid) {
+ throwIfPreT("getUidRule is not available on pre-T devices");
+
+ final long match = getMatchByFirewallChain(chain);
+ final boolean isAllowList = isFirewallAllowList(chain);
+ try {
+ final UidOwnerValue uidMatch = uidOwnerMap.getValue(new Struct.S32(uid));
+ final boolean isMatchEnabled = uidMatch != null && (uidMatch.rule & match) != 0;
+ return isMatchEnabled == isAllowList ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to get uid rule status: " + Os.strerror(e.errno));
+ }
+ }
+}
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index d464e3d..28d5891 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -39,6 +39,8 @@
import android.os.ServiceSpecificException;
import android.util.Pair;
+import com.android.modules.utils.build.SdkLevel;
+
import java.util.StringJoiner;
/**
@@ -124,4 +126,15 @@
}
return sj.toString();
}
+
+ public static final boolean PRE_T = !SdkLevel.isAtLeastT();
+
+ /**
+ * Throw UnsupportedOperationException if SdkLevel is before T.
+ */
+ public static void throwIfPreT(final String msg) {
+ if (PRE_T) {
+ throw new UnsupportedOperationException(msg);
+ }
+ }
}
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 2315521..32058a4 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -26,9 +26,11 @@
import static android.net.QosCallback.QosCallbackRegistrationException;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresApi;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
@@ -115,6 +117,14 @@
private static final String TAG = "ConnectivityManager";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+ // available here
+ /** @hide */
+ public static class Flags {
+ static final String SET_DATA_SAVER_VIA_CM =
+ "com.android.net.flags.set_data_saver_via_cm";
+ }
+
/**
* A change in network connectivity has occurred. A default connection has either
* been established or lost. The NetworkInfo for the affected network is
@@ -3811,11 +3821,28 @@
@RequiresPermission(anyOf = {
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
android.Manifest.permission.NETWORK_FACTORY})
- public Network registerNetworkAgent(INetworkAgent na, NetworkInfo ni, LinkProperties lp,
- NetworkCapabilities nc, @NonNull NetworkScore score, NetworkAgentConfig config,
- int providerId) {
+ public Network registerNetworkAgent(@NonNull INetworkAgent na, @NonNull NetworkInfo ni,
+ @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+ @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, int providerId) {
+ return registerNetworkAgent(na, ni, lp, nc, null /* localNetworkConfig */, score, config,
+ providerId);
+ }
+
+ /**
+ * @hide
+ * Register a NetworkAgent with ConnectivityService.
+ * @return Network corresponding to NetworkAgent.
+ */
+ @RequiresPermission(anyOf = {
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ android.Manifest.permission.NETWORK_FACTORY})
+ public Network registerNetworkAgent(@NonNull INetworkAgent na, @NonNull NetworkInfo ni,
+ @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+ @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
+ @NonNull NetworkAgentConfig config, int providerId) {
try {
- return mService.registerNetworkAgent(na, ni, lp, nc, score, config, providerId);
+ return mService.registerNetworkAgent(na, ni, lp, nc, score, localNetworkConfig, config,
+ providerId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -5941,6 +5968,28 @@
}
/**
+ * Sets data saver switch.
+ *
+ * @param enable True if enable.
+ * @throws IllegalStateException if failed.
+ * @hide
+ */
+ @FlaggedApi(Flags.SET_DATA_SAVER_VIA_CM)
+ @SystemApi(client = MODULE_LIBRARIES)
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+ })
+ public void setDataSaverEnabled(final boolean enable) {
+ try {
+ mService.setDataSaverEnabled(enable);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Adds the specified UID to the list of UIds that are allowed to use data on metered networks
* even when background data is restricted. The deny list takes precedence over the allow list.
*
@@ -6157,4 +6206,24 @@
throw e.rethrowFromSystemServer();
}
}
+
+ private static final Object sRoutingCoordinatorManagerLock = new Object();
+ @GuardedBy("sRoutingCoordinatorManagerLock")
+ private static RoutingCoordinatorManager sRoutingCoordinatorManager = null;
+ /** @hide */
+ @RequiresApi(Build.VERSION_CODES.S)
+ public RoutingCoordinatorManager getRoutingCoordinatorManager() {
+ try {
+ synchronized (sRoutingCoordinatorManagerLock) {
+ if (null == sRoutingCoordinatorManager) {
+ sRoutingCoordinatorManager = new RoutingCoordinatorManager(mContext,
+ IRoutingCoordinator.Stub.asInterface(
+ mService.getRoutingCoordinatorService()));
+ }
+ return sRoutingCoordinatorManager;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index ebe8bca..d3a02b9 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -27,6 +27,7 @@
import android.net.IQosCallback;
import android.net.ISocketKeepaliveCallback;
import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
import android.net.Network;
import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
@@ -146,7 +147,8 @@
void declareNetworkRequestUnfulfillable(in NetworkRequest request);
Network registerNetworkAgent(in INetworkAgent na, in NetworkInfo ni, in LinkProperties lp,
- in NetworkCapabilities nc, in NetworkScore score, in NetworkAgentConfig config,
+ in NetworkCapabilities nc, in NetworkScore score,
+ in LocalNetworkConfig localNetworkConfig, in NetworkAgentConfig config,
in int factorySerialNumber);
NetworkRequest requestNetwork(int uid, in NetworkCapabilities networkCapabilities, int reqType,
@@ -238,6 +240,8 @@
void setTestAllowBadWifiUntil(long timeMs);
+ void setDataSaverEnabled(boolean enable);
+
void updateMeteredNetworkAllowList(int uid, boolean add);
void updateMeteredNetworkDenyList(int uid, boolean add);
@@ -257,4 +261,6 @@
void setVpnNetworkPreference(String session, in UidRange[] ranges);
void setTestLowTcpPollingTimerForKeepalive(long timeMs);
+
+ IBinder getRoutingCoordinatorService();
}
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl
index b375b7b..61b27b5 100644
--- a/framework/src/android/net/INetworkAgentRegistry.aidl
+++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -17,6 +17,7 @@
import android.net.DscpPolicy;
import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
@@ -34,6 +35,7 @@
void sendLinkProperties(in LinkProperties lp);
// TODO: consider replacing this by "markConnected()" and removing
void sendNetworkInfo(in NetworkInfo info);
+ void sendLocalNetworkConfig(in LocalNetworkConfig config);
void sendScore(in NetworkScore score);
void sendExplicitlySelected(boolean explicitlySelected, boolean acceptPartial);
void sendSocketKeepaliveEvent(int slot, int reason);
diff --git a/framework/src/android/net/IRoutingCoordinator.aidl b/framework/src/android/net/IRoutingCoordinator.aidl
new file mode 100644
index 0000000..a5cda98
--- /dev/null
+++ b/framework/src/android/net/IRoutingCoordinator.aidl
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 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;
+
+import android.net.RouteInfo;
+
+/** @hide */
+interface IRoutingCoordinator {
+ /**
+ * Add a route for specific network
+ *
+ * @param netId the network to add the route to
+ * @param route the route to add
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ void addRoute(int netId, in RouteInfo route);
+
+ /**
+ * Remove a route for specific network
+ *
+ * @param netId the network to remove the route from
+ * @param route the route to remove
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ void removeRoute(int netId, in RouteInfo route);
+
+ /**
+ * Update a route for specific network
+ *
+ * @param netId the network to update the route for
+ * @param route parcelable with route information
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ void updateRoute(int netId, in RouteInfo route);
+
+ /**
+ * Adds an interface to a network. The interface must not be assigned to any network, including
+ * the specified network.
+ *
+ * @param netId the network to add the interface to.
+ * @param iface the name of the interface to add.
+ *
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ void addInterfaceToNetwork(int netId, in String iface);
+
+ /**
+ * Removes an interface from a network. The interface must be assigned to the specified network.
+ *
+ * @param netId the network to remove the interface from.
+ * @param iface the name of the interface to remove.
+ *
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ void removeInterfaceFromNetwork(int netId, in String iface);
+}
diff --git a/framework/src/android/net/LocalNetworkConfig.java b/framework/src/android/net/LocalNetworkConfig.java
new file mode 100644
index 0000000..fca7fd1
--- /dev/null
+++ b/framework/src/android/net/LocalNetworkConfig.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A class to communicate configuration info about a local network through {@link NetworkAgent}.
+ * @hide
+ */
+// TODO : @SystemApi
+public final class LocalNetworkConfig implements Parcelable {
+ @Nullable
+ private final NetworkRequest mUpstreamSelector;
+
+ @NonNull
+ private final MulticastRoutingConfig mUpstreamMulticastRoutingConfig;
+
+ @NonNull
+ private final MulticastRoutingConfig mDownstreamMulticastRoutingConfig;
+
+ private LocalNetworkConfig(@Nullable final NetworkRequest upstreamSelector,
+ @Nullable final MulticastRoutingConfig upstreamConfig,
+ @Nullable final MulticastRoutingConfig downstreamConfig) {
+ mUpstreamSelector = upstreamSelector;
+ if (null != upstreamConfig) {
+ mUpstreamMulticastRoutingConfig = upstreamConfig;
+ } else {
+ mUpstreamMulticastRoutingConfig = MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+ }
+ if (null != downstreamConfig) {
+ mDownstreamMulticastRoutingConfig = downstreamConfig;
+ } else {
+ mDownstreamMulticastRoutingConfig = MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+ }
+ }
+
+ /**
+ * Get the request choosing which network traffic from this network is forwarded to and from.
+ *
+ * This may be null if the local network doesn't forward the traffic anywhere.
+ */
+ @Nullable
+ public NetworkRequest getUpstreamSelector() {
+ return mUpstreamSelector;
+ }
+
+ public @NonNull MulticastRoutingConfig getUpstreamMulticastRoutingConfig() {
+ return mUpstreamMulticastRoutingConfig;
+ }
+
+ public @NonNull MulticastRoutingConfig getDownstreamMulticastRoutingConfig() {
+ return mDownstreamMulticastRoutingConfig;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull final Parcel dest, final int flags) {
+ dest.writeParcelable(mUpstreamSelector, flags);
+ dest.writeParcelable(mUpstreamMulticastRoutingConfig, flags);
+ dest.writeParcelable(mDownstreamMulticastRoutingConfig, flags);
+ }
+
+ public static final @NonNull Creator<LocalNetworkConfig> CREATOR = new Creator<>() {
+ public LocalNetworkConfig createFromParcel(Parcel in) {
+ final NetworkRequest upstreamSelector = in.readParcelable(null);
+ final MulticastRoutingConfig upstreamConfig = in.readParcelable(null);
+ final MulticastRoutingConfig downstreamConfig = in.readParcelable(null);
+ return new LocalNetworkConfig(
+ upstreamSelector, upstreamConfig, downstreamConfig);
+ }
+
+ @Override
+ public LocalNetworkConfig[] newArray(final int size) {
+ return new LocalNetworkConfig[size];
+ }
+ };
+
+
+ public static final class Builder {
+ @Nullable
+ NetworkRequest mUpstreamSelector;
+
+ @Nullable
+ MulticastRoutingConfig mUpstreamMulticastRoutingConfig;
+
+ @Nullable
+ MulticastRoutingConfig mDownstreamMulticastRoutingConfig;
+
+ /**
+ * Create a Builder
+ */
+ public Builder() {
+ }
+
+ /**
+ * Set to choose where this local network should forward its traffic to.
+ *
+ * The system will automatically choose the best network matching the request as an
+ * upstream, and set up forwarding between this local network and the chosen upstream.
+ * If no network matches the request, there is no upstream and the traffic is not forwarded.
+ * The caller can know when this changes by listening to link properties changes of
+ * this network with the {@link android.net.LinkProperties#getForwardedNetwork()} getter.
+ *
+ * Set this to null if the local network shouldn't be forwarded. Default is null.
+ */
+ @NonNull
+ public Builder setUpstreamSelector(@Nullable NetworkRequest upstreamSelector) {
+ mUpstreamSelector = upstreamSelector;
+ return this;
+ }
+
+ /**
+ * Set the upstream multicast routing config.
+ *
+ * If null, don't route multicast packets upstream. This is equivalent to a
+ * MulticastRoutingConfig in mode FORWARD_NONE. The default is null.
+ */
+ @NonNull
+ public Builder setUpstreamMulticastRoutingConfig(@Nullable MulticastRoutingConfig cfg) {
+ mUpstreamMulticastRoutingConfig = cfg;
+ return this;
+ }
+
+ /**
+ * Set the downstream multicast routing config.
+ *
+ * If null, don't route multicast packets downstream. This is equivalent to a
+ * MulticastRoutingConfig in mode FORWARD_NONE. The default is null.
+ */
+ @NonNull
+ public Builder setDownstreamMulticastRoutingConfig(@Nullable MulticastRoutingConfig cfg) {
+ mDownstreamMulticastRoutingConfig = cfg;
+ return this;
+ }
+
+ /**
+ * Build the LocalNetworkConfig object.
+ */
+ @NonNull
+ public LocalNetworkConfig build() {
+ return new LocalNetworkConfig(mUpstreamSelector,
+ mUpstreamMulticastRoutingConfig,
+ mDownstreamMulticastRoutingConfig);
+ }
+ }
+}
diff --git a/framework/src/android/net/MulticastRoutingConfig.java b/framework/src/android/net/MulticastRoutingConfig.java
new file mode 100644
index 0000000..ebd9fc5
--- /dev/null
+++ b/framework/src/android/net/MulticastRoutingConfig.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2023 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;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * A class representing a configuration for multicast routing.
+ *
+ * Internal usage to Connectivity
+ * @hide
+ */
+// TODO : @SystemApi
+public class MulticastRoutingConfig implements Parcelable {
+ private static final String TAG = MulticastRoutingConfig.class.getSimpleName();
+
+ /** Do not forward any multicast packets. */
+ public static final int FORWARD_NONE = 0;
+ /**
+ * Forward only multicast packets with destination in the list of listening addresses.
+ * Ignore the min scope.
+ */
+ public static final int FORWARD_SELECTED = 1;
+ /**
+ * Forward all multicast packets with scope greater or equal than the min scope.
+ * Ignore the list of listening addresses.
+ */
+ public static final int FORWARD_WITH_MIN_SCOPE = 2;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "FORWARD_" }, value = {
+ FORWARD_NONE,
+ FORWARD_SELECTED,
+ FORWARD_WITH_MIN_SCOPE
+ })
+ public @interface MulticastForwardingMode {}
+
+ /**
+ * Not a multicast scope, for configurations that do not use the min scope.
+ */
+ public static final int MULTICAST_SCOPE_NONE = -1;
+
+ public static final MulticastRoutingConfig CONFIG_FORWARD_NONE =
+ new MulticastRoutingConfig(FORWARD_NONE, MULTICAST_SCOPE_NONE, null);
+
+ @MulticastForwardingMode
+ private final int mForwardingMode;
+
+ private final int mMinScope;
+
+ @NonNull
+ private final Set<Inet6Address> mListeningAddresses;
+
+ private MulticastRoutingConfig(@MulticastForwardingMode final int mode, final int scope,
+ @Nullable final Set<Inet6Address> addresses) {
+ mForwardingMode = mode;
+ mMinScope = scope;
+ if (null != addresses) {
+ mListeningAddresses = Collections.unmodifiableSet(new ArraySet<>(addresses));
+ } else {
+ mListeningAddresses = Collections.emptySet();
+ }
+ }
+
+ /**
+ * Returns the forwarding mode.
+ */
+ @MulticastForwardingMode
+ public int getForwardingMode() {
+ return mForwardingMode;
+ }
+
+ /**
+ * Returns the minimal group address scope that is allowed for forwarding.
+ * If the forwarding mode is not FORWARD_WITH_MIN_SCOPE, will be MULTICAST_SCOPE_NONE.
+ */
+ public int getMinScope() {
+ return mMinScope;
+ }
+
+ /**
+ * Returns the list of group addresses listened by the outgoing interface.
+ * The list will be empty if the forwarding mode is not FORWARD_SELECTED.
+ */
+ @NonNull
+ public Set<Inet6Address> getMulticastListeningAddresses() {
+ return mListeningAddresses;
+ }
+
+ private MulticastRoutingConfig(Parcel in) {
+ mForwardingMode = in.readInt();
+ mMinScope = in.readInt();
+ final int count = in.readInt();
+ final ArraySet<Inet6Address> listeningAddresses = new ArraySet<>(count);
+ final byte[] buffer = new byte[16]; // Size of an Inet6Address
+ for (int i = 0; i < count; ++i) {
+ in.readByteArray(buffer);
+ try {
+ listeningAddresses.add((Inet6Address) Inet6Address.getByAddress(buffer));
+ } catch (UnknownHostException e) {
+ Log.wtf(TAG, "Can't read inet6address : " + Arrays.toString(buffer));
+ }
+ }
+ mListeningAddresses = Collections.unmodifiableSet(listeningAddresses);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mForwardingMode);
+ dest.writeInt(mMinScope);
+ dest.writeInt(mListeningAddresses.size());
+ for (final Inet6Address addr : mListeningAddresses) {
+ dest.writeByteArray(addr.getAddress());
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<MulticastRoutingConfig> CREATOR = new Creator<>() {
+ @Override
+ public MulticastRoutingConfig createFromParcel(Parcel in) {
+ return new MulticastRoutingConfig(in);
+ }
+
+ @Override
+ public MulticastRoutingConfig[] newArray(int size) {
+ return new MulticastRoutingConfig[size];
+ }
+ };
+
+ public static class Builder {
+ @MulticastForwardingMode
+ private final int mForwardingMode;
+ private int mMinScope;
+ private final ArraySet<Inet6Address> mListeningAddresses;
+
+ private Builder(@MulticastForwardingMode final int mode, int scope) {
+ mForwardingMode = mode;
+ mMinScope = scope;
+ mListeningAddresses = new ArraySet<>();
+ }
+
+ /**
+ * Create a builder that forwards nothing.
+ * No properties can be set on such a builder.
+ */
+ public static Builder newBuilderForwardingNone() {
+ return new Builder(FORWARD_NONE, MULTICAST_SCOPE_NONE);
+ }
+
+ /**
+ * Create a builder that forwards packets above a certain scope
+ *
+ * The scope can be changed on this builder, but not the listening addresses.
+ * @param scope the initial scope
+ */
+ public static Builder newBuilderWithMinScope(final int scope) {
+ return new Builder(FORWARD_WITH_MIN_SCOPE, scope);
+ }
+
+ /**
+ * Create a builder that forwards a specified list of listening addresses.
+ *
+ * Addresses can be added and removed from this builder, but the scope can't be set.
+ */
+ public static Builder newBuilderWithListeningAddresses() {
+ return new Builder(FORWARD_SELECTED, MULTICAST_SCOPE_NONE);
+ }
+
+ /**
+ * Sets the minimum scope for this multicast routing config.
+ * This is only meaningful (indeed, allowed) for configs in FORWARD_WITH_MIN_SCOPE mode.
+ * @return this builder
+ */
+ public Builder setMinimumScope(final int scope) {
+ if (FORWARD_WITH_MIN_SCOPE != mForwardingMode) {
+ throw new IllegalArgumentException("Can't set the scope on a builder in mode "
+ + modeToString(mForwardingMode));
+ }
+ mMinScope = scope;
+ return this;
+ }
+
+ /**
+ * Add an address to the set of listening addresses.
+ *
+ * This is only meaningful (indeed, allowed) for configs in FORWARD_SELECTED mode.
+ * If this address was already added, this is a no-op.
+ * @return this builder
+ */
+ public Builder addListeningAddress(@NonNull final Inet6Address address) {
+ if (FORWARD_SELECTED != mForwardingMode) {
+ throw new IllegalArgumentException("Can't add an address on a builder in mode "
+ + modeToString(mForwardingMode));
+ }
+ // TODO : should we check that this is a multicast address ?
+ mListeningAddresses.add(address);
+ return this;
+ }
+
+ /**
+ * Remove an address from the set of listening addresses.
+ *
+ * This is only meaningful (indeed, allowed) for configs in FORWARD_SELECTED mode.
+ * If this address was not added, or was already removed, this is a no-op.
+ * @return this builder
+ */
+ public Builder removeListeningAddress(@NonNull final Inet6Address address) {
+ if (FORWARD_SELECTED != mForwardingMode) {
+ throw new IllegalArgumentException("Can't remove an address on a builder in mode "
+ + modeToString(mForwardingMode));
+ }
+ mListeningAddresses.remove(address);
+ return this;
+ }
+
+ /**
+ * Build the config.
+ */
+ public MulticastRoutingConfig build() {
+ return new MulticastRoutingConfig(mForwardingMode, mMinScope, mListeningAddresses);
+ }
+ }
+
+ private static String modeToString(@MulticastForwardingMode final int mode) {
+ switch (mode) {
+ case FORWARD_NONE: return "FORWARD_NONE";
+ case FORWARD_SELECTED: return "FORWARD_SELECTED";
+ case FORWARD_WITH_MIN_SCOPE: return "FORWARD_WITH_MIN_SCOPE";
+ default: return "unknown multicast routing mode " + mode;
+ }
+ }
+}
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 177f7e3..4e9087c 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -151,7 +151,7 @@
/**
* Sent by the NetworkAgent to ConnectivityService to pass the current
- * NetworkCapabilties.
+ * NetworkCapabilities.
* obj = NetworkCapabilities
* @hide
*/
@@ -443,6 +443,14 @@
public static final int EVENT_UNREGISTER_AFTER_REPLACEMENT = BASE + 29;
/**
+ * Sent by the NetworkAgent to ConnectivityService to pass the new value of the local
+ * network agent config.
+ * obj = {@code Pair<NetworkAgentInfo, LocalNetworkConfig>}
+ * @hide
+ */
+ public static final int EVENT_LOCAL_NETWORK_CONFIG_CHANGED = BASE + 30;
+
+ /**
* DSCP policy was successfully added.
*/
public static final int DSCP_POLICY_STATUS_SUCCESS = 0;
@@ -517,20 +525,47 @@
@NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
@NonNull NetworkScore score, @NonNull NetworkAgentConfig config,
@Nullable NetworkProvider provider) {
- this(looper, context, logTag, nc, lp, score, config,
+ this(context, looper, logTag, nc, lp, null /* localNetworkConfig */, score, config,
+ provider);
+ }
+
+ /**
+ * Create a new network agent.
+ * @param context a {@link Context} to get system services from.
+ * @param looper the {@link Looper} on which to invoke the callbacks.
+ * @param logTag the tag for logs
+ * @param nc the initial {@link NetworkCapabilities} of this network. Update with
+ * sendNetworkCapabilities.
+ * @param lp the initial {@link LinkProperties} of this network. Update with sendLinkProperties.
+ * @param localNetworkConfig the initial {@link LocalNetworkConfig} of this
+ * network. Update with sendLocalNetworkConfig. Must be
+ * non-null iff the nc have NET_CAPABILITY_LOCAL_NETWORK.
+ * @param score the initial score of this network. Update with sendNetworkScore.
+ * @param config an immutable {@link NetworkAgentConfig} for this agent.
+ * @param provider the {@link NetworkProvider} managing this agent.
+ * @hide
+ */
+ // TODO : expose
+ public NetworkAgent(@NonNull Context context, @NonNull Looper looper, @NonNull String logTag,
+ @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
+ @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
+ @NonNull NetworkAgentConfig config, @Nullable NetworkProvider provider) {
+ this(looper, context, logTag, nc, lp, localNetworkConfig, score, config,
provider == null ? NetworkProvider.ID_NONE : provider.getProviderId(),
getLegacyNetworkInfo(config));
}
private static class InitialConfiguration {
- public final Context context;
- public final NetworkCapabilities capabilities;
- public final LinkProperties properties;
- public final NetworkScore score;
- public final NetworkAgentConfig config;
- public final NetworkInfo info;
+ @NonNull public final Context context;
+ @NonNull public final NetworkCapabilities capabilities;
+ @NonNull public final LinkProperties properties;
+ @NonNull public final NetworkScore score;
+ @NonNull public final NetworkAgentConfig config;
+ @NonNull public final NetworkInfo info;
+ @Nullable public final LocalNetworkConfig localNetworkConfig;
InitialConfiguration(@NonNull Context context, @NonNull NetworkCapabilities capabilities,
- @NonNull LinkProperties properties, @NonNull NetworkScore score,
+ @NonNull LinkProperties properties,
+ @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
@NonNull NetworkAgentConfig config, @NonNull NetworkInfo info) {
this.context = context;
this.capabilities = capabilities;
@@ -538,14 +573,15 @@
this.score = score;
this.config = config;
this.info = info;
+ this.localNetworkConfig = localNetworkConfig;
}
}
private volatile InitialConfiguration mInitialConfiguration;
private NetworkAgent(@NonNull Looper looper, @NonNull Context context, @NonNull String logTag,
@NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
- @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, int providerId,
- @NonNull NetworkInfo ni) {
+ @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
+ @NonNull NetworkAgentConfig config, int providerId, @NonNull NetworkInfo ni) {
mHandler = new NetworkAgentHandler(looper);
LOG_TAG = logTag;
mNetworkInfo = new NetworkInfo(ni);
@@ -556,7 +592,7 @@
mInitialConfiguration = new InitialConfiguration(context,
new NetworkCapabilities(nc, NetworkCapabilities.REDACT_NONE),
- new LinkProperties(lp), score, config, ni);
+ new LinkProperties(lp), localNetworkConfig, score, config, ni);
}
private class NetworkAgentHandler extends Handler {
@@ -723,7 +759,8 @@
mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
new NetworkInfo(mInitialConfiguration.info),
mInitialConfiguration.properties, mInitialConfiguration.capabilities,
- mInitialConfiguration.score, mInitialConfiguration.config, providerId);
+ mInitialConfiguration.localNetworkConfig, mInitialConfiguration.score,
+ mInitialConfiguration.config, providerId);
mInitialConfiguration = null; // All this memory can now be GC'd
}
return mNetwork;
@@ -1099,6 +1136,18 @@
}
/**
+ * Must be called by the agent when the network's {@link LocalNetworkConfig} changes.
+ * @param config the new LocalNetworkConfig
+ * @hide
+ */
+ public void sendLocalNetworkConfig(@NonNull LocalNetworkConfig config) {
+ Objects.requireNonNull(config);
+ // If the agent doesn't have NET_CAPABILITY_LOCAL_NETWORK, this will be ignored by
+ // ConnectivityService with a Log.wtf.
+ queueOrSendMessage(reg -> reg.sendLocalNetworkConfig(config));
+ }
+
+ /**
* Must be called by the agent to update the score of this network.
*
* @param score the new score.
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index abda1fa..f959114 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -29,6 +29,9 @@
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.net.ConnectivityManager.NetworkCallback;
+// Can't be imported because aconfig tooling doesn't exist on udc-mainline-prod yet
+// See inner class Flags which mimics this for the time being
+// import android.net.flags.Flags;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
@@ -121,6 +124,14 @@
public final class NetworkCapabilities implements Parcelable {
private static final String TAG = "NetworkCapabilities";
+ // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+ // available here
+ /** @hide */
+ public static class Flags {
+ static final String FLAG_FORBIDDEN_CAPABILITY =
+ "com.android.net.flags.forbidden_capability";
+ }
+
/**
* Mechanism to support redaction of fields in NetworkCapabilities that are guarded by specific
* app permissions.
@@ -442,6 +453,7 @@
NET_CAPABILITY_MMTEL,
NET_CAPABILITY_PRIORITIZE_LATENCY,
NET_CAPABILITY_PRIORITIZE_BANDWIDTH,
+ NET_CAPABILITY_LOCAL_NETWORK,
})
public @interface NetCapability { }
@@ -703,7 +715,21 @@
*/
public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35;
- private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
+ /**
+ * This is a local network, e.g. a tethering downstream or a P2P direct network.
+ *
+ * <p>
+ * Note that local networks are not sent to callbacks by default. To receive callbacks about
+ * them, the {@link NetworkRequest} instance must be prepared to see them, either by
+ * adding the capability with {@link NetworkRequest.Builder#addCapability}, by removing
+ * this forbidden capability with {@link NetworkRequest.Builder#removeForbiddenCapability},
+ * or by clearing all capabilites with {@link NetworkRequest.Builder#clearCapabilities()}.
+ * </p>
+ * @hide
+ */
+ public static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
+
+ private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_LOCAL_NETWORK;
// Set all bits up to the MAX_NET_CAPABILITY-th bit
private static final long ALL_VALID_CAPABILITIES = (2L << MAX_NET_CAPABILITY) - 1;
@@ -793,6 +819,10 @@
* Adds the given capability to this {@code NetworkCapability} instance.
* Note that when searching for a network to satisfy a request, all capabilities
* requested must be satisfied.
+ * <p>
+ * If the capability was previously added to the list of forbidden capabilities (either
+ * by default or added using {@link #addForbiddenCapability(int)}), then it will be removed
+ * from the list of forbidden capabilities as well.
*
* @param capability the capability to be added.
* @return This NetworkCapabilities instance, to facilitate chaining.
@@ -801,8 +831,7 @@
public @NonNull NetworkCapabilities addCapability(@NetCapability int capability) {
// If the given capability was previously added to the list of forbidden capabilities
// then the capability will also be removed from the list of forbidden capabilities.
- // TODO: Consider adding forbidden capabilities to the public API and mention this
- // in the documentation.
+ // TODO: Add forbidden capabilities to the public API
checkValidCapability(capability);
mNetworkCapabilities |= 1L << capability;
// remove from forbidden capability list
@@ -845,7 +874,7 @@
}
/**
- * Removes (if found) the given forbidden capability from this {@code NetworkCapability}
+ * Removes (if found) the given forbidden capability from this {@link NetworkCapabilities}
* instance that were added via addForbiddenCapability(int) or setCapabilities(int[], int[]).
*
* @param capability the capability to be removed.
@@ -859,6 +888,16 @@
}
/**
+ * Removes all forbidden capabilities from this {@link NetworkCapabilities} instance.
+ * @return This NetworkCapabilities instance, to facilitate chaining.
+ * @hide
+ */
+ public @NonNull NetworkCapabilities removeAllForbiddenCapabilities() {
+ mForbiddenNetworkCapabilities = 0;
+ return this;
+ }
+
+ /**
* Sets (or clears) the given capability on this {@link NetworkCapabilities}
* instance.
* @hide
@@ -901,6 +940,8 @@
* @return an array of forbidden capability values for this instance.
* @hide
*/
+ @NonNull
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
public @NetCapability int[] getForbiddenCapabilities() {
return BitUtils.unpackBits(mForbiddenNetworkCapabilities);
}
@@ -1000,7 +1041,7 @@
/**
* Tests for the presence of a capability on this instance.
*
- * @param capability the capabilities to be tested for.
+ * @param capability the capability to be tested for.
* @return {@code true} if set on this instance.
*/
public boolean hasCapability(@NetCapability int capability) {
@@ -1008,19 +1049,27 @@
&& ((mNetworkCapabilities & (1L << capability)) != 0);
}
- /** @hide */
+ /**
+ * Tests for the presence of a forbidden capability on this instance.
+ *
+ * @param capability the capability to be tested for.
+ * @return {@code true} if this capability is set forbidden on this instance.
+ * @hide
+ */
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
public boolean hasForbiddenCapability(@NetCapability int capability) {
return isValidCapability(capability)
&& ((mForbiddenNetworkCapabilities & (1L << capability)) != 0);
}
/**
- * Check if this NetworkCapabilities has system managed capabilities or not.
+ * Check if this NetworkCapabilities has connectivity-managed capabilities or not.
* @hide
*/
public boolean hasConnectivityManagedCapability() {
- return ((mNetworkCapabilities & CONNECTIVITY_MANAGED_CAPABILITIES) != 0);
+ return (mNetworkCapabilities & CONNECTIVITY_MANAGED_CAPABILITIES) != 0
+ || mForbiddenNetworkCapabilities != 0;
}
/**
@@ -2500,6 +2549,7 @@
case NET_CAPABILITY_MMTEL: return "MMTEL";
case NET_CAPABILITY_PRIORITIZE_LATENCY: return "PRIORITIZE_LATENCY";
case NET_CAPABILITY_PRIORITIZE_BANDWIDTH: return "PRIORITIZE_BANDWIDTH";
+ case NET_CAPABILITY_LOCAL_NETWORK: return "LOCAL_NETWORK";
default: return Integer.toString(capability);
}
}
@@ -2889,6 +2939,44 @@
}
/**
+ * Adds the given capability to the list of forbidden capabilities.
+ *
+ * A network with a capability will not match a {@link NetworkCapabilities} or
+ * {@link NetworkRequest} which has said capability set as forbidden. For example, if
+ * a request has NET_CAPABILITY_INTERNET in the list of forbidden capabilities, networks
+ * with NET_CAPABILITY_INTERNET will not match the request.
+ *
+ * If the capability was previously added to the list of required capabilities (for
+ * example, it was there by default or added using {@link #addCapability(int)} method), then
+ * it will be removed from the list of required capabilities as well.
+ *
+ * @param capability the capability
+ * @return this builder
+ * @hide
+ */
+ @NonNull
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
+ public Builder addForbiddenCapability(@NetCapability final int capability) {
+ mCaps.addForbiddenCapability(capability);
+ return this;
+ }
+
+ /**
+ * Removes the given capability from the list of forbidden capabilities.
+ *
+ * @see #addForbiddenCapability(int)
+ * @param capability the capability
+ * @return this builder
+ * @hide
+ */
+ @NonNull
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
+ public Builder removeForbiddenCapability(@NetCapability final int capability) {
+ mCaps.removeForbiddenCapability(capability);
+ return this;
+ }
+
+ /**
* Adds the given enterprise capability identifier.
* Note that when searching for a network to satisfy a request, all capabilities identifier
* requested must be satisfied. Enterprise capability identifier is applicable only
@@ -3235,4 +3323,4 @@
return new NetworkCapabilities(mCaps);
}
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 6c351d0..9824faa 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -20,6 +20,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -39,6 +40,8 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
+// TODO : replace with android.net.flags.Flags when aconfig is supported on udc-mainline-prod
+// import android.net.NetworkCapabilities.Flags;
import android.net.NetworkCapabilities.NetCapability;
import android.net.NetworkCapabilities.Transport;
import android.os.Build;
@@ -281,6 +284,15 @@
NET_CAPABILITY_TRUSTED,
NET_CAPABILITY_VALIDATED);
+ /**
+ * Capabilities that are forbidden by default.
+ * Forbidden capabilities only make sense in NetworkRequest, not for network agents.
+ * Therefore these capabilities are only in NetworkRequest.
+ */
+ private static final int[] DEFAULT_FORBIDDEN_CAPABILITIES = new int[] {
+ NET_CAPABILITY_LOCAL_NETWORK
+ };
+
private final NetworkCapabilities mNetworkCapabilities;
// A boolean that represents whether the NOT_VCN_MANAGED capability should be deduced when
@@ -296,6 +308,16 @@
// it for apps that do not have the NETWORK_SETTINGS permission.
mNetworkCapabilities = new NetworkCapabilities();
mNetworkCapabilities.setSingleUid(Process.myUid());
+ // Default forbidden capabilities are foremost meant to help with backward
+ // compatibility. When adding new types of network identified by a capability that
+ // might confuse older apps, a default forbidden capability will have apps not see
+ // these networks unless they explicitly ask for it.
+ // If the app called clearCapabilities() it will see everything, but then it
+ // can be argued that it's fair to send them too, since it asked for everything
+ // explicitly.
+ for (final int forbiddenCap : DEFAULT_FORBIDDEN_CAPABILITIES) {
+ mNetworkCapabilities.addForbiddenCapability(forbiddenCap);
+ }
}
/**
@@ -408,6 +430,7 @@
@NonNull
@SuppressLint("MissingGetterMatchingBuilder")
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
public Builder addForbiddenCapability(@NetworkCapabilities.NetCapability int capability) {
mNetworkCapabilities.addForbiddenCapability(capability);
return this;
@@ -424,6 +447,7 @@
@NonNull
@SuppressLint("BuilderSetStyle")
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
public Builder removeForbiddenCapability(
@NetworkCapabilities.NetCapability int capability) {
mNetworkCapabilities.removeForbiddenCapability(capability);
@@ -433,6 +457,7 @@
/**
* Completely clears all the {@code NetworkCapabilities} from this builder instance,
* removing even the capabilities that are set by default when the object is constructed.
+ * Also removes any set forbidden capabilities.
*
* @return The builder to facilitate chaining.
*/
@@ -721,6 +746,7 @@
* @hide
*/
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public instead of @SystemApi
public boolean hasForbiddenCapability(@NetCapability int capability) {
return networkCapabilities.hasForbiddenCapability(capability);
}
@@ -843,6 +869,7 @@
*/
@NonNull
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public instead of @SystemApi
public @NetCapability int[] getForbiddenCapabilities() {
// No need to make a defensive copy here as NC#getForbiddenCapabilities() already returns
// a new array.
diff --git a/framework/src/android/net/NetworkScore.java b/framework/src/android/net/NetworkScore.java
index 815e2b0..00382f6 100644
--- a/framework/src/android/net/NetworkScore.java
+++ b/framework/src/android/net/NetworkScore.java
@@ -44,7 +44,9 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
KEEP_CONNECTED_NONE,
- KEEP_CONNECTED_FOR_HANDOVER
+ KEEP_CONNECTED_FOR_HANDOVER,
+ KEEP_CONNECTED_FOR_TEST,
+ KEEP_CONNECTED_DOWNSTREAM_NETWORK
})
public @interface KeepConnectedReason { }
@@ -57,6 +59,18 @@
* is being considered for handover.
*/
public static final int KEEP_CONNECTED_FOR_HANDOVER = 1;
+ /**
+ * Keep this network connected even if there is no outstanding request for it, because it
+ * is used in a test and it's not necessarily easy to file the right request for it.
+ * @hide
+ */
+ public static final int KEEP_CONNECTED_FOR_TEST = 2;
+ /**
+ * Keep this network connected even if there is no outstanding request for it, because
+ * it is a downstream network.
+ * @hide
+ */
+ public static final int KEEP_CONNECTED_DOWNSTREAM_NETWORK = 3;
// Agent-managed policies
// This network should lose to a wifi that has ever been validated
diff --git a/framework/src/android/net/RouteInfo.java b/framework/src/android/net/RouteInfo.java
index df5f151..e8ebf81 100644
--- a/framework/src/android/net/RouteInfo.java
+++ b/framework/src/android/net/RouteInfo.java
@@ -584,7 +584,7 @@
}
RouteKey p = (RouteKey) o;
// No need to do anything special for scoped addresses. Inet6Address#equals does not
- // consider the scope ID, but the netd route IPCs (e.g., INetd#networkAddRouteParcel)
+ // consider the scope ID, but the route IPCs (e.g., RoutingCoordinatorManager#addRoute)
// and the kernel ignore scoped addresses both in the prefix and in the nexthop and only
// look at RTA_OIF.
return Objects.equals(p.mDestination, mDestination)
diff --git a/framework/src/android/net/RoutingCoordinatorManager.java b/framework/src/android/net/RoutingCoordinatorManager.java
new file mode 100644
index 0000000..5576cb0
--- /dev/null
+++ b/framework/src/android/net/RoutingCoordinatorManager.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2023 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;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * A manager class for talking to the routing coordinator service.
+ *
+ * This class should only be used by the connectivity and tethering module. This is enforced
+ * by the build rules. Do not change build rules to gain access to this class from elsewhere.
+ * @hide
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public class RoutingCoordinatorManager {
+ @NonNull final Context mContext;
+ @NonNull final IRoutingCoordinator mService;
+
+ public RoutingCoordinatorManager(@NonNull final Context context,
+ @NonNull final IRoutingCoordinator service) {
+ mContext = context;
+ mService = service;
+ }
+
+ /**
+ * Add a route for specific network
+ *
+ * @param netId the network to add the route to
+ * @param route the route to add
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void addRoute(final int netId, final RouteInfo route) {
+ try {
+ mService.addRoute(netId, route);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Remove a route for specific network
+ *
+ * @param netId the network to remove the route from
+ * @param route the route to remove
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void removeRoute(final int netId, final RouteInfo route) {
+ try {
+ mService.removeRoute(netId, route);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Update a route for specific network
+ *
+ * @param netId the network to update the route for
+ * @param route parcelable with route information
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void updateRoute(final int netId, final RouteInfo route) {
+ try {
+ mService.updateRoute(netId, route);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Adds an interface to a network. The interface must not be assigned to any network, including
+ * the specified network.
+ *
+ * @param netId the network to add the interface to.
+ * @param iface the name of the interface to add.
+ *
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ public void addInterfaceToNetwork(final int netId, final String iface) {
+ try {
+ mService.addInterfaceToNetwork(netId, iface);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Removes an interface from a network. The interface must be assigned to the specified network.
+ *
+ * @param netId the network to remove the interface from.
+ * @param iface the name of the interface to remove.
+ *
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ public void removeInterfaceFromNetwork(final int netId, final String iface) {
+ try {
+ mService.removeInterfaceFromNetwork(netId, iface);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/service/src/com/android/server/UidOwnerValue.java b/framework/src/android/net/UidOwnerValue.java
similarity index 86%
rename from service/src/com/android/server/UidOwnerValue.java
rename to framework/src/android/net/UidOwnerValue.java
index d6c0e0d..e8ae604 100644
--- a/service/src/com/android/server/UidOwnerValue.java
+++ b/framework/src/android/net/UidOwnerValue.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -14,11 +14,15 @@
* limitations under the License.
*/
-package com.android.server;
+package android.net;
import com.android.net.module.util.Struct;
-/** Value type for per uid traffic control configuration map */
+/**
+ * Value type for per uid traffic control configuration map.
+ *
+ * @hide
+ */
public class UidOwnerValue extends Struct {
// Allowed interface index. Only applicable if IIF_MATCH is set in the rule bitmask below.
@Field(order = 0, type = Type.S32)
diff --git a/framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java b/framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java
index d65858f..c2d75d2 100644
--- a/framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java
+++ b/framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.net.ConnectivityManager;
+import android.net.RoutingCoordinatorManager;
import android.os.Build;
import android.os.IBinder;
@@ -34,15 +35,28 @@
* linter).
* @hide
*/
-@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+// TODO : rename this so that it doesn't reference "Tiramisu" since it can be used in S.
+@RequiresApi(Build.VERSION_CODES.S)
public class TiramisuConnectivityInternalApiUtil {
/**
* Get a service binder token for
* {@link com.android.server.connectivity.wear.CompanionDeviceManagerProxyService}.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public static IBinder getCompanionDeviceManagerProxyService(Context ctx) {
final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
return cm.getCompanionDeviceManagerProxyService();
}
+
+ /**
+ * Obtain a routing coordinator manager from a context, possibly cross-module.
+ * @param ctx the context
+ * @return an instance of the coordinator manager
+ */
+ @RequiresApi(Build.VERSION_CODES.S)
+ public static RoutingCoordinatorManager getRoutingCoordinatorManager(Context ctx) {
+ final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
+ return cm.getRoutingCoordinatorManager();
+ }
}
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index a61d180..66a1ffe 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -26,7 +26,7 @@
"bluetooth-test-util-lib",
"compatibility-device-util-axt",
"ctstestrunner-axt",
- "truth-prebuilt",
+ "truth",
],
libs: [
"android.test.base",
diff --git a/nearby/tests/integration/privileged/Android.bp b/nearby/tests/integration/privileged/Android.bp
index e3250f6..9b6e488 100644
--- a/nearby/tests/integration/privileged/Android.bp
+++ b/nearby/tests/integration/privileged/Android.bp
@@ -27,7 +27,7 @@
"androidx.test.ext.junit",
"androidx.test.rules",
"junit",
- "truth-prebuilt",
+ "truth",
],
test_suites: ["device-tests"],
}
diff --git a/nearby/tests/integration/untrusted/Android.bp b/nearby/tests/integration/untrusted/Android.bp
index 57499e4..75f765b 100644
--- a/nearby/tests/integration/untrusted/Android.bp
+++ b/nearby/tests/integration/untrusted/Android.bp
@@ -31,7 +31,7 @@
"androidx.test.uiautomator_uiautomator",
"junit",
"kotlin-test",
- "truth-prebuilt",
+ "truth",
],
test_suites: ["device-tests"],
}
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 9b35452..112c751 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -42,7 +42,7 @@
"mockito-target-extended-minus-junit4",
"platform-test-annotations",
"service-nearby-pre-jarjar",
- "truth-prebuilt",
+ "truth",
// "Robolectric_all-target",
],
// these are needed for Extended Mockito
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
new file mode 100644
index 0000000..1f92374
--- /dev/null
+++ b/netbpfload/Android.bp
@@ -0,0 +1,51 @@
+//
+// Copyright (C) 2023 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.
+//
+
+cc_binary {
+ name: "netbpfload",
+
+ defaults: ["bpf_defaults"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wthread-safety",
+ ],
+ sanitize: {
+ integer_overflow: true,
+ },
+
+ header_libs: ["bpf_headers"],
+ shared_libs: [
+ "libbase",
+ "liblog",
+ ],
+ srcs: [
+ "loader.cpp",
+ "NetBpfLoad.cpp",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+ // really should be Android 14/U (34), but we cannot include binaries built
+ // against newer sdk in the apex, which still targets 30(R):
+ // module "netbpfload" variant "android_x86_apex30": should support
+ // min_sdk_version(30) for "com.android.tethering": newer SDK(34).
+ min_sdk_version: "30",
+
+ init_rc: ["netbpfload.rc"],
+ required: ["bpfloader"],
+}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 8e47ea8..6152287 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -38,18 +38,18 @@
#include <sys/stat.h>
#include <sys/types.h>
+#include <android/api-level.h>
#include <android-base/logging.h>
#include <android-base/macros.h>
#include <android-base/properties.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include <android-base/unique_fd.h>
-#include <libbpf_android.h>
#include <log/log.h>
-#include <netdutils/Misc.h>
-#include <netdutils/Slice.h>
+
#include "BpfSyscallWrappers.h"
#include "bpf/BpfUtils.h"
+#include "loader.h"
using android::base::EndsWith;
using android::bpf::domain;
@@ -66,76 +66,34 @@
abort(); // can only hit this if permissions (likely selinux) are screwed up
}
-constexpr unsigned long long kTetheringApexDomainBitmask =
- domainToBitmask(domain::tethering) |
- domainToBitmask(domain::net_private) |
- domainToBitmask(domain::net_shared) |
- domainToBitmask(domain::netd_readonly) |
- domainToBitmask(domain::netd_shared);
-
-// Programs shipped inside the tethering apex should be limited to networking stuff,
-// as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
-// since they are less stable abi/api and may conflict with platform uses of bpf.
-constexpr bpf_prog_type kTetheringApexAllowedProgTypes[] = {
- BPF_PROG_TYPE_CGROUP_SKB,
- BPF_PROG_TYPE_CGROUP_SOCK,
- BPF_PROG_TYPE_CGROUP_SOCKOPT,
- BPF_PROG_TYPE_CGROUP_SOCK_ADDR,
- BPF_PROG_TYPE_CGROUP_SYSCTL,
- BPF_PROG_TYPE_LWT_IN,
- BPF_PROG_TYPE_LWT_OUT,
- BPF_PROG_TYPE_LWT_SEG6LOCAL,
- BPF_PROG_TYPE_LWT_XMIT,
- BPF_PROG_TYPE_SCHED_ACT,
- BPF_PROG_TYPE_SCHED_CLS,
- BPF_PROG_TYPE_SOCKET_FILTER,
- BPF_PROG_TYPE_SOCK_OPS,
- BPF_PROG_TYPE_XDP,
-};
-
const android::bpf::Location locations[] = {
// S+ Tethering mainline module (network_stack): tether offload
{
.dir = "/apex/com.android.tethering/etc/bpf/",
.prefix = "tethering/",
- .allowedDomainBitmask = kTetheringApexDomainBitmask,
- .allowedProgTypes = kTetheringApexAllowedProgTypes,
- .allowedProgTypesLength = arraysize(kTetheringApexAllowedProgTypes),
},
// T+ Tethering mainline module (shared with netd & system server)
// netutils_wrapper (for iptables xt_bpf) has access to programs
{
.dir = "/apex/com.android.tethering/etc/bpf/netd_shared/",
.prefix = "netd_shared/",
- .allowedDomainBitmask = kTetheringApexDomainBitmask,
- .allowedProgTypes = kTetheringApexAllowedProgTypes,
- .allowedProgTypesLength = arraysize(kTetheringApexAllowedProgTypes),
},
// T+ Tethering mainline module (shared with netd & system server)
// netutils_wrapper has no access, netd has read only access
{
.dir = "/apex/com.android.tethering/etc/bpf/netd_readonly/",
.prefix = "netd_readonly/",
- .allowedDomainBitmask = kTetheringApexDomainBitmask,
- .allowedProgTypes = kTetheringApexAllowedProgTypes,
- .allowedProgTypesLength = arraysize(kTetheringApexAllowedProgTypes),
},
// T+ Tethering mainline module (shared with system server)
{
.dir = "/apex/com.android.tethering/etc/bpf/net_shared/",
.prefix = "net_shared/",
- .allowedDomainBitmask = kTetheringApexDomainBitmask,
- .allowedProgTypes = kTetheringApexAllowedProgTypes,
- .allowedProgTypesLength = arraysize(kTetheringApexAllowedProgTypes),
},
// T+ Tethering mainline module (not shared, just network_stack)
{
.dir = "/apex/com.android.tethering/etc/bpf/net_private/",
.prefix = "net_private/",
- .allowedDomainBitmask = kTetheringApexDomainBitmask,
- .allowedProgTypes = kTetheringApexAllowedProgTypes,
- .allowedProgTypesLength = arraysize(kTetheringApexAllowedProgTypes),
},
};
@@ -211,10 +169,13 @@
return 0;
}
-int main(int argc, char** argv) {
+int main(int argc, char** argv, char * const envp[]) {
(void)argc;
android::base::InitLogging(argv, &android::base::KernelLogger);
+ const int device_api_level = android_get_device_api_level();
+ const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
+
if (!android::bpf::isAtLeastKernelVersion(4, 19, 0)) {
ALOGE("Android U QPR2 requires kernel 4.19.");
return 1;
@@ -251,24 +212,27 @@
return 1;
}
- // Linux 5.16-rc1 changed the default to 2 (disabled but changeable), but we need 0 (enabled)
- // (this writeFile is known to fail on at least 4.19, but always defaults to 0 on pre-5.13,
- // on 5.13+ it depends on CONFIG_BPF_UNPRIV_DEFAULT_OFF)
- if (writeProcSysFile("/proc/sys/kernel/unprivileged_bpf_disabled", "0\n") &&
- android::bpf::isAtLeastKernelVersion(5, 13, 0)) return 1;
+ if (isAtLeastU) {
+ // Linux 5.16-rc1 changed the default to 2 (disabled but changeable),
+ // but we need 0 (enabled)
+ // (this writeFile is known to fail on at least 4.19, but always defaults to 0 on
+ // pre-5.13, on 5.13+ it depends on CONFIG_BPF_UNPRIV_DEFAULT_OFF)
+ if (writeProcSysFile("/proc/sys/kernel/unprivileged_bpf_disabled", "0\n") &&
+ android::bpf::isAtLeastKernelVersion(5, 13, 0)) return 1;
- // Enable the eBPF JIT -- but do note that on 64-bit kernels it is likely
- // already force enabled by the kernel config option BPF_JIT_ALWAYS_ON.
- // (Note: this (open) will fail with ENOENT 'No such file or directory' if
- // kernel does not have CONFIG_BPF_JIT=y)
- // BPF_JIT is required by R VINTF (which means 4.14/4.19/5.4 kernels),
- // but 4.14/4.19 were released with P & Q, and only 5.4 is new in R+.
- if (writeProcSysFile("/proc/sys/net/core/bpf_jit_enable", "1\n")) return 1;
+ // Enable the eBPF JIT -- but do note that on 64-bit kernels it is likely
+ // already force enabled by the kernel config option BPF_JIT_ALWAYS_ON.
+ // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+ // kernel does not have CONFIG_BPF_JIT=y)
+ // BPF_JIT is required by R VINTF (which means 4.14/4.19/5.4 kernels),
+ // but 4.14/4.19 were released with P & Q, and only 5.4 is new in R+.
+ if (writeProcSysFile("/proc/sys/net/core/bpf_jit_enable", "1\n")) return 1;
- // Enable JIT kallsyms export for privileged users only
- // (Note: this (open) will fail with ENOENT 'No such file or directory' if
- // kernel does not have CONFIG_HAVE_EBPF_JIT=y)
- if (writeProcSysFile("/proc/sys/net/core/bpf_jit_kallsyms", "1\n")) return 1;
+ // Enable JIT kallsyms export for privileged users only
+ // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+ // kernel does not have CONFIG_HAVE_EBPF_JIT=y)
+ if (writeProcSysFile("/proc/sys/net/core/bpf_jit_kallsyms", "1\n")) return 1;
+ }
// Create all the pin subdirectories
// (this must be done first to allow selinux_context and pin_subdir functionality,
@@ -278,13 +242,6 @@
if (createSysFsBpfSubDir(location.prefix)) return 1;
}
- // Note: there's no actual src dir for fs_bpf_loader .o's,
- // so it is not listed in 'locations[].prefix'.
- // This is because this is primarily meant for triggering genfscon rules,
- // and as such this will likely always be the case.
- // Thus we need to manually create the /sys/fs/bpf/loader subdirectory.
- if (createSysFsBpfSubDir("loader")) return 1;
-
// Load all ELF objects, create programs and maps, and pin them
for (const auto& location : locations) {
if (loadAllElfObjects(location) != 0) {
@@ -307,10 +264,12 @@
return 1;
}
- if (android::base::SetProperty("bpf.progs_loaded", "1") == false) {
- ALOGE("Failed to set bpf.progs_loaded property");
- return 1;
+ ALOGI("done, transferring control to platform bpfloader.");
+
+ const char * args[] = { "/system/bin/bpfloader", NULL, };
+ if (execve(args[0], (char**)args, envp)) {
+ ALOGE("FATAL: execve('/system/bin/bpfloader'): %d[%s]", errno, strerror(errno));
}
- return 0;
+ return 1;
}
diff --git a/netbpfload/initrc-doc/README.txt b/netbpfload/initrc-doc/README.txt
new file mode 100644
index 0000000..42e1fc2
--- /dev/null
+++ b/netbpfload/initrc-doc/README.txt
@@ -0,0 +1,62 @@
+This directory contains comment stripped versions of
+ //system/bpf/bpfloader/bpfloader.rc
+from previous versions of Android.
+
+Generated via:
+ (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android11-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+ (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android12-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+ (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android13-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+ (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android14-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+ (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/main:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
+
+this is entirely equivalent to:
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/main:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
+
+it is also equivalent to:
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-v2-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+
+ie. there were no changes between R/S/T and R/S/T QPR3, and no change between U and U QPR1.
+
+Note: Sv2 sdk/api level is actually 32, it just didn't change anything wrt. bpf, so doesn't matter.
+
+
+Key takeaways:
+
+= R bpfloader:
+ - CHOWN + SYS_ADMIN
+ - asynchronous startup
+ - platform only
+ - proc file setup handled by initrc
+
+= S bpfloader
+ - adds NET_ADMIN
+ - synchronous startup
+ - platform + mainline tethering offload
+
+= T bpfloader
+ - platform + mainline networking (including tethering offload)
+ - supported btf for maps via exec of btfloader
+
+= U bpfloader
+ - proc file setup moved into bpfloader binary
+ - explicitly specified user and groups:
+ group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+ user root
+
+= U QPR2 bpfloader
+ - drops support of btf for maps
+ - invocation of /system/bin/netbpfload binary, which after handling *all*
+ networking bpf related things executes the platform /system/bin/bpfloader
+ which handles non-networking bpf.
+
+Note that there is now a copy of 'netbpfload' provided by the tethering apex
+mainline module at /apex/com.android.tethering/bin/netbpfload, which due
+to the use of execve("/system/bin/bpfloader") relies on T+ selinux which was
+added for btf map support (specifically the ability to exec the "btfloader").
diff --git a/netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc b/netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc
new file mode 100644
index 0000000..482a7db
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc
@@ -0,0 +1,11 @@
+on load_bpf_programs
+ write /proc/sys/net/core/bpf_jit_enable 1
+ write /proc/sys/net/core/bpf_jit_kallsyms 1
+ start bpfloader
+
+service bpfloader /system/bin/bpfloader
+ capabilities CHOWN SYS_ADMIN
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+ updatable
diff --git a/netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc b/netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc
new file mode 100644
index 0000000..4117887
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc
@@ -0,0 +1,11 @@
+on load_bpf_programs
+ write /proc/sys/net/core/bpf_jit_enable 1
+ write /proc/sys/net/core/bpf_jit_kallsyms 1
+ exec_start bpfloader
+
+service bpfloader /system/bin/bpfloader
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+ updatable
diff --git a/netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc b/netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc
new file mode 100644
index 0000000..f0b6700
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc
@@ -0,0 +1,12 @@
+on load_bpf_programs
+ write /proc/sys/kernel/unprivileged_bpf_disabled 0
+ write /proc/sys/net/core/bpf_jit_enable 1
+ write /proc/sys/net/core/bpf_jit_kallsyms 1
+ exec_start bpfloader
+
+service bpfloader /system/bin/bpfloader
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+ updatable
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
new file mode 100644
index 0000000..8f3f462
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
@@ -0,0 +1,11 @@
+on load_bpf_programs
+ exec_start bpfloader
+
+service bpfloader /system/bin/netbpfload
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+ user root
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+ updatable
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc b/netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc
new file mode 100644
index 0000000..592303e
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc
@@ -0,0 +1,11 @@
+on load_bpf_programs
+ exec_start bpfloader
+
+service bpfloader /system/bin/bpfloader
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+ user root
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+ updatable
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
index 3bb758b..c534b2c 100644
--- a/netbpfload/loader.cpp
+++ b/netbpfload/loader.cpp
@@ -43,7 +43,7 @@
#include "BpfSyscallWrappers.h"
#include "bpf/BpfUtils.h"
#include "bpf/bpf_map_def.h"
-#include "include/libbpf_android.h"
+#include "loader.h"
#if BPFLOADER_VERSION < COMPILE_FOR_BPFLOADER_VERSION
#error "BPFLOADER_VERSION is less than COMPILE_FOR_BPFLOADER_VERSION"
@@ -59,9 +59,9 @@
#include <android-base/cmsg.h>
#include <android-base/file.h>
+#include <android-base/properties.h>
#include <android-base/strings.h>
#include <android-base/unique_fd.h>
-#include <cutils/properties.h>
#define BPF_FS_PATH "/sys/fs/bpf/"
@@ -79,17 +79,11 @@
using std::string;
using std::vector;
-static std::string getBuildTypeInternal() {
- char value[PROPERTY_VALUE_MAX] = {};
- (void)property_get("ro.build.type", value, "unknown"); // ignore length
- return value;
-}
-
namespace android {
namespace bpf {
const std::string& getBuildType() {
- static std::string t = getBuildTypeInternal();
+ static std::string t = android::base::GetProperty("ro.build.type", "unknown");
return t;
}
@@ -178,6 +172,10 @@
*
* However, be aware that you should not be directly using the SECTION() macro.
* Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE/CRITICAL macros.
+ *
+ * Programs shipped inside the tethering apex should be limited to networking stuff,
+ * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
+ * since they are less stable abi/api and may conflict with platform uses of bpf.
*/
sectionType sectionNameTypes[] = {
{"bind4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
@@ -189,13 +187,10 @@
{"egress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_EGRESS},
{"getsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_GETSOCKOPT},
{"ingress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_INGRESS},
- {"kprobe/", BPF_PROG_TYPE_KPROBE, BPF_ATTACH_TYPE_UNSPEC},
- {"kretprobe/", BPF_PROG_TYPE_KPROBE, BPF_ATTACH_TYPE_UNSPEC},
{"lwt_in/", BPF_PROG_TYPE_LWT_IN, BPF_ATTACH_TYPE_UNSPEC},
{"lwt_out/", BPF_PROG_TYPE_LWT_OUT, BPF_ATTACH_TYPE_UNSPEC},
{"lwt_seg6local/", BPF_PROG_TYPE_LWT_SEG6LOCAL, BPF_ATTACH_TYPE_UNSPEC},
{"lwt_xmit/", BPF_PROG_TYPE_LWT_XMIT, BPF_ATTACH_TYPE_UNSPEC},
- {"perf_event/", BPF_PROG_TYPE_PERF_EVENT, BPF_ATTACH_TYPE_UNSPEC},
{"postbind4/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET4_POST_BIND},
{"postbind6/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET6_POST_BIND},
{"recvmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
@@ -208,9 +203,6 @@
{"skfilter/", BPF_PROG_TYPE_SOCKET_FILTER, BPF_ATTACH_TYPE_UNSPEC},
{"sockops/", BPF_PROG_TYPE_SOCK_OPS, BPF_CGROUP_SOCK_OPS},
{"sysctl", BPF_PROG_TYPE_CGROUP_SYSCTL, BPF_CGROUP_SYSCTL},
- {"tracepoint/", BPF_PROG_TYPE_TRACEPOINT, BPF_ATTACH_TYPE_UNSPEC},
- {"uprobe/", BPF_PROG_TYPE_KPROBE, BPF_ATTACH_TYPE_UNSPEC},
- {"uretprobe/", BPF_PROG_TYPE_KPROBE, BPF_ATTACH_TYPE_UNSPEC},
{"xdp/", BPF_PROG_TYPE_XDP, BPF_ATTACH_TYPE_UNSPEC},
};
@@ -393,19 +385,10 @@
return 0;
}
-static enum bpf_prog_type getFuseProgType() {
- int result = BPF_PROG_TYPE_UNSPEC;
- ifstream("/sys/fs/fuse/bpf_prog_type_fuse") >> result;
- return static_cast<bpf_prog_type>(result);
-}
-
static enum bpf_prog_type getSectionType(string& name) {
for (auto& snt : sectionNameTypes)
if (StartsWith(name, snt.name)) return snt.type;
- // TODO Remove this code when fuse-bpf is upstream and this BPF_PROG_TYPE_FUSE is fixed
- if (StartsWith(name, "fuse/")) return getFuseProgType();
-
return BPF_PROG_TYPE_UNSPEC;
}
@@ -415,6 +398,7 @@
return BPF_ATTACH_TYPE_UNSPEC;
}
+/*
static string getSectionName(enum bpf_prog_type type)
{
for (auto& snt : sectionNameTypes)
@@ -423,6 +407,7 @@
return "UNKNOWN SECTION NAME " + std::to_string(type);
}
+*/
static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd,
size_t sizeOfBpfProgDef) {
@@ -502,22 +487,8 @@
return 0;
}
-static bool IsAllowed(bpf_prog_type type, const bpf_prog_type* allowed, size_t numAllowed) {
- if (allowed == nullptr) return true;
-
- for (size_t i = 0; i < numAllowed; i++) {
- if (allowed[i] == BPF_PROG_TYPE_UNSPEC) {
- if (type == getFuseProgType()) return true;
- } else if (type == allowed[i])
- return true;
- }
-
- return false;
-}
-
/* Read a section by its index - for ex to get sec hdr strtab blob */
-static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs, size_t sizeOfBpfProgDef,
- const bpf_prog_type* allowed, size_t numAllowed) {
+static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs, size_t sizeOfBpfProgDef) {
vector<Elf64_Shdr> shTable;
int entries, ret = 0;
@@ -544,11 +515,6 @@
if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
- if (!IsAllowed(ptype, allowed, numAllowed)) {
- ALOGE("Program type %s not permitted here", getSectionName(ptype).c_str());
- return -1;
- }
-
// This must be done before '/' is replaced with '_'.
cs_temp.expected_attach_type = getExpectedAttachType(name);
@@ -655,8 +621,7 @@
}
static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
- const char* prefix, const unsigned long long allowedDomainBitmask,
- const size_t sizeOfBpfMapDef) {
+ const char* prefix, const size_t sizeOfBpfMapDef) {
int ret;
vector<char> mdData;
vector<struct bpf_map_def> md;
@@ -767,11 +732,6 @@
domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
if (specified(selinux_context)) {
- if (!inDomainBitmask(selinux_context, allowedDomainBitmask)) {
- ALOGE("map %s has invalid selinux_context of %d (allowed bitmask 0x%llx)",
- mapNames[i].c_str(), selinux_context, allowedDomainBitmask);
- return -EINVAL;
- }
ALOGI("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
md[i].selinux_context, selinux_context, lookupSelinuxContext(selinux_context),
lookupPinSubdir(selinux_context));
@@ -780,11 +740,6 @@
domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
if (unrecognized(pin_subdir)) return -ENOTDIR;
if (specified(pin_subdir)) {
- if (!inDomainBitmask(pin_subdir, allowedDomainBitmask)) {
- ALOGE("map %s has invalid pin_subdir of %d (allowed bitmask 0x%llx)",
- mapNames[i].c_str(), pin_subdir, allowedDomainBitmask);
- return -EINVAL;
- }
ALOGI("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
pin_subdir, lookupPinSubdir(pin_subdir));
}
@@ -955,7 +910,7 @@
}
static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
- const char* prefix, const unsigned long long allowedDomainBitmask) {
+ const char* prefix) {
unsigned kvers = kernelVersion();
if (!kvers) {
@@ -1014,22 +969,12 @@
if (unrecognized(pin_subdir)) return -ENOTDIR;
if (specified(selinux_context)) {
- if (!inDomainBitmask(selinux_context, allowedDomainBitmask)) {
- ALOGE("prog %s has invalid selinux_context of %d (allowed bitmask 0x%llx)",
- name.c_str(), selinux_context, allowedDomainBitmask);
- return -EINVAL;
- }
ALOGI("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
cs[i].prog_def->selinux_context, selinux_context,
lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
}
if (specified(pin_subdir)) {
- if (!inDomainBitmask(pin_subdir, allowedDomainBitmask)) {
- ALOGE("prog %s has invalid pin_subdir of %d (allowed bitmask 0x%llx)", name.c_str(),
- pin_subdir, allowedDomainBitmask);
- return -EINVAL;
- }
ALOGI("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
cs[i].prog_def->pin_subdir, pin_subdir, lookupPinSubdir(pin_subdir));
}
@@ -1210,8 +1155,7 @@
return -1;
}
- ret = readCodeSections(elfFile, cs, sizeOfBpfProgDef, location.allowedProgTypes,
- location.allowedProgTypesLength);
+ ret = readCodeSections(elfFile, cs, sizeOfBpfProgDef);
if (ret) {
ALOGE("Couldn't read all code sections in %s", elfPath);
return ret;
@@ -1220,8 +1164,7 @@
/* Just for future debugging */
if (0) dumpAllCs(cs);
- ret = createMaps(elfPath, elfFile, mapFds, location.prefix, location.allowedDomainBitmask,
- sizeOfBpfMapDef);
+ ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef);
if (ret) {
ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
return ret;
@@ -1232,8 +1175,7 @@
applyMapRelo(elfFile, mapFds, cs);
- ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix,
- location.allowedDomainBitmask);
+ ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix);
if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
return ret;
diff --git a/netbpfload/loader.h b/netbpfload/loader.h
index a47e4da..b884637 100644
--- a/netbpfload/loader.h
+++ b/netbpfload/loader.h
@@ -64,20 +64,9 @@
return d != domain::unspecified;
}
-static constexpr unsigned long long domainToBitmask(domain d) {
- return specified(d) ? 1uLL << (static_cast<int>(d) - 1) : 0;
-}
-
-static constexpr bool inDomainBitmask(domain d, unsigned long long v) {
- return domainToBitmask(d) & v;
-}
-
struct Location {
const char* const dir = "";
const char* const prefix = "";
- unsigned long long allowedDomainBitmask = 0;
- const bpf_prog_type* allowedProgTypes = nullptr;
- size_t allowedProgTypesLength = 0;
};
// BPF loader implementation. Loads an eBPF ELF object
diff --git a/netbpfload/netbpfload.rc b/netbpfload/netbpfload.rc
index 20fbb9f..14181dc 100644
--- a/netbpfload/netbpfload.rc
+++ b/netbpfload/netbpfload.rc
@@ -3,7 +3,7 @@
# a tad earlier. There's no benefit to that though, since on 4.9+ P+ devices netd
# will just block until bpfloader finishes and sets the bpf.progs_loaded property.
#
-# It is important that we start netbpfload after:
+# It is important that we start bpfloader after:
# - /sys/fs/bpf is already mounted,
# - apex (incl. rollback) is initialized (so that in the future we can load bpf
# programs shipped as part of apex mainline modules)
@@ -15,9 +15,10 @@
# considered to have booted successfully.
#
on load_bpf_programs
- exec_start netbpfload
+ exec_start bpfloader
-service netbpfload /system/bin/netbpfload
+service bpfloader /system/bin/netbpfload
+ # netbpfload will do network bpf loading, then execute /system/bin/bpfloader
capabilities CHOWN SYS_ADMIN NET_ADMIN
# The following group memberships are a workaround for lack of DAC_OVERRIDE
# and allow us to open (among other things) files that we created and are
@@ -27,28 +28,28 @@
group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
user root
#
- # Set RLIMIT_MEMLOCK to 1GiB for netbpfload
+ # Set RLIMIT_MEMLOCK to 1GiB for bpfloader
#
- # Actually only 8MiB would be needed if netbpfload ran as its own uid.
+ # Actually only 8MiB would be needed if bpfloader ran as its own uid.
#
# However, while the rlimit is per-thread, the accounting is system wide.
# So, for example, if the graphics stack has already allocated 10MiB of
- # memlock data before netbpfload even gets a chance to run, it would fail
+ # memlock data before bpfloader even gets a chance to run, it would fail
# if its memlock rlimit is only 8MiB - since there would be none left for it.
#
- # netbpfload succeeding is critical to system health, since a failure will
+ # bpfloader succeeding is critical to system health, since a failure will
# cause netd crashloop and thus system server crashloop... and the only
# recovery is a full kernel reboot.
#
# We've had issues where devices would sometimes (rarely) boot into
- # a crashloop because netbpfload would occasionally lose a boot time
+ # a crashloop because bpfloader would occasionally lose a boot time
# race against the graphics stack's boot time locked memory allocation.
#
- # Thus netbpfload's memlock has to be 8MB higher then the locked memory
+ # Thus bpfloader's memlock has to be 8MB higher then the locked memory
# consumption of the root uid anywhere else in the system...
# But we don't know what that is for all possible devices...
#
- # Ideally, we'd simply grant netbpfload the IPC_LOCK capability and it
+ # Ideally, we'd simply grant bpfloader the IPC_LOCK capability and it
# would simply ignore it's memlock rlimit... but it turns that this
# capability is not even checked by the kernel's bpf system call.
#
@@ -57,29 +58,29 @@
rlimit memlock 1073741824 1073741824
oneshot
#
- # How to debug bootloops caused by 'netbpfload-failed'.
+ # How to debug bootloops caused by 'bpfloader-failed'.
#
# 1. On some lower RAM devices (like wembley) you may need to first enable developer mode
# (from the Settings app UI), and change the developer option "Logger buffer sizes"
# from the default (wembley: 64kB) to the maximum (1M) per log buffer.
# Otherwise buffer will overflow before you manage to dump it and you'll get useless logs.
#
- # 2. comment out 'reboot_on_failure reboot,netbpfload-failed' below
+ # 2. comment out 'reboot_on_failure reboot,bpfloader-failed' below
# 3. rebuild/reflash/reboot
- # 4. as the device is booting up capture netbpfload logs via:
- # adb logcat -s 'NetBpfLoad:*' 'NetBpfLoader:*'
+ # 4. as the device is booting up capture bpfloader logs via:
+ # adb logcat -s 'bpfloader:*' 'LibBpfLoader:*' 'NetBpfLoad:*' 'NetBpfLoader:*'
#
# something like:
- # $ adb reboot; sleep 1; adb wait-for-device; adb root; sleep 1; adb wait-for-device; adb logcat -s 'NetBpfLoad:*' 'NetBpfLoader:*'
+ # $ adb reboot; sleep 1; adb wait-for-device; adb root; sleep 1; adb wait-for-device; adb logcat -s 'bpfloader:*' 'LibBpfLoader:*' 'NetBpfLoad:*' 'NetBpfLoader:*'
# will take care of capturing logs as early as possible
#
- # 5. look through the logs from the kernel's bpf verifier that netbpfload dumps out,
+ # 5. look through the logs from the kernel's bpf verifier that bpfloader dumps out,
# it usually makes sense to search back from the end and find the particular
- # bpf verifier failure that caused netbpfload to terminate early with an error code.
+ # bpf verifier failure that caused bpfloader to terminate early with an error code.
# This will probably be something along the lines of 'too many jumps' or
# 'cannot prove return value is 0 or 1' or 'unsupported / unknown operation / helper',
# 'invalid bpf_context access', etc.
#
- reboot_on_failure reboot,netbpfload-failed
+ reboot_on_failure reboot,bpfloader-failed
# we're not really updatable, but want to be able to load bpf programs shipped in apexes
updatable
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index fa92f10..a7a4059 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -130,12 +130,21 @@
attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
}
- // This should trivially pass, since we just attached up above,
- // but BPF_PROG_QUERY is only implemented on 4.19+ kernels.
if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ RETURN_IF_NOT_OK(attachProgramToCgroup(
+ "/sys/fs/bpf/netd_readonly/prog_block_bind4_block_port",
+ cg_fd, BPF_CGROUP_INET4_BIND));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(
+ "/sys/fs/bpf/netd_readonly/prog_block_bind6_block_port",
+ cg_fd, BPF_CGROUP_INET6_BIND));
+
+ // This should trivially pass, since we just attached up above,
+ // but BPF_PROG_QUERY is only implemented on 4.19+ kernels.
if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
}
return netdutils::status::ok;
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 16a8242..a21c033 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -20,7 +20,7 @@
name: "RemoteAuthUnitTests",
defaults: [
"enable-remoteauth-targets",
- "mts-target-sdk-version-current"
+ "mts-target-sdk-version-current",
],
sdk_version: "test_current",
min_sdk_version: "31",
@@ -45,7 +45,7 @@
"mockito-target-extended-minus-junit4",
"platform-test-annotations",
"service-remoteauth-pre-jarjar",
- "truth-prebuilt",
+ "truth",
],
// these are needed for Extended Mockito
jni_libs: [
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 7e2d2f4..bc49f0e 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -102,7 +102,12 @@
],
exclude_srcs: [
"src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java",
- "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java"
+ "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java",
+ "src/com/android/server/connectivity/mdns/MdnsAdvertiser.java",
+ "src/com/android/server/connectivity/mdns/MdnsAnnouncer.java",
+ "src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java",
+ "src/com/android/server/connectivity/mdns/MdnsProber.java",
+ "src/com/android/server/connectivity/mdns/MdnsRecordRepository.java",
],
static_libs: [
"net-utils-device-common-mdns-standalone-build-test",
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 93ccb85..cc3f019 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -1703,15 +1703,20 @@
am.addOnUidImportanceListener(new UidImportanceListener(handler),
mRunningAppActiveImportanceCutoff);
+ final MdnsFeatureFlags flags = new MdnsFeatureFlags.Builder()
+ .setIsMdnsOffloadFeatureEnabled(mDeps.isTetheringFeatureNotChickenedOut(
+ mContext, MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD))
+ .setIncludeInetAddressRecordsInProbing(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING))
+ .setIsExpiredServicesRemovalEnabled(mDeps.isTrunkStableFeatureEnabled(
+ MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
+ .build();
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
LOGGER.forSubComponent("MdnsMultinetworkSocketClient"));
mMdnsDiscoveryManager = deps.makeMdnsDiscoveryManager(new ExecutorProvider(),
- mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"));
+ mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"), flags);
handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
- MdnsFeatureFlags flags = new MdnsFeatureFlags.Builder().setIsMdnsOffloadFeatureEnabled(
- mDeps.isTetheringFeatureNotChickenedOut(mContext,
- MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD)).build();
mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider,
new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"), flags);
mClock = deps.makeClock();
@@ -1769,12 +1774,21 @@
}
/**
+ * @see DeviceConfigUtils#isTrunkStableFeatureEnabled
+ */
+ public boolean isTrunkStableFeatureEnabled(String feature) {
+ return DeviceConfigUtils.isTrunkStableFeatureEnabled(feature);
+ }
+
+ /**
* @see MdnsDiscoveryManager
*/
public MdnsDiscoveryManager makeMdnsDiscoveryManager(
@NonNull ExecutorProvider executorProvider,
- @NonNull MdnsMultinetworkSocketClient socketClient, @NonNull SharedLog sharedLog) {
- return new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog);
+ @NonNull MdnsMultinetworkSocketClient socketClient, @NonNull SharedLog sharedLog,
+ @NonNull MdnsFeatureFlags featureFlags) {
+ return new MdnsDiscoveryManager(
+ executorProvider, socketClient, sharedLog, featureFlags);
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index a946bca..28e3924 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -96,10 +96,11 @@
@NonNull Looper looper, @NonNull byte[] packetCreationBuffer,
@NonNull MdnsInterfaceAdvertiser.Callback cb,
@NonNull String[] deviceHostName,
- @NonNull SharedLog sharedLog) {
+ @NonNull SharedLog sharedLog,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
// Note NetworkInterface is final and not mockable
return new MdnsInterfaceAdvertiser(socket, initialAddresses, looper,
- packetCreationBuffer, cb, deviceHostName, sharedLog);
+ packetCreationBuffer, cb, deviceHostName, sharedLog, mdnsFeatureFlags);
}
/**
@@ -394,7 +395,8 @@
if (advertiser == null) {
advertiser = mDeps.makeAdvertiser(socket, addresses, mLooper, mPacketCreationBuffer,
mInterfaceAdvertiserCb, mDeviceHostName,
- mSharedLog.forSubComponent(socket.getInterface().getName()));
+ mSharedLog.forSubComponent(socket.getInterface().getName()),
+ mMdnsFeatureFlags);
mAllAdvertisers.put(socket, advertiser);
advertiser.start();
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
index d9bc643..5812797 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
@@ -110,7 +110,7 @@
@NonNull MdnsReplySender replySender,
@Nullable PacketRepeaterCallback<BaseAnnouncementInfo> cb,
@NonNull SharedLog sharedLog) {
- super(looper, replySender, cb, sharedLog);
+ super(looper, replySender, cb, sharedLog, MdnsAdvertiser.DBG);
}
// TODO: Notify MdnsRecordRepository that the records were announced for that service ID,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
index 1251170..b83a6a0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
@@ -19,6 +19,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import java.net.InetAddress;
+import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
@@ -42,6 +43,10 @@
public static final String SUBTYPE_PREFIX = "_";
private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
private static final String MDNS_IPV6_HOST_ADDRESS = "FF02::FB";
+ public static final InetSocketAddress IPV6_SOCKET_ADDR = new InetSocketAddress(
+ getMdnsIPv6Address(), MDNS_PORT);
+ public static final InetSocketAddress IPV4_SOCKET_ADDR = new InetSocketAddress(
+ getMdnsIPv4Address(), MDNS_PORT);
private static InetAddress mdnsAddress;
private MdnsConstants() {
}
@@ -75,4 +80,4 @@
public static Charset getUtf8Charset() {
return UTF_8;
}
-}
\ No newline at end of file
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 24e9fa8..766f999 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -53,6 +53,7 @@
@NonNull private final Handler handler;
@Nullable private final HandlerThread handlerThread;
@NonNull private final MdnsServiceCache serviceCache;
+ @NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
private static class PerSocketServiceTypeClients {
private final ArrayMap<Pair<String, SocketKey>, MdnsServiceTypeClient> clients =
@@ -117,20 +118,22 @@
}
public MdnsDiscoveryManager(@NonNull ExecutorProvider executorProvider,
- @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog) {
+ @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
this.executorProvider = executorProvider;
this.socketClient = socketClient;
this.sharedLog = sharedLog;
this.perSocketServiceTypeClients = new PerSocketServiceTypeClients();
+ this.mdnsFeatureFlags = mdnsFeatureFlags;
if (socketClient.getLooper() != null) {
this.handlerThread = null;
this.handler = new Handler(socketClient.getLooper());
- this.serviceCache = new MdnsServiceCache(socketClient.getLooper());
+ this.serviceCache = new MdnsServiceCache(socketClient.getLooper(), mdnsFeatureFlags);
} else {
this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName());
this.handlerThread.start();
this.handler = new Handler(handlerThread.getLooper());
- this.serviceCache = new MdnsServiceCache(handlerThread.getLooper());
+ this.serviceCache = new MdnsServiceCache(handlerThread.getLooper(), mdnsFeatureFlags);
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 9840409..6f7645e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -20,18 +20,39 @@
*/
public class MdnsFeatureFlags {
/**
- * The feature flag for control whether the mDNS offload is enabled or not.
+ * A feature flag to control whether the mDNS offload is enabled or not.
*/
public static final String NSD_FORCE_DISABLE_MDNS_OFFLOAD = "nsd_force_disable_mdns_offload";
+ /**
+ * A feature flag to control whether the probing question should include
+ * InetAddressRecords or not.
+ */
+ public static final String INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING =
+ "include_inet_address_records_in_probing";
+ /**
+ * A feature flag to control whether expired services removal should be enabled.
+ */
+ public static final String NSD_EXPIRED_SERVICES_REMOVAL =
+ "nsd_expired_services_removal";
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
+ // Flag for including InetAddressRecords in probing questions.
+ public final boolean mIncludeInetAddressRecordsInProbing;
+
+ // Flag for expired services removal
+ public final boolean mIsExpiredServicesRemovalEnabled;
+
/**
* The constructor for {@link MdnsFeatureFlags}.
*/
- public MdnsFeatureFlags(boolean isOffloadFeatureEnabled) {
+ public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
+ boolean includeInetAddressRecordsInProbing, boolean isExpiredServicesRemovalEnabled) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
+ mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
+ mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
}
@@ -44,16 +65,22 @@
public static final class Builder {
private boolean mIsMdnsOffloadFeatureEnabled;
+ private boolean mIncludeInetAddressRecordsInProbing;
+ private boolean mIsExpiredServicesRemovalEnabled;
/**
* The constructor for {@link Builder}.
*/
public Builder() {
mIsMdnsOffloadFeatureEnabled = false;
+ mIncludeInetAddressRecordsInProbing = false;
+ mIsExpiredServicesRemovalEnabled = true; // Default enabled.
}
/**
- * Set if the mDNS offload feature is enabled.
+ * Set whether the mDNS offload feature is enabled.
+ *
+ * @see #NSD_FORCE_DISABLE_MDNS_OFFLOAD
*/
public Builder setIsMdnsOffloadFeatureEnabled(boolean isMdnsOffloadFeatureEnabled) {
mIsMdnsOffloadFeatureEnabled = isMdnsOffloadFeatureEnabled;
@@ -61,11 +88,32 @@
}
/**
+ * Set whether the probing question should include InetAddressRecords.
+ *
+ * @see #INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING
+ */
+ public Builder setIncludeInetAddressRecordsInProbing(
+ boolean includeInetAddressRecordsInProbing) {
+ mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
+ return this;
+ }
+
+ /**
+ * Set whether the expired services removal is enabled.
+ *
+ * @see #NSD_EXPIRED_SERVICES_REMOVAL
+ */
+ public Builder setIsExpiredServicesRemovalEnabled(boolean isExpiredServicesRemovalEnabled) {
+ mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
- return new MdnsFeatureFlags(mIsMdnsOffloadFeatureEnabled);
+ return new MdnsFeatureFlags(mIsMdnsOffloadFeatureEnabled,
+ mIncludeInetAddressRecordsInProbing, mIsExpiredServicesRemovalEnabled);
}
-
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
index dd8a526..973fd96 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
@@ -18,7 +18,7 @@
import android.annotation.Nullable;
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
import java.io.IOException;
import java.net.Inet4Address;
@@ -29,7 +29,7 @@
import java.util.Objects;
/** An mDNS "AAAA" or "A" record, which holds an IPv6 or IPv4 address. */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class MdnsInetAddressRecord extends MdnsRecord {
@Nullable private Inet6Address inet6Address;
@Nullable private Inet4Address inet4Address;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 40dfd57..e07d380 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -150,8 +150,8 @@
/** @see MdnsRecordRepository */
@NonNull
public MdnsRecordRepository makeRecordRepository(@NonNull Looper looper,
- @NonNull String[] deviceHostName) {
- return new MdnsRecordRepository(looper, deviceHostName);
+ @NonNull String[] deviceHostName, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+ return new MdnsRecordRepository(looper, deviceHostName, mdnsFeatureFlags);
}
/** @see MdnsReplySender */
@@ -161,7 +161,7 @@
@NonNull SharedLog sharedLog) {
return new MdnsReplySender(looper, socket, packetCreationBuffer,
sharedLog.forSubComponent(
- MdnsReplySender.class.getSimpleName() + "/" + interfaceTag));
+ MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG);
}
/** @see MdnsAnnouncer */
@@ -187,16 +187,18 @@
public MdnsInterfaceAdvertiser(@NonNull MdnsInterfaceSocket socket,
@NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper,
@NonNull byte[] packetCreationBuffer, @NonNull Callback cb,
- @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog) {
+ @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
this(socket, initialAddresses, looper, packetCreationBuffer, cb,
- new Dependencies(), deviceHostName, sharedLog);
+ new Dependencies(), deviceHostName, sharedLog, mdnsFeatureFlags);
}
public MdnsInterfaceAdvertiser(@NonNull MdnsInterfaceSocket socket,
@NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper,
@NonNull byte[] packetCreationBuffer, @NonNull Callback cb, @NonNull Dependencies deps,
- @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog) {
- mRecordRepository = deps.makeRecordRepository(looper, deviceHostName);
+ @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+ mRecordRepository = deps.makeRecordRepository(looper, deviceHostName, mdnsFeatureFlags);
mRecordRepository.updateAddresses(initialAddresses);
mSocket = socket;
mCb = cb;
@@ -370,7 +372,7 @@
// happen when the incoming packet has answer records (not a question), so there will be no
// answer. One exception is simultaneous probe tiebreaking (rfc6762 8.2), in which case the
// conflicting service is still probing and won't reply either.
- final MdnsRecordRepository.ReplyInfo answers = mRecordRepository.getReply(packet, src);
+ final MdnsReplyInfo answers = mRecordRepository.getReply(packet, src);
if (answers == null) return;
mReplySender.queueReply(answers);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
index fd0f5c9..e84cead 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
@@ -16,8 +16,8 @@
package com.android.server.connectivity.mdns;
-import static com.android.server.connectivity.mdns.MdnsRecordRepository.IPV4_ADDR;
-import static com.android.server.connectivity.mdns.MdnsRecordRepository.IPV6_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -38,9 +38,8 @@
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public abstract class MdnsPacketRepeater<T extends MdnsPacketRepeater.Request> {
- private static final boolean DBG = MdnsAdvertiser.DBG;
private static final InetSocketAddress[] ALL_ADDRS = new InetSocketAddress[] {
- IPV4_ADDR, IPV6_ADDR
+ IPV4_SOCKET_ADDR, IPV6_SOCKET_ADDR
};
@NonNull
@@ -51,6 +50,7 @@
private final PacketRepeaterCallback<T> mCb;
@NonNull
private final SharedLog mSharedLog;
+ private final boolean mEnableDebugLog;
/**
* Status callback from {@link MdnsPacketRepeater}.
@@ -111,7 +111,7 @@
}
final MdnsPacket packet = request.getPacket(index);
- if (DBG) {
+ if (mEnableDebugLog) {
mSharedLog.v("Sending packets for iteration " + index + " out of "
+ request.getNumSends() + " for ID " + msg.what);
}
@@ -134,7 +134,7 @@
// likely not to be available since the device is in deep sleep anyway.
final long delay = request.getDelayMs(nextIndex);
sendMessageDelayed(obtainMessage(msg.what, nextIndex, 0, request), delay);
- if (DBG) mSharedLog.v("Scheduled next packet in " + delay + "ms");
+ if (mEnableDebugLog) mSharedLog.v("Scheduled next packet in " + delay + "ms");
}
// Call onSent after scheduling the next run, to allow the callback to cancel it
@@ -145,15 +145,17 @@
}
protected MdnsPacketRepeater(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
- @Nullable PacketRepeaterCallback<T> cb, @NonNull SharedLog sharedLog) {
+ @Nullable PacketRepeaterCallback<T> cb, @NonNull SharedLog sharedLog,
+ boolean enableDebugLog) {
mHandler = new ProbeHandler(looper);
mReplySender = replySender;
mCb = cb;
mSharedLog = sharedLog;
+ mEnableDebugLog = enableDebugLog;
}
protected void startSending(int id, @NonNull T request, long initialDelayMs) {
- if (DBG) {
+ if (mEnableDebugLog) {
mSharedLog.v("Starting send with id " + id + ", request "
+ request.getClass().getSimpleName() + ", delay " + initialDelayMs);
}
@@ -172,7 +174,7 @@
// all in the handler queue; unless this method is called from a message, but the current
// message cannot be cancelled.
if (mHandler.hasMessages(id)) {
- if (DBG) {
+ if (mEnableDebugLog) {
mSharedLog.v("Stopping send on id " + id);
}
mHandler.removeMessages(id);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
index c88ead0..41cc380 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -18,14 +18,15 @@
import android.annotation.Nullable;
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.util.Arrays;
/** An mDNS "PTR" record, which holds a name (the "pointer"). */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class MdnsPointerRecord extends MdnsRecord {
private String[] pointer;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
index f2b562a..e88947a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
@@ -40,9 +40,8 @@
private static final long CONFLICT_RETRY_DELAY_MS = 5_000L;
public MdnsProber(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
- @NonNull PacketRepeaterCallback<ProbingInfo> cb,
- @NonNull SharedLog sharedLog) {
- super(looper, replySender, cb, sharedLog);
+ @NonNull PacketRepeaterCallback<ProbingInfo> cb, @NonNull SharedLog sharedLog) {
+ super(looper, replySender, cb, sharedLog, MdnsAdvertiser.DBG);
}
/** Probing request to send with {@link MdnsProber}. */
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index f532372..73c1758 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
import android.annotation.NonNull;
@@ -79,11 +81,6 @@
private static final String[] DNS_SD_SERVICE_TYPE =
new String[] { "_services", "_dns-sd", "_udp", LOCAL_TLD };
- public static final InetSocketAddress IPV6_ADDR = new InetSocketAddress(
- MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
- public static final InetSocketAddress IPV4_ADDR = new InetSocketAddress(
- MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
-
@NonNull
private final Random mDelayGenerator = new Random();
// Map of service unique ID -> records for service
@@ -95,16 +92,19 @@
private final Looper mLooper;
@NonNull
private final String[] mDeviceHostname;
+ private final MdnsFeatureFlags mMdnsFeatureFlags;
- public MdnsRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostname) {
- this(looper, new Dependencies(), deviceHostname);
+ public MdnsRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostname,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+ this(looper, new Dependencies(), deviceHostname, mdnsFeatureFlags);
}
@VisibleForTesting
public MdnsRecordRepository(@NonNull Looper looper, @NonNull Dependencies deps,
- @NonNull String[] deviceHostname) {
+ @NonNull String[] deviceHostname, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
mDeviceHostname = deviceHostname;
mLooper = looper;
+ mMdnsFeatureFlags = mdnsFeatureFlags;
}
/**
@@ -354,7 +354,8 @@
}
private MdnsProber.ProbingInfo makeProbingInfo(int serviceId,
- @NonNull MdnsServiceRecord srvRecord) {
+ @NonNull MdnsServiceRecord srvRecord,
+ @NonNull List<MdnsInetAddressRecord> inetAddressRecords) {
final List<MdnsRecord> probingRecords = new ArrayList<>();
// Probe with cacheFlush cleared; it is set when announcing, as it was verified unique:
// RFC6762 10.2
@@ -366,6 +367,15 @@
srvRecord.getServicePort(),
srvRecord.getServiceHost()));
+ for (MdnsInetAddressRecord inetAddressRecord : inetAddressRecords) {
+ probingRecords.add(new MdnsInetAddressRecord(inetAddressRecord.getName(),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ inetAddressRecord.getTtl(),
+ inetAddressRecord.getInet4Address() == null
+ ? inetAddressRecord.getInet6Address()
+ : inetAddressRecord.getInet4Address()));
+ }
return new MdnsProber.ProbingInfo(serviceId, probingRecords);
}
@@ -455,44 +465,13 @@
}
/**
- * Info about a reply to be sent.
- */
- public static class ReplyInfo {
- @NonNull
- public final List<MdnsRecord> answers;
- @NonNull
- public final List<MdnsRecord> additionalAnswers;
- public final long sendDelayMs;
- @NonNull
- public final InetSocketAddress destination;
-
- public ReplyInfo(
- @NonNull List<MdnsRecord> answers,
- @NonNull List<MdnsRecord> additionalAnswers,
- long sendDelayMs,
- @NonNull InetSocketAddress destination) {
- this.answers = answers;
- this.additionalAnswers = additionalAnswers;
- this.sendDelayMs = sendDelayMs;
- this.destination = destination;
- }
-
- @Override
- public String toString() {
- return "{ReplyInfo to " + destination + ", answers: " + answers.size()
- + ", additionalAnswers: " + additionalAnswers.size()
- + ", sendDelayMs " + sendDelayMs + "}";
- }
- }
-
- /**
* Get the reply to send to an incoming packet.
*
* @param packet The incoming packet.
* @param src The source address of the incoming packet.
*/
@Nullable
- public ReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
+ public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
final long now = SystemClock.elapsedRealtime();
final boolean replyUnicast = (packet.flags & MdnsConstants.QCLASS_UNICAST) != 0;
final ArrayList<MdnsRecord> additionalAnswerRecords = new ArrayList<>();
@@ -543,9 +522,9 @@
if (replyUnicast) {
dest = src;
} else if (src.getAddress() instanceof Inet4Address) {
- dest = IPV4_ADDR;
+ dest = IPV4_SOCKET_ADDR;
} else {
- dest = IPV6_ADDR;
+ dest = IPV6_SOCKET_ADDR;
}
// Build the list of answer records from their RecordInfo
@@ -559,7 +538,7 @@
answerRecords.add(info.record);
}
- return new ReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
+ return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
}
/**
@@ -858,6 +837,18 @@
return conflicting;
}
+ private List<MdnsInetAddressRecord> makeProbingInetAddressRecords() {
+ final List<MdnsInetAddressRecord> records = new ArrayList<>();
+ if (mMdnsFeatureFlags.mIncludeInetAddressRecordsInProbing) {
+ for (RecordInfo<?> record : mGeneralRecords) {
+ if (record.record instanceof MdnsInetAddressRecord) {
+ records.add((MdnsInetAddressRecord) record.record);
+ }
+ }
+ }
+ return records;
+ }
+
/**
* (Re)set a service to the probing state.
* @return The {@link MdnsProber.ProbingInfo} to send for probing.
@@ -868,7 +859,8 @@
if (registration == null) return null;
registration.setProbing(true);
- return makeProbingInfo(serviceId, registration.srvRecord.record);
+ return makeProbingInfo(
+ serviceId, registration.srvRecord.record, makeProbingInetAddressRecords());
}
/**
@@ -904,7 +896,8 @@
final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
existing.subtype, existing.repliedServiceCount, existing.sentPacketCount);
mServices.put(serviceId, newService);
- return makeProbingInfo(serviceId, newService.srvRecord.record);
+ return makeProbingInfo(
+ serviceId, newService.srvRecord.record, makeProbingInetAddressRecords());
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
new file mode 100644
index 0000000..ce61b54
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+
+import java.net.InetSocketAddress;
+import java.util.List;
+
+/**
+ * Info about a mDNS reply to be sent.
+ */
+public final class MdnsReplyInfo {
+ @NonNull
+ public final List<MdnsRecord> answers;
+ @NonNull
+ public final List<MdnsRecord> additionalAnswers;
+ public final long sendDelayMs;
+ @NonNull
+ public final InetSocketAddress destination;
+
+ public MdnsReplyInfo(
+ @NonNull List<MdnsRecord> answers,
+ @NonNull List<MdnsRecord> additionalAnswers,
+ long sendDelayMs,
+ @NonNull InetSocketAddress destination) {
+ this.answers = answers;
+ this.additionalAnswers = additionalAnswers;
+ this.sendDelayMs = sendDelayMs;
+ this.destination = destination;
+ }
+
+ @Override
+ public String toString() {
+ return "{MdnsReplyInfo to " + destination + ", answers: " + answers.size()
+ + ", additionalAnswers: " + additionalAnswers.size()
+ + ", sendDelayMs " + sendDelayMs + "}";
+ }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index 3d64b5a..abf5d99 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -26,7 +26,6 @@
import android.os.Message;
import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.MdnsRecordRepository.ReplyInfo;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
@@ -45,7 +44,6 @@
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class MdnsReplySender {
- private static final boolean DBG = MdnsAdvertiser.DBG;
private static final int MSG_SEND = 1;
private static final int PACKET_NOT_SENT = 0;
private static final int PACKET_SENT = 1;
@@ -58,24 +56,27 @@
private final byte[] mPacketCreationBuffer;
@NonNull
private final SharedLog mSharedLog;
+ private final boolean mEnableDebugLog;
public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
- @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog) {
+ @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
+ boolean enableDebugLog) {
mHandler = new SendHandler(looper);
mSocket = socket;
mPacketCreationBuffer = packetCreationBuffer;
mSharedLog = sharedLog;
+ mEnableDebugLog = enableDebugLog;
}
/**
* Queue a reply to be sent when its send delay expires.
*/
- public void queueReply(@NonNull ReplyInfo reply) {
+ public void queueReply(@NonNull MdnsReplyInfo reply) {
ensureRunningOnHandlerThread(mHandler);
// TODO: implement response aggregation (RFC 6762 6.4)
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
- if (DBG) {
+ if (mEnableDebugLog) {
mSharedLog.v("Scheduling " + reply);
}
}
@@ -118,8 +119,8 @@
@Override
public void handleMessage(@NonNull Message msg) {
- final ReplyInfo replyInfo = (ReplyInfo) msg.obj;
- if (DBG) mSharedLog.v("Sending " + replyInfo);
+ final MdnsReplyInfo replyInfo = (MdnsReplyInfo) msg.obj;
+ if (mEnableDebugLog) mSharedLog.v("Sending " + replyInfo);
final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
final MdnsPacket packet = new MdnsPacket(flags,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index ec6af9b..d3493c7 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -42,7 +42,7 @@
* to their default value (0, false or null).
*/
public class MdnsServiceCache {
- private static class CacheKey {
+ static class CacheKey {
@NonNull final String mLowercaseServiceType;
@NonNull final SocketKey mSocketKey;
@@ -72,27 +72,33 @@
*/
@NonNull
private final ArrayMap<CacheKey, List<MdnsResponse>> mCachedServices = new ArrayMap<>();
+ /**
+ * A map of service expire callbacks. Key is composed of service type and socket and value is
+ * the callback listener.
+ */
+ @NonNull
+ private final ArrayMap<CacheKey, ServiceExpiredCallback> mCallbacks = new ArrayMap<>();
@NonNull
private final Handler mHandler;
+ @NonNull
+ private final MdnsFeatureFlags mMdnsFeatureFlags;
- public MdnsServiceCache(@NonNull Looper looper) {
+ public MdnsServiceCache(@NonNull Looper looper, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
mHandler = new Handler(looper);
+ mMdnsFeatureFlags = mdnsFeatureFlags;
}
/**
* Get the cache services which are queried from given service type and socket.
*
- * @param serviceType the target service type.
- * @param socketKey the target socket
+ * @param cacheKey the target CacheKey.
* @return the set of services which matches the given service type.
*/
@NonNull
- public List<MdnsResponse> getCachedServices(@NonNull String serviceType,
- @NonNull SocketKey socketKey) {
+ public List<MdnsResponse> getCachedServices(@NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
- final CacheKey key = new CacheKey(serviceType, socketKey);
- return mCachedServices.containsKey(key)
- ? Collections.unmodifiableList(new ArrayList<>(mCachedServices.get(key)))
+ return mCachedServices.containsKey(cacheKey)
+ ? Collections.unmodifiableList(new ArrayList<>(mCachedServices.get(cacheKey)))
: Collections.emptyList();
}
@@ -117,16 +123,13 @@
* Get the cache service.
*
* @param serviceName the target service name.
- * @param serviceType the target service type.
- * @param socketKey the target socket
+ * @param cacheKey the target CacheKey.
* @return the service which matches given conditions.
*/
@Nullable
- public MdnsResponse getCachedService(@NonNull String serviceName,
- @NonNull String serviceType, @NonNull SocketKey socketKey) {
+ public MdnsResponse getCachedService(@NonNull String serviceName, @NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
- final List<MdnsResponse> responses =
- mCachedServices.get(new CacheKey(serviceType, socketKey));
+ final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
if (responses == null) {
return null;
}
@@ -137,15 +140,13 @@
/**
* Add or update a service.
*
- * @param serviceType the service type.
- * @param socketKey the target socket
+ * @param cacheKey the target CacheKey.
* @param response the response of the discovered service.
*/
- public void addOrUpdateService(@NonNull String serviceType, @NonNull SocketKey socketKey,
- @NonNull MdnsResponse response) {
+ public void addOrUpdateService(@NonNull CacheKey cacheKey, @NonNull MdnsResponse response) {
ensureRunningOnHandlerThread(mHandler);
final List<MdnsResponse> responses = mCachedServices.computeIfAbsent(
- new CacheKey(serviceType, socketKey), key -> new ArrayList<>());
+ cacheKey, key -> new ArrayList<>());
// Remove existing service if present.
final MdnsResponse existing =
findMatchedResponse(responses, response.getServiceInstanceName());
@@ -157,15 +158,12 @@
* Remove a service which matches the given service name, type and socket.
*
* @param serviceName the target service name.
- * @param serviceType the target service type.
- * @param socketKey the target socket.
+ * @param cacheKey the target CacheKey.
*/
@Nullable
- public MdnsResponse removeService(@NonNull String serviceName, @NonNull String serviceType,
- @NonNull SocketKey socketKey) {
+ public MdnsResponse removeService(@NonNull String serviceName, @NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
- final List<MdnsResponse> responses =
- mCachedServices.get(new CacheKey(serviceType, socketKey));
+ final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
if (responses == null) {
return null;
}
@@ -180,5 +178,37 @@
return null;
}
+ /**
+ * Register a callback to listen to service expiration.
+ *
+ * <p> Registering the same callback instance twice is a no-op, since MdnsServiceTypeClient
+ * relies on this.
+ *
+ * @param cacheKey the target CacheKey.
+ * @param callback the callback that notify the service is expired.
+ */
+ public void registerServiceExpiredCallback(@NonNull CacheKey cacheKey,
+ @NonNull ServiceExpiredCallback callback) {
+ ensureRunningOnHandlerThread(mHandler);
+ mCallbacks.put(cacheKey, callback);
+ }
+
+ /**
+ * Unregister the service expired callback.
+ *
+ * @param cacheKey the CacheKey that is registered to listen service expiration before.
+ */
+ public void unregisterServiceExpiredCallback(@NonNull CacheKey cacheKey) {
+ ensureRunningOnHandlerThread(mHandler);
+ mCallbacks.remove(cacheKey);
+ }
+
+ /*** Callbacks for listening service expiration */
+ public interface ServiceExpiredCallback {
+ /*** Notify the service is expired */
+ void onServiceRecordExpired(@NonNull MdnsResponse previousResponse,
+ @Nullable MdnsResponse newResponse);
+ }
+
// TODO: check ttl expiration for each service and notify to the clients.
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
index f851b35..4d407be 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -18,7 +18,8 @@
import android.annotation.Nullable;
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
@@ -27,7 +28,7 @@
import java.util.Objects;
/** An mDNS "SRV" record, which contains service information. */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class MdnsServiceRecord extends MdnsRecord {
public static final int PROTO_NONE = 0;
public static final int PROTO_TCP = 1;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index bbe8f4c..0a03186 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -16,6 +16,7 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback;
import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse;
import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
@@ -71,6 +72,15 @@
* The service caches for each socket. It should be accessed from looper thread only.
*/
@NonNull private final MdnsServiceCache serviceCache;
+ @NonNull private final MdnsServiceCache.CacheKey cacheKey;
+ @NonNull private final ServiceExpiredCallback serviceExpiredCallback =
+ new ServiceExpiredCallback() {
+ @Override
+ public void onServiceRecordExpired(@NonNull MdnsResponse previousResponse,
+ @Nullable MdnsResponse newResponse) {
+ notifyRemovedServiceToListeners(previousResponse, "Service record expired");
+ }
+ };
private final ArrayMap<MdnsServiceBrowserListener, MdnsSearchOptions> listeners =
new ArrayMap<>();
private final boolean removeServiceAfterTtlExpires =
@@ -225,6 +235,16 @@
this.dependencies = dependencies;
this.serviceCache = serviceCache;
this.mdnsQueryScheduler = new MdnsQueryScheduler();
+ this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
+ }
+
+ /**
+ * Do the cleanup of the MdnsServiceTypeClient
+ */
+ private void shutDown() {
+ removeScheduledTask();
+ mdnsQueryScheduler.cancelScheduledRun();
+ serviceCache.unregisterServiceExpiredCallback(cacheKey);
}
private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
@@ -293,7 +313,7 @@
boolean hadReply = false;
if (listeners.put(listener, searchOptions) == null) {
for (MdnsResponse existingResponse :
- serviceCache.getCachedServices(serviceType, socketKey)) {
+ serviceCache.getCachedServices(cacheKey)) {
if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
final MdnsServiceInfo info =
buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
@@ -341,6 +361,8 @@
servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
executor.submit(queryTask);
}
+
+ serviceCache.registerServiceExpiredCallback(cacheKey, serviceExpiredCallback);
}
/**
@@ -390,8 +412,7 @@
return listeners.isEmpty();
}
if (listeners.isEmpty()) {
- removeScheduledTask();
- mdnsQueryScheduler.cancelScheduledRun();
+ shutDown();
}
return listeners.isEmpty();
}
@@ -404,8 +425,7 @@
ensureRunningOnHandlerThread(handler);
// Augment the list of current known responses, and generated responses for resolve
// requests if there is no known response
- final List<MdnsResponse> cachedList =
- serviceCache.getCachedServices(serviceType, socketKey);
+ final List<MdnsResponse> cachedList = serviceCache.getCachedServices(cacheKey);
final List<MdnsResponse> currentList = new ArrayList<>(cachedList);
List<MdnsResponse> additionalResponses = makeResponsesForResolve(socketKey);
for (MdnsResponse additionalResponse : additionalResponses) {
@@ -432,7 +452,7 @@
} else if (findMatchedResponse(cachedList, serviceInstanceName) != null) {
// If the response is not modified and already in the cache. The cache will
// need to be updated to refresh the last receipt time.
- serviceCache.addOrUpdateService(serviceType, socketKey, response);
+ serviceCache.addOrUpdateService(cacheKey, response);
}
}
if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK)) {
@@ -458,44 +478,50 @@
}
}
- /** Notify all services are removed because the socket is destroyed. */
- public void notifySocketDestroyed() {
- ensureRunningOnHandlerThread(handler);
- for (MdnsResponse response : serviceCache.getCachedServices(serviceType, socketKey)) {
- final String name = response.getServiceInstanceName();
- if (name == null) continue;
- for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
- final MdnsServiceBrowserListener listener = listeners.keyAt(i);
- final MdnsServiceInfo serviceInfo =
- buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
+ private void notifyRemovedServiceToListeners(@NonNull MdnsResponse response,
+ @NonNull String message) {
+ for (int i = 0; i < listeners.size(); i++) {
+ if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
+ final MdnsServiceBrowserListener listener = listeners.keyAt(i);
+ if (response.getServiceInstanceName() != null) {
+ final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
+ response, serviceTypeLabels);
if (response.isComplete()) {
- sharedLog.log("Socket destroyed. onServiceRemoved: " + name);
+ sharedLog.log(message + ". onServiceRemoved: " + serviceInfo);
listener.onServiceRemoved(serviceInfo);
}
- sharedLog.log("Socket destroyed. onServiceNameRemoved: " + name);
+ sharedLog.log(message + ". onServiceNameRemoved: " + serviceInfo);
listener.onServiceNameRemoved(serviceInfo);
}
}
- removeScheduledTask();
- mdnsQueryScheduler.cancelScheduledRun();
+ }
+
+ /** Notify all services are removed because the socket is destroyed. */
+ public void notifySocketDestroyed() {
+ ensureRunningOnHandlerThread(handler);
+ for (MdnsResponse response : serviceCache.getCachedServices(cacheKey)) {
+ final String name = response.getServiceInstanceName();
+ if (name == null) continue;
+ notifyRemovedServiceToListeners(response, "Socket destroyed");
+ }
+ shutDown();
}
private void onResponseModified(@NonNull MdnsResponse response) {
final String serviceInstanceName = response.getServiceInstanceName();
final MdnsResponse currentResponse =
- serviceCache.getCachedService(serviceInstanceName, serviceType, socketKey);
+ serviceCache.getCachedService(serviceInstanceName, cacheKey);
boolean newServiceFound = false;
boolean serviceBecomesComplete = false;
if (currentResponse == null) {
newServiceFound = true;
if (serviceInstanceName != null) {
- serviceCache.addOrUpdateService(serviceType, socketKey, response);
+ serviceCache.addOrUpdateService(cacheKey, response);
}
} else {
boolean before = currentResponse.isComplete();
- serviceCache.addOrUpdateService(serviceType, socketKey, response);
+ serviceCache.addOrUpdateService(cacheKey, response);
boolean after = response.isComplete();
serviceBecomesComplete = !before && after;
}
@@ -529,22 +555,11 @@
private void onGoodbyeReceived(@Nullable String serviceInstanceName) {
final MdnsResponse response =
- serviceCache.removeService(serviceInstanceName, serviceType, socketKey);
+ serviceCache.removeService(serviceInstanceName, cacheKey);
if (response == null) {
return;
}
- for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
- final MdnsServiceBrowserListener listener = listeners.keyAt(i);
- final MdnsServiceInfo serviceInfo =
- buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
- if (response.isComplete()) {
- sharedLog.log("onServiceRemoved: " + serviceInfo);
- listener.onServiceRemoved(serviceInfo);
- }
- sharedLog.log("onServiceNameRemoved: " + serviceInfo);
- listener.onServiceNameRemoved(serviceInfo);
- }
+ notifyRemovedServiceToListeners(response, "Goodbye received");
}
private boolean shouldRemoveServiceAfterTtlExpires() {
@@ -567,7 +582,7 @@
continue;
}
MdnsResponse knownResponse =
- serviceCache.getCachedService(resolveName, serviceType, socketKey);
+ serviceCache.getCachedService(resolveName, cacheKey);
if (knownResponse == null) {
final ArrayList<String> instanceFullName = new ArrayList<>(
serviceTypeLabels.length + 1);
@@ -585,36 +600,18 @@
private void tryRemoveServiceAfterTtlExpires() {
if (!shouldRemoveServiceAfterTtlExpires()) return;
- Iterator<MdnsResponse> iter =
- serviceCache.getCachedServices(serviceType, socketKey).iterator();
+ final Iterator<MdnsResponse> iter = serviceCache.getCachedServices(cacheKey).iterator();
while (iter.hasNext()) {
MdnsResponse existingResponse = iter.next();
- final String serviceInstanceName = existingResponse.getServiceInstanceName();
if (existingResponse.hasServiceRecord()
&& existingResponse.getServiceRecord()
.getRemainingTTL(clock.elapsedRealtime()) == 0) {
- serviceCache.removeService(serviceInstanceName, serviceType, socketKey);
- for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(existingResponse, listeners.valueAt(i))) {
- continue;
- }
- final MdnsServiceBrowserListener listener = listeners.keyAt(i);
- if (serviceInstanceName != null) {
- final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
- existingResponse, serviceTypeLabels);
- if (existingResponse.isComplete()) {
- sharedLog.log("TTL expired. onServiceRemoved: " + serviceInfo);
- listener.onServiceRemoved(serviceInfo);
- }
- sharedLog.log("TTL expired. onServiceNameRemoved: " + serviceInfo);
- listener.onServiceNameRemoved(serviceInfo);
- }
- }
+ serviceCache.removeService(existingResponse.getServiceInstanceName(), cacheKey);
+ notifyRemovedServiceToListeners(existingResponse, "TTL expired");
}
}
}
-
private static class QuerySentArguments {
private final int transactionId;
private final List<String> subTypes = new ArrayList<>();
@@ -672,7 +669,7 @@
private long getMinRemainingTtl(long now) {
long minRemainingTtl = Long.MAX_VALUE;
- for (MdnsResponse response : serviceCache.getCachedServices(serviceType, socketKey)) {
+ for (MdnsResponse response : serviceCache.getCachedServices(cacheKey)) {
if (!response.isComplete()) {
continue;
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
index 4149dbe..cf6c8ac 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -18,7 +18,8 @@
import android.annotation.Nullable;
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
import java.io.IOException;
@@ -28,7 +29,7 @@
import java.util.Objects;
/** An mDNS "TXT" record, which contains a list of {@link TextEntry}. */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class MdnsTextRecord extends MdnsRecord {
private List<TextEntry> entries;
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 48e86d8..01b8de7 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -48,6 +48,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
@@ -237,7 +238,18 @@
mDeps = deps;
// Interface match regex.
- mIfaceMatch = mDeps.getInterfaceRegexFromResource(mContext);
+ String ifaceMatchRegex = mDeps.getInterfaceRegexFromResource(mContext);
+ // "*" is a magic string to indicate "pick the default".
+ if (ifaceMatchRegex.equals("*")) {
+ if (SdkLevel.isAtLeastU()) {
+ // On U+, include both usb%d and eth%d interfaces.
+ ifaceMatchRegex = "(usb|eth)\\d+";
+ } else {
+ // On T, include only eth%d interfaces.
+ ifaceMatchRegex = "eth\\d+";
+ }
+ }
+ mIfaceMatch = ifaceMatchRegex;
// Read default Ethernet interface configuration from resources
final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context);
diff --git a/service-t/src/com/android/server/net/NetworkStatsEventLogger.java b/service-t/src/com/android/server/net/NetworkStatsEventLogger.java
new file mode 100644
index 0000000..679837a
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsEventLogger.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Helper class for NetworkStatsService to log events.
+ *
+ * @hide
+ */
+public class NetworkStatsEventLogger {
+ static final int POLL_REASON_DUMPSYS = 0;
+ static final int POLL_REASON_FORCE_UPDATE = 1;
+ static final int POLL_REASON_GLOBAL_ALERT = 2;
+ static final int POLL_REASON_NETWORK_STATUS_CHANGED = 3;
+ static final int POLL_REASON_OPEN_SESSION = 4;
+ static final int POLL_REASON_PERIODIC = 5;
+ static final int POLL_REASON_RAT_CHANGED = 6;
+ static final int POLL_REASON_REG_CALLBACK = 7;
+ static final int POLL_REASON_REMOVE_UIDS = 8;
+ static final int POLL_REASON_UPSTREAM_CHANGED = 9;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "POLL_REASON_" }, value = {
+ POLL_REASON_DUMPSYS,
+ POLL_REASON_FORCE_UPDATE,
+ POLL_REASON_GLOBAL_ALERT,
+ POLL_REASON_NETWORK_STATUS_CHANGED,
+ POLL_REASON_OPEN_SESSION,
+ POLL_REASON_PERIODIC,
+ POLL_REASON_RAT_CHANGED,
+ POLL_REASON_REMOVE_UIDS,
+ POLL_REASON_REG_CALLBACK,
+ POLL_REASON_UPSTREAM_CHANGED
+ })
+ public @interface PollReason {
+ }
+ static final int MAX_POLL_REASON = POLL_REASON_UPSTREAM_CHANGED;
+
+ @VisibleForTesting(visibility = PRIVATE)
+ public static final int MAX_EVENTS_LOGS = 50;
+ private final LocalLog mEventChanges = new LocalLog(MAX_EVENTS_LOGS);
+ private final int[] mPollEventCounts = new int[MAX_POLL_REASON + 1];
+
+ /**
+ * Log a poll event.
+ *
+ * @param flags Flags used when polling. See NetworkStatsService#FLAG_PERSIST_*.
+ * @param event The event of polling to be logged.
+ */
+ public void logPollEvent(int flags, @NonNull PollEvent event) {
+ mEventChanges.log("Poll(flags=" + flags + ", " + event + ")");
+ mPollEventCounts[event.reason]++;
+ }
+
+ /**
+ * Print poll counts per reason into the given stream.
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public void dumpPollCountsPerReason(@NonNull IndentingPrintWriter pw) {
+ pw.println("Poll counts per reason:");
+ pw.increaseIndent();
+ for (int i = 0; i <= MAX_POLL_REASON; i++) {
+ pw.println(PollEvent.pollReasonNameOf(i) + ": " + mPollEventCounts[i]);
+ }
+ pw.decreaseIndent();
+ pw.println();
+ }
+
+ /**
+ * Print recent poll events into the given stream.
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public void dumpRecentPollEvents(@NonNull IndentingPrintWriter pw) {
+ pw.println("Recent poll events:");
+ pw.increaseIndent();
+ mEventChanges.reverseDump(pw);
+ pw.decreaseIndent();
+ pw.println();
+ }
+
+ /**
+ * Print the object's state into the given stream.
+ */
+ public void dump(@NonNull IndentingPrintWriter pw) {
+ dumpPollCountsPerReason(pw);
+ dumpRecentPollEvents(pw);
+ }
+
+ public static class PollEvent {
+ public final int reason;
+
+ public PollEvent(@PollReason int reason) {
+ if (reason < 0 || reason > MAX_POLL_REASON) {
+ throw new IllegalArgumentException("Unsupported poll reason: " + reason);
+ }
+ this.reason = reason;
+ }
+
+ @Override
+ public String toString() {
+ return "PollEvent{" + "reason=" + pollReasonNameOf(reason) + "}";
+ }
+
+ /**
+ * Get the name of the given reason.
+ *
+ * If the reason does not have a String representation, returns its integer representation.
+ */
+ @NonNull
+ public static String pollReasonNameOf(@PollReason int reason) {
+ switch (reason) {
+ case POLL_REASON_DUMPSYS: return "DUMPSYS";
+ case POLL_REASON_FORCE_UPDATE: return "FORCE_UPDATE";
+ case POLL_REASON_GLOBAL_ALERT: return "GLOBAL_ALERT";
+ case POLL_REASON_NETWORK_STATUS_CHANGED: return "NETWORK_STATUS_CHANGED";
+ case POLL_REASON_OPEN_SESSION: return "OPEN_SESSION";
+ case POLL_REASON_PERIODIC: return "PERIODIC";
+ case POLL_REASON_RAT_CHANGED: return "RAT_CHANGED";
+ case POLL_REASON_REMOVE_UIDS: return "REMOVE_UIDS";
+ case POLL_REASON_REG_CALLBACK: return "REG_CALLBACK";
+ case POLL_REASON_UPSTREAM_CHANGED: return "UPSTREAM_CHANGED";
+ default: return Integer.toString(reason);
+ }
+ }
+ }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 25e59d5..46afd31 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -66,6 +66,17 @@
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport;
import static com.android.net.module.util.NetworkStatsUtils.LIMIT_GLOBAL_ALERT;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_DUMPSYS;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_FORCE_UPDATE;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_GLOBAL_ALERT;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_NETWORK_STATUS_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_OPEN_SESSION;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REMOVE_UIDS;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_UPSTREAM_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.PollEvent;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -281,6 +292,8 @@
static final String NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME = "import.attempts";
static final String NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME = "import.successes";
static final String NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME = "import.fallbacks";
+ static final String CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER =
+ "enable_network_stats_event_logger";
private final Context mContext;
private final NetworkStatsFactory mStatsFactory;
@@ -441,6 +454,7 @@
* Map from key {@code OpenSessionKey} to count of opened sessions. This is for recording
* the caller of open session and it is only for debugging.
*/
+ // TODO: Move to NetworkStatsEventLogger to centralize event logging.
@GuardedBy("mOpenSessionCallsLock")
private final HashMap<OpenSessionKey, Integer> mOpenSessionCallsPerCaller = new HashMap<>();
@@ -513,19 +527,21 @@
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_PERFORM_POLL: {
- performPoll(FLAG_PERSIST_ALL);
+ performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent((int) msg.obj));
break;
}
case MSG_NOTIFY_NETWORK_STATUS: {
- // If no cached states, ignore.
- if (mLastNetworkStateSnapshots == null) break;
- // TODO (b/181642673): Protect mDefaultNetworks from concurrent accessing.
- handleNotifyNetworkStatus(
- mDefaultNetworks, mLastNetworkStateSnapshots, mActiveIface);
+ synchronized (mStatsLock) {
+ // If no cached states, ignore.
+ if (mLastNetworkStateSnapshots == null) break;
+ handleNotifyNetworkStatus(mDefaultNetworks, mLastNetworkStateSnapshots,
+ mActiveIface, maybeCreatePollEvent((int) msg.obj));
+ }
break;
}
case MSG_PERFORM_POLL_REGISTER_ALERT: {
- performPoll(FLAG_PERSIST_NETWORK);
+ performPoll(FLAG_PERSIST_NETWORK,
+ maybeCreatePollEvent(POLL_REASON_GLOBAL_ALERT));
registerGlobalAlert();
break;
}
@@ -611,6 +627,13 @@
mStatsMapB = mDeps.getStatsMapB();
mAppUidStatsMap = mDeps.getAppUidStatsMap();
mIfaceStatsMap = mDeps.getIfaceStatsMap();
+ // To prevent any possible races, the flag is not allowed to change until rebooting.
+ mSupportEventLogger = mDeps.supportEventLogger(mContext);
+ if (mSupportEventLogger) {
+ mEventLogger = new NetworkStatsEventLogger();
+ } else {
+ mEventLogger = null;
+ }
// TODO: Remove bpfNetMaps creation and always start SkDestroyListener
// Following code is for the experiment to verify the SkDestroyListener refactoring. Based
@@ -839,6 +862,14 @@
IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
return new SkDestroyListener(cookieTagMap, handler, new SharedLog(TAG));
}
+
+ /**
+ * Get whether event logger feature is supported.
+ */
+ public boolean supportEventLogger(Context ctx) {
+ return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+ ctx, CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER);
+ }
}
/**
@@ -1431,7 +1462,7 @@
| NetworkStatsManager.FLAG_POLL_FORCE)) != 0) {
final long ident = Binder.clearCallingIdentity();
try {
- performPoll(FLAG_PERSIST_ALL);
+ performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_OPEN_SESSION));
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -1827,7 +1858,8 @@
final long token = Binder.clearCallingIdentity();
try {
- handleNotifyNetworkStatus(defaultNetworks, networkStates, activeIface);
+ handleNotifyNetworkStatus(defaultNetworks, networkStates, activeIface,
+ maybeCreatePollEvent(POLL_REASON_NETWORK_STATUS_CHANGED));
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1844,7 +1876,8 @@
final long token = Binder.clearCallingIdentity();
try {
- performPoll(FLAG_PERSIST_ALL);
+ // TODO: Log callstack for system server callers.
+ performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_FORCE_UPDATE));
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1901,7 +1934,8 @@
}
// Create baseline stats
- mHandler.sendMessage(mHandler.obtainMessage(MSG_PERFORM_POLL));
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_PERFORM_POLL,
+ POLL_REASON_REG_CALLBACK));
return normalizedRequest;
}
@@ -1998,7 +2032,8 @@
new TetheringManager.TetheringEventCallback() {
@Override
public void onUpstreamChanged(@Nullable Network network) {
- performPoll(FLAG_PERSIST_NETWORK);
+ performPoll(FLAG_PERSIST_NETWORK,
+ maybeCreatePollEvent(POLL_REASON_UPSTREAM_CHANGED));
}
};
@@ -2007,7 +2042,7 @@
public void onReceive(Context context, Intent intent) {
// on background handler thread, and verified UPDATE_DEVICE_STATS
// permission above.
- performPoll(FLAG_PERSIST_ALL);
+ performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_PERIODIC));
// verify that we're watching global alert
registerGlobalAlert();
@@ -2071,19 +2106,20 @@
public void handleOnCollapsedRatTypeChanged() {
// Protect service from frequently updating. Remove pending messages if any.
mHandler.removeMessages(MSG_NOTIFY_NETWORK_STATUS);
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(MSG_NOTIFY_NETWORK_STATUS), mSettings.getPollDelay());
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_NOTIFY_NETWORK_STATUS,
+ POLL_REASON_RAT_CHANGED), mSettings.getPollDelay());
}
private void handleNotifyNetworkStatus(
Network[] defaultNetworks,
NetworkStateSnapshot[] snapshots,
- String activeIface) {
+ String activeIface,
+ @Nullable PollEvent event) {
synchronized (mStatsLock) {
mWakeLock.acquire();
try {
mActiveIface = activeIface;
- handleNotifyNetworkStatusLocked(defaultNetworks, snapshots);
+ handleNotifyNetworkStatusLocked(defaultNetworks, snapshots, event);
} finally {
mWakeLock.release();
}
@@ -2097,7 +2133,7 @@
*/
@GuardedBy("mStatsLock")
private void handleNotifyNetworkStatusLocked(@NonNull Network[] defaultNetworks,
- @NonNull NetworkStateSnapshot[] snapshots) {
+ @NonNull NetworkStateSnapshot[] snapshots, @Nullable PollEvent event) {
if (!mSystemReady) return;
if (LOGV) Log.v(TAG, "handleNotifyNetworkStatusLocked()");
@@ -2107,7 +2143,7 @@
// poll, but only persist network stats to keep codepath fast. UID stats
// will be persisted during next alarm poll event.
- performPollLocked(FLAG_PERSIST_NETWORK);
+ performPollLocked(FLAG_PERSIST_NETWORK, event);
// Rebuild active interfaces based on connected networks
mActiveIfaces.clear();
@@ -2324,12 +2360,12 @@
}
}
- private void performPoll(int flags) {
+ private void performPoll(int flags, @Nullable PollEvent event) {
synchronized (mStatsLock) {
mWakeLock.acquire();
try {
- performPollLocked(flags);
+ performPollLocked(flags, event);
} finally {
mWakeLock.release();
}
@@ -2341,11 +2377,15 @@
* {@link NetworkStatsHistory}.
*/
@GuardedBy("mStatsLock")
- private void performPollLocked(int flags) {
+ private void performPollLocked(int flags, @Nullable PollEvent event) {
if (!mSystemReady) return;
if (LOGV) Log.v(TAG, "performPollLocked(flags=0x" + Integer.toHexString(flags) + ")");
Trace.traceBegin(TRACE_TAG_NETWORK, "performPollLocked");
+ if (mSupportEventLogger) {
+ mEventLogger.logPollEvent(flags, event);
+ }
+
final boolean persistNetwork = (flags & FLAG_PERSIST_NETWORK) != 0;
final boolean persistUid = (flags & FLAG_PERSIST_UID) != 0;
final boolean persistForce = (flags & FLAG_PERSIST_FORCE) != 0;
@@ -2545,7 +2585,7 @@
if (LOGV) Log.v(TAG, "removeUidsLocked() for UIDs " + Arrays.toString(uids));
// Perform one last poll before removing
- performPollLocked(FLAG_PERSIST_ALL);
+ performPollLocked(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_REMOVE_UIDS));
mUidRecorder.removeUidsLocked(uids);
mUidTagRecorder.removeUidsLocked(uids);
@@ -2628,7 +2668,8 @@
}
if (poll) {
- performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE);
+ performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE,
+ maybeCreatePollEvent(POLL_REASON_DUMPSYS));
pw.println("Forced poll");
return;
}
@@ -2688,6 +2729,7 @@
pw.println("(failed to dump platform legacy stats import counters)");
}
}
+ pw.println(CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER + ": " + mSupportEventLogger);
pw.decreaseIndent();
@@ -2745,6 +2787,10 @@
pw.decreaseIndent();
pw.println();
+ if (mSupportEventLogger) {
+ mEventLogger.dump(pw);
+ }
+
pw.println("Stats Providers:");
pw.increaseIndent();
invokeForAllStatsProviderCallbacks((cb) -> {
@@ -3214,6 +3260,22 @@
}
}
+ private final boolean mSupportEventLogger;
+ @GuardedBy("mStatsLock")
+ @Nullable
+ private final NetworkStatsEventLogger mEventLogger;
+
+ /**
+ * Create a PollEvent instance if the feature is enabled.
+ */
+ @Nullable
+ public PollEvent maybeCreatePollEvent(@NetworkStatsEventLogger.PollReason int reason) {
+ if (mSupportEventLogger) {
+ return new PollEvent(reason);
+ }
+ return null;
+ }
+
private class DropBoxNonMonotonicObserver implements NonMonotonicObserver<String> {
@Override
public void foundNonMonotonic(NetworkStats left, int leftIndex, NetworkStats right,
diff --git a/service/Android.bp b/service/Android.bp
index 8e59e86..250693f 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -188,7 +188,6 @@
"dnsresolver_aidl_interface-V11-java",
"modules-utils-shell-command-handler",
"net-utils-device-common",
- "net-utils-device-common-bpf",
"net-utils-device-common-ip",
"net-utils-device-common-netlink",
"net-utils-services-common",
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index f30abc6..045d707f 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -194,8 +194,11 @@
-->
</string-array>
- <!-- Regex of wired ethernet ifaces -->
- <string translatable="false" name="config_ethernet_iface_regex">eth\\d</string>
+ <!-- Regex of wired ethernet ifaces. Network interfaces that match this regex will be tracked
+ by ethernet service.
+ If set to "*", ethernet service uses "(eth|usb)\\d+" on Android U+ and eth\\d+ on
+ Android T. -->
+ <string translatable="false" name="config_ethernet_iface_regex">*</string>
<!-- Ignores Wi-Fi validation failures after roam.
If validation fails on a Wi-Fi network after a roam to a new BSSID,
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 6a34a24..6ade124 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -19,6 +19,10 @@
import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
import static android.net.BpfNetMapsConstants.COOKIE_TAG_MAP_PATH;
import static android.net.BpfNetMapsConstants.CURRENT_STATS_MAP_CONFIGURATION_KEY;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_DISABLED;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
import static android.net.BpfNetMapsConstants.IIF_MATCH;
import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
@@ -26,6 +30,7 @@
import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH;
import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
+import static android.net.BpfNetMapsUtils.PRE_T;
import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
import static android.net.BpfNetMapsUtils.matchToString;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
@@ -51,7 +56,9 @@
import android.app.StatsManager;
import android.content.Context;
+import android.net.BpfNetMapsReader;
import android.net.INetd;
+import android.net.UidOwnerValue;
import android.os.Build;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
@@ -92,7 +99,6 @@
* {@hide}
*/
public class BpfNetMaps {
- private static final boolean PRE_T = !SdkLevel.isAtLeastT();
static {
if (!PRE_T) {
System.loadLibrary("service-connectivity");
@@ -128,6 +134,8 @@
private static IBpfMap<S32, UidOwnerValue> sUidOwnerMap = null;
private static IBpfMap<S32, U8> sUidPermissionMap = null;
private static IBpfMap<CookieTagMapKey, CookieTagMapValue> sCookieTagMap = null;
+ // TODO: Add BOOL class and replace U8?
+ private static IBpfMap<S32, U8> sDataSaverEnabledMap = null;
private static final List<Pair<Integer, String>> PERMISSION_LIST = Arrays.asList(
Pair.create(PERMISSION_INTERNET, "PERMISSION_INTERNET"),
@@ -175,6 +183,14 @@
sCookieTagMap = cookieTagMap;
}
+ /**
+ * Set dataSaverEnabledMap for test.
+ */
+ @VisibleForTesting
+ public static void setDataSaverEnabledMapForTest(IBpfMap<S32, U8> dataSaverEnabledMap) {
+ sDataSaverEnabledMap = dataSaverEnabledMap;
+ }
+
private static IBpfMap<S32, U32> getConfigurationMap() {
try {
return new BpfMap<>(
@@ -211,6 +227,15 @@
}
}
+ private static IBpfMap<S32, U8> getDataSaverEnabledMap() {
+ try {
+ return new BpfMap<>(
+ DATA_SAVER_ENABLED_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, U8.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open data saver enabled map", e);
+ }
+ }
+
private static void initBpfMaps() {
if (sConfigurationMap == null) {
sConfigurationMap = getConfigurationMap();
@@ -244,6 +269,15 @@
if (sCookieTagMap == null) {
sCookieTagMap = getCookieTagMap();
}
+
+ if (sDataSaverEnabledMap == null) {
+ sDataSaverEnabledMap = getDataSaverEnabledMap();
+ }
+ try {
+ sDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Failed to initialize data saver configuration", e);
+ }
}
/**
@@ -298,6 +332,7 @@
}
/** Constructor used after T that doesn't need to use netd anymore. */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public BpfNetMaps(final Context context) {
this(context, null);
@@ -420,6 +455,7 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void addNaughtyApp(final int uid) {
throwIfPreT("addNaughtyApp is not available on pre-T devices");
@@ -438,6 +474,7 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void removeNaughtyApp(final int uid) {
throwIfPreT("removeNaughtyApp is not available on pre-T devices");
@@ -456,6 +493,7 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void addNiceApp(final int uid) {
throwIfPreT("addNiceApp is not available on pre-T devices");
@@ -474,6 +512,7 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void removeNiceApp(final int uid) {
throwIfPreT("removeNiceApp is not available on pre-T devices");
@@ -494,6 +533,7 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void setChildChain(final int childChain, final boolean enable) {
throwIfPreT("setChildChain is not available on pre-T devices");
@@ -523,18 +563,14 @@
* @throws UnsupportedOperationException if called on pre-T devices.
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ *
+ * @deprecated Use {@link BpfNetMapsReader#isChainEnabled} instead.
*/
+ // TODO: Migrate the callers to use {@link BpfNetMapsReader#isChainEnabled} instead.
+ @Deprecated
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public boolean isChainEnabled(final int childChain) {
- throwIfPreT("isChainEnabled is not available on pre-T devices");
-
- final long match = getMatchByFirewallChain(childChain);
- try {
- final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
- return (config.val & match) != 0;
- } catch (ErrnoException e) {
- throw new ServiceSpecificException(e.errno,
- "Unable to get firewall chain status: " + Os.strerror(e.errno));
- }
+ return BpfNetMapsReader.isChainEnabled(sConfigurationMap, childChain);
}
private Set<Integer> asSet(final int[] uids) {
@@ -554,6 +590,7 @@
* @throws UnsupportedOperationException if called on pre-T devices.
* @throws IllegalArgumentException if {@code chain} is not a valid chain.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void replaceUidChain(final int chain, final int[] uids) {
throwIfPreT("replaceUidChain is not available on pre-T devices");
@@ -638,6 +675,7 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void setUidRule(final int childChain, final int uid, final int firewallRule) {
throwIfPreT("setUidRule is not available on pre-T devices");
@@ -667,20 +705,12 @@
* @throws UnsupportedOperationException if called on pre-T devices.
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ *
+ * @deprecated use {@link BpfNetMapsReader#getUidRule} instead.
*/
+ // TODO: Migrate the callers to use {@link BpfNetMapsReader#getUidRule} instead.
public int getUidRule(final int childChain, final int uid) {
- throwIfPreT("isUidChainEnabled is not available on pre-T devices");
-
- final long match = getMatchByFirewallChain(childChain);
- final boolean isAllowList = isFirewallAllowList(childChain);
- try {
- final UidOwnerValue uidMatch = sUidOwnerMap.getValue(new S32(uid));
- final boolean isMatchEnabled = uidMatch != null && (uidMatch.rule & match) != 0;
- return isMatchEnabled == isAllowList ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
- } catch (ErrnoException e) {
- throw new ServiceSpecificException(e.errno,
- "Unable to get uid rule status: " + Os.strerror(e.errno));
- }
+ return BpfNetMapsReader.getUidRule(sUidOwnerMap, childChain, uid);
}
private Set<Integer> getUidsMatchEnabled(final int childChain) throws ErrnoException {
@@ -830,6 +860,7 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void updateUidLockdownRule(final int uid, final boolean add) {
throwIfPreT("updateUidLockdownRule is not available on pre-T devices");
@@ -852,6 +883,7 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void swapActiveStatsMap() {
throwIfPreT("swapActiveStatsMap is not available on pre-T devices");
@@ -926,7 +958,29 @@
}
}
+ /**
+ * Set Data Saver enabled or disabled
+ *
+ * @param enable whether Data Saver is enabled or disabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ public void setDataSaverEnabled(boolean enable) {
+ throwIfPreT("setDataSaverEnabled is not available on pre-T devices");
+
+ try {
+ final short config = enable ? DATA_SAVER_ENABLED : DATA_SAVER_DISABLED;
+ sDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(config));
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno, "Unable to set data saver: "
+ + Os.strerror(e.errno));
+ }
+ }
+
/** Register callback for statsd to pull atom. */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void setPullAtomCallback(final Context context) {
throwIfPreT("setPullAtomCallback is not available on pre-T devices");
@@ -1007,6 +1061,15 @@
}
}
+ private void dumpDataSaverConfig(final IndentingPrintWriter pw) {
+ try {
+ final short config = sDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val;
+ // Any non-zero value converted from short to boolean is true by convention.
+ pw.println("sDataSaverEnabledMap: " + (config != DATA_SAVER_DISABLED));
+ } catch (ErrnoException e) {
+ pw.println("Failed to read data saver configuration: " + e);
+ }
+ }
/**
* Dump BPF maps
*
@@ -1016,6 +1079,7 @@
* @throws IOException when file descriptor is invalid.
* @throws ServiceSpecificException when the method is called on an unsupported device.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void dump(final IndentingPrintWriter pw, final FileDescriptor fd, boolean verbose)
throws IOException, ServiceSpecificException {
if (PRE_T) {
@@ -1056,6 +1120,8 @@
});
BpfDump.dumpMap(sUidPermissionMap, pw, "sUidPermissionMap",
(uid, permission) -> uid.val + " " + permissionToString(permission.val));
+
+ dumpDataSaverConfig(pw);
pw.decreaseIndent();
}
}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 01ffb03..dc09237 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission.RECEIVE_DATA_ACTIVITY_CHANGE;
import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
import static android.content.pm.PackageManager.FEATURE_BLUETOOTH;
+import static android.content.pm.PackageManager.FEATURE_LEANBACK;
import static android.content.pm.PackageManager.FEATURE_WATCH;
import static android.content.pm.PackageManager.FEATURE_WIFI;
import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT;
@@ -67,6 +68,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -97,12 +99,19 @@
import static android.system.OsConstants.ETH_P_ALL;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
+
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_BIND;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_BIND;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_EGRESS;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_INGRESS;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_SOCK_CREATE;
import static com.android.net.module.util.NetworkMonitorUtils.isPrivateDnsValidationRequired;
import static com.android.net.module.util.PermissionUtils.checkAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
+
import static java.util.Map.Entry;
import android.Manifest;
@@ -157,6 +166,7 @@
import android.net.IpMemoryStore;
import android.net.IpPrefix;
import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
import android.net.MatchAllNetworkSpecifier;
import android.net.NativeNetworkConfig;
import android.net.NativeNetworkType;
@@ -189,7 +199,6 @@
import android.net.QosSocketFilter;
import android.net.QosSocketInfo;
import android.net.RouteInfo;
-import android.net.RouteInfoParcel;
import android.net.SocketKeepalive;
import android.net.TetheringManager;
import android.net.TransportInfo;
@@ -275,6 +284,7 @@
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
import com.android.net.module.util.BinderUtils;
import com.android.net.module.util.BitUtils;
+import com.android.net.module.util.BpfUtils;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.InterfaceParams;
@@ -301,6 +311,7 @@
import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
import com.android.server.connectivity.DscpPolicyTracker;
import com.android.server.connectivity.FullScore;
+import com.android.server.connectivity.HandlerUtils;
import com.android.server.connectivity.InvalidTagException;
import com.android.server.connectivity.KeepaliveResourceUtil;
import com.android.server.connectivity.KeepaliveTracker;
@@ -318,6 +329,7 @@
import com.android.server.connectivity.ProfileNetworkPreferenceInfo;
import com.android.server.connectivity.ProxyTracker;
import com.android.server.connectivity.QosCallbackTracker;
+import com.android.server.connectivity.RoutingCoordinatorService;
import com.android.server.connectivity.UidRangeUtils;
import com.android.server.connectivity.VpnNetworkPreferenceInfo;
import com.android.server.connectivity.wear.CompanionDeviceManagerProxyService;
@@ -481,6 +493,7 @@
@GuardedBy("mTNSLock")
private TestNetworkService mTNS;
private final CompanionDeviceManagerProxyService mCdmps;
+ private final RoutingCoordinatorService mRoutingCoordinatorService;
private final Object mTNSLock = new Object();
@@ -1253,16 +1266,24 @@
private static final String PRIORITY_ARG = "--dump-priority";
private static final String PRIORITY_ARG_HIGH = "HIGH";
private static final String PRIORITY_ARG_NORMAL = "NORMAL";
+ private static final int DUMPSYS_DEFAULT_TIMEOUT_MS = 10_000;
LocalPriorityDump() {}
private void dumpHigh(FileDescriptor fd, PrintWriter pw) {
- doDump(fd, pw, new String[] {DIAG_ARG});
- doDump(fd, pw, new String[] {SHORT_ARG});
+ if (!HandlerUtils.runWithScissors(mHandler, () -> {
+ doDump(fd, pw, new String[]{DIAG_ARG});
+ doDump(fd, pw, new String[]{SHORT_ARG});
+ }, DUMPSYS_DEFAULT_TIMEOUT_MS)) {
+ pw.println("dumpHigh timeout");
+ }
}
private void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args) {
- doDump(fd, pw, args);
+ if (!HandlerUtils.runWithScissors(mHandler, () -> doDump(fd, pw, args),
+ DUMPSYS_DEFAULT_TIMEOUT_MS)) {
+ pw.println("dumpNormal timeout");
+ }
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
@@ -1315,6 +1336,10 @@
return SdkLevel.isAtLeastU();
}
+ public boolean isAtLeastV() {
+ return SdkLevel.isAtLeastV();
+ }
+
/**
* Get system properties to use in ConnectivityService.
*/
@@ -1511,6 +1536,14 @@
}
/**
+ * Get BPF program Id from CGROUP. See {@link BpfUtils#getProgramId}.
+ */
+ public int getBpfProgramId(final int attachType)
+ throws IOException {
+ return BpfUtils.getProgramId(attachType);
+ }
+
+ /**
* Wraps {@link BroadcastOptionsShimImpl#newInstance(BroadcastOptions)}
*/
// TODO: when available in all active branches:
@@ -1767,7 +1800,7 @@
mNoServiceNetwork = new NetworkAgentInfo(null,
new Network(INetd.UNREACHABLE_NET_ID),
new NetworkInfo(TYPE_NONE, 0, "", ""),
- new LinkProperties(), new NetworkCapabilities(),
+ new LinkProperties(), new NetworkCapabilities(), null /* localNetworkConfig */,
new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null,
new NetworkAgentConfig(), this, null, null, 0, INVALID_UID,
mLingerDelayMs, mQosCallbackTracker, mDeps);
@@ -1794,6 +1827,8 @@
mCdmps = null;
}
+ mRoutingCoordinatorService = new RoutingCoordinatorService(netd);
+
mDestroyFrozenSockets = mDeps.isAtLeastU()
&& mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION);
mDelayDestroyFrozenSockets = mDeps.isAtLeastU()
@@ -3234,6 +3269,26 @@
pw.decreaseIndent();
}
+ private void dumpBpfProgramStatus(IndentingPrintWriter pw) {
+ pw.println("Bpf Program Status:");
+ pw.increaseIndent();
+ try {
+ pw.print("CGROUP_INET_INGRESS: ");
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_INGRESS));
+ pw.print("CGROUP_INET_EGRESS: ");
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_EGRESS));
+ pw.print("CGROUP_INET_SOCK_CREATE: ");
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_SOCK_CREATE));
+ pw.print("CGROUP_INET4_BIND: ");
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET4_BIND));
+ pw.print("CGROUP_INET6_BIND: ");
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET6_BIND));
+ } catch (IOException e) {
+ pw.println(" IOException");
+ }
+ pw.decreaseIndent();
+ }
+
@VisibleForTesting
static final String KEY_DESTROY_FROZEN_SOCKETS_VERSION = "destroy_frozen_sockets_version";
@VisibleForTesting
@@ -3848,6 +3903,9 @@
dumpCloseFrozenAppSockets(pw);
pw.println();
+ dumpBpfProgramStatus(pw);
+
+ pw.println();
if (!CollectionUtils.contains(args, SHORT_ARG)) {
pw.println();
@@ -4115,6 +4173,11 @@
updateNetworkInfo(nai, info);
break;
}
+ case NetworkAgent.EVENT_LOCAL_NETWORK_CONFIG_CHANGED: {
+ final LocalNetworkConfig config = (LocalNetworkConfig) arg.second;
+ updateLocalNetworkConfig(nai, config);
+ break;
+ }
case NetworkAgent.EVENT_NETWORK_SCORE_CHANGED: {
updateNetworkScore(nai, (NetworkScore) arg.second);
break;
@@ -5000,7 +5063,10 @@
!nai.networkAgentConfig.allowBypass /* secure */,
getVpnType(nai), nai.networkAgentConfig.excludeLocalRouteVpn);
} else {
- config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL,
+ final boolean hasLocalCap =
+ nai.networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ config = new NativeNetworkConfig(nai.network.getNetId(),
+ hasLocalCap ? NativeNetworkType.PHYSICAL_LOCAL : NativeNetworkType.PHYSICAL,
getNetworkPermission(nai.networkCapabilities),
false /* secure */,
VpnManager.TYPE_VPN_NONE,
@@ -5008,8 +5074,8 @@
}
mNetd.networkCreate(config);
mDnsResolver.createNetworkCache(nai.network.getNetId());
- mDnsManager.updateTransportsForNetwork(nai.network.getNetId(),
- nai.networkCapabilities.getTransportTypes());
+ mDnsManager.updateCapabilitiesForNetwork(nai.network.getNetId(),
+ nai.networkCapabilities);
return true;
} catch (RemoteException | ServiceSpecificException e) {
loge("Error creating network " + nai.toShortString() + ": " + e.getMessage());
@@ -8028,6 +8094,7 @@
}
}
}
+ if (!highestPriorityNri.isBeingSatisfied()) return null;
return highestPriorityNri.getSatisfier();
}
@@ -8050,6 +8117,18 @@
}
/**
+ * Returns whether local agents are supported on this device.
+ *
+ * Local agents are supported from U on TVs, and from V on all devices.
+ */
+ @VisibleForTesting
+ public boolean areLocalAgentsSupported() {
+ final PackageManager pm = mContext.getPackageManager();
+ // Local agents are supported starting on U on TVs and on V on everything else.
+ return mDeps.isAtLeastV() || (mDeps.isAtLeastU() && pm.hasSystemFeature(FEATURE_LEANBACK));
+ }
+
+ /**
* Register a new agent with ConnectivityService to handle a network.
*
* @param na a reference for ConnectivityService to contact the agent asynchronously.
@@ -8060,13 +8139,18 @@
* @param networkCapabilities the initial capabilites of this network. They can be updated
* later : see {@link #updateCapabilities}.
* @param initialScore the initial score of the network. See {@link NetworkAgentInfo#getScore}.
+ * @param localNetworkConfig config about this local network, or null if not a local network
* @param networkAgentConfig metadata about the network. This is never updated.
* @param providerId the ID of the provider owning this NetworkAgent.
* @return the network created for this agent.
*/
- public Network registerNetworkAgent(INetworkAgent na, NetworkInfo networkInfo,
- LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
- @NonNull NetworkScore initialScore, NetworkAgentConfig networkAgentConfig,
+ public Network registerNetworkAgent(INetworkAgent na,
+ NetworkInfo networkInfo,
+ LinkProperties linkProperties,
+ NetworkCapabilities networkCapabilities,
+ @NonNull NetworkScore initialScore,
+ @Nullable LocalNetworkConfig localNetworkConfig,
+ NetworkAgentConfig networkAgentConfig,
int providerId) {
Objects.requireNonNull(networkInfo, "networkInfo must not be null");
Objects.requireNonNull(linkProperties, "linkProperties must not be null");
@@ -8078,12 +8162,26 @@
} else {
enforceNetworkFactoryPermission();
}
+ final boolean hasLocalCap =
+ networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ if (hasLocalCap && !areLocalAgentsSupported()) {
+ // Before U, netd doesn't support PHYSICAL_LOCAL networks so this can't work.
+ throw new IllegalArgumentException("Local agents are not supported in this version");
+ }
+ final boolean hasLocalNetworkConfig = null != localNetworkConfig;
+ if (hasLocalCap != hasLocalNetworkConfig) {
+ throw new IllegalArgumentException(null != localNetworkConfig
+ ? "Only local network agents can have a LocalNetworkConfig"
+ : "Local network agents must have a LocalNetworkConfig"
+ );
+ }
final int uid = mDeps.getCallingUid();
final long token = Binder.clearCallingIdentity();
try {
return registerNetworkAgentInternal(na, networkInfo, linkProperties,
- networkCapabilities, initialScore, networkAgentConfig, providerId, uid);
+ networkCapabilities, initialScore, networkAgentConfig, localNetworkConfig,
+ providerId, uid);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -8091,7 +8189,8 @@
private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
- NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId,
+ NetworkScore currentScore, NetworkAgentConfig networkAgentConfig,
+ @Nullable LocalNetworkConfig localNetworkConfig, int providerId,
int uid) {
// Make a copy of the passed NI, LP, NC as the caller may hold a reference to them
@@ -8099,6 +8198,7 @@
final NetworkInfo niCopy = new NetworkInfo(networkInfo);
final NetworkCapabilities ncCopy = new NetworkCapabilities(networkCapabilities);
final LinkProperties lpCopy = new LinkProperties(linkProperties);
+ // No need to copy |localNetworkConfiguration| as it is immutable.
// At this point the capabilities/properties are untrusted and unverified, e.g. checks that
// the capabilities' access UIDs comply with security limitations. They will be sanitized
@@ -8106,9 +8206,9 @@
// because some of the checks must happen on the handler thread.
final NetworkAgentInfo nai = new NetworkAgentInfo(na,
new Network(mNetIdManager.reserveNetId()), niCopy, lpCopy, ncCopy,
- currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
- this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
- mQosCallbackTracker, mDeps);
+ localNetworkConfig, currentScore, mContext, mTrackerHandler,
+ new NetworkAgentConfig(networkAgentConfig), this, mNetd, mDnsResolver, providerId,
+ uid, mLingerDelayMs, mQosCallbackTracker, mDeps);
final String extraInfo = niCopy.getExtraInfo();
final String name = TextUtils.isEmpty(extraInfo)
@@ -8418,7 +8518,7 @@
for (final String iface : interfaceDiff.added) {
try {
if (DBG) log("Adding iface " + iface + " to network " + netId);
- mNetd.networkAddInterface(netId, iface);
+ mRoutingCoordinatorService.addInterfaceToNetwork(netId, iface);
wakeupModifyInterface(iface, nai, true);
mDeps.reportNetworkInterfaceForTransports(mContext, iface,
nai.networkCapabilities.getTransportTypes());
@@ -8431,45 +8531,13 @@
try {
if (DBG) log("Removing iface " + iface + " from network " + netId);
wakeupModifyInterface(iface, nai, false);
- mNetd.networkRemoveInterface(netId, iface);
+ mRoutingCoordinatorService.removeInterfaceFromNetwork(netId, iface);
} catch (Exception e) {
loge("Exception removing interface: " + e);
}
}
}
- // TODO: move to frameworks/libs/net.
- private RouteInfoParcel convertRouteInfo(RouteInfo route) {
- final String nextHop;
-
- switch (route.getType()) {
- case RouteInfo.RTN_UNICAST:
- if (route.hasGateway()) {
- nextHop = route.getGateway().getHostAddress();
- } else {
- nextHop = INetd.NEXTHOP_NONE;
- }
- break;
- case RouteInfo.RTN_UNREACHABLE:
- nextHop = INetd.NEXTHOP_UNREACHABLE;
- break;
- case RouteInfo.RTN_THROW:
- nextHop = INetd.NEXTHOP_THROW;
- break;
- default:
- nextHop = INetd.NEXTHOP_NONE;
- break;
- }
-
- final RouteInfoParcel rip = new RouteInfoParcel();
- rip.ifName = route.getInterface();
- rip.destination = route.getDestination().toString();
- rip.nextHop = nextHop;
- rip.mtu = route.getMtu();
-
- return rip;
- }
-
/**
* Have netd update routes from oldLp to newLp.
* @return true if routes changed between oldLp and newLp
@@ -8490,10 +8558,10 @@
if (route.hasGateway()) continue;
if (VDBG || DDBG) log("Adding Route [" + route + "] to network " + netId);
try {
- mNetd.networkAddRouteParcel(netId, convertRouteInfo(route));
+ mRoutingCoordinatorService.addRoute(netId, route);
} catch (Exception e) {
if ((route.getDestination().getAddress() instanceof Inet4Address) || VDBG) {
- loge("Exception in networkAddRouteParcel for non-gateway: " + e);
+ loge("Exception in addRoute for non-gateway: " + e);
}
}
}
@@ -8501,10 +8569,10 @@
if (!route.hasGateway()) continue;
if (VDBG || DDBG) log("Adding Route [" + route + "] to network " + netId);
try {
- mNetd.networkAddRouteParcel(netId, convertRouteInfo(route));
+ mRoutingCoordinatorService.addRoute(netId, route);
} catch (Exception e) {
if ((route.getGateway() instanceof Inet4Address) || VDBG) {
- loge("Exception in networkAddRouteParcel for gateway: " + e);
+ loge("Exception in addRoute for gateway: " + e);
}
}
}
@@ -8512,18 +8580,18 @@
for (RouteInfo route : routeDiff.removed) {
if (VDBG || DDBG) log("Removing Route [" + route + "] from network " + netId);
try {
- mNetd.networkRemoveRouteParcel(netId, convertRouteInfo(route));
+ mRoutingCoordinatorService.removeRoute(netId, route);
} catch (Exception e) {
- loge("Exception in networkRemoveRouteParcel: " + e);
+ loge("Exception in removeRoute: " + e);
}
}
for (RouteInfo route : routeDiff.updated) {
if (VDBG || DDBG) log("Updating Route [" + route + "] from network " + netId);
try {
- mNetd.networkUpdateRouteParcel(netId, convertRouteInfo(route));
+ mRoutingCoordinatorService.updateRoute(netId, route);
} catch (Exception e) {
- loge("Exception in networkUpdateRouteParcel: " + e);
+ loge("Exception in updateRoute: " + e);
}
}
return !routeDiff.added.isEmpty() || !routeDiff.removed.isEmpty()
@@ -8831,8 +8899,7 @@
propagateUnderlyingNetworkCapabilities(nai.network);
if (!newNc.equalsTransportTypes(prevNc)) {
- mDnsManager.updateTransportsForNetwork(
- nai.network.getNetId(), newNc.getTransportTypes());
+ mDnsManager.updateCapabilitiesForNetwork(nai.network.getNetId(), newNc);
}
maybeSendProxyBroadcast(nai, prevNc, newNc);
@@ -8843,6 +8910,16 @@
updateCapabilities(nai.getScore(), nai, nai.networkCapabilities);
}
+ private void updateLocalNetworkConfig(@NonNull final NetworkAgentInfo nai,
+ @NonNull final LocalNetworkConfig config) {
+ if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+ Log.wtf(TAG, "Ignoring update of a local network info on non-local network " + nai);
+ return;
+ }
+ // TODO : actually apply the diff.
+ nai.localNetworkConfig = config;
+ }
+
/**
* Returns the interface which requires VPN isolation (ingress interface filtering).
*
@@ -9183,7 +9260,7 @@
// are Type.LISTEN, but should not have NetworkCallbacks invoked.
return;
}
- Bundle bundle = new Bundle();
+ final Bundle bundle = new Bundle();
// TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
// TODO: check if defensive copies of data is needed.
final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
@@ -10154,7 +10231,7 @@
// If a rate limit has been configured and is applicable to this network (network
// provides internet connectivity), apply it. The tc police filter cannot be attached
// before the clsact qdisc is added which happens as part of updateLinkProperties ->
- // updateInterfaces -> INetd#networkAddInterface.
+ // updateInterfaces -> RoutingCoordinatorManager#addInterfaceToNetwork
// Note: in case of a system server crash, the NetworkController constructor in netd
// (called when netd starts up) deletes the clsact qdisc of all interfaces.
if (canNetworkBeRateLimited(networkAgent) && mIngressRateLimit >= 0) {
@@ -10731,6 +10808,17 @@
Log.d(TAG, "Reevaluating network " + nai.network);
reportNetworkConnectivity(nai.network, !nai.isValidated());
return 0;
+ case "bpf-get-cgroup-program-id": {
+ // Usage : adb shell cmd connectivity bpf-get-cgroup-program-id <type>
+ // Get cgroup bpf program Id for the given type. See BpfUtils#getProgramId
+ // for more detail.
+ // If type can't be parsed, this throws NumberFormatException, which
+ // is passed back to adb who prints it.
+ final int type = Integer.parseInt(getNextArg());
+ final int ret = BpfUtils.getProgramId(type);
+ pw.println(ret);
+ return 0;
+ }
default:
return handleDefaultCommands(cmd);
}
@@ -11462,7 +11550,8 @@
// If there is no default network, default network is considered active to keep the existing
// behavior. Initial value is used until first connect to the default network.
private volatile boolean mIsDefaultNetworkActive = true;
- private final ArrayMap<String, IdleTimerParams> mActiveIdleTimers = new ArrayMap<>();
+ // Key is netId. Value is configured idle timer information.
+ private final SparseArray<IdleTimerParams> mActiveIdleTimers = new SparseArray<>();
private static class IdleTimerParams {
public final int timeout;
@@ -11490,7 +11579,7 @@
public void handleReportNetworkActivity(NetworkActivityParams activityParams) {
ensureRunningOnConnectivityServiceThread();
- if (mActiveIdleTimers.isEmpty()) {
+ if (mActiveIdleTimers.size() == 0) {
// This activity change is not for the current default network.
// This can happen if netd callback post activity change event message but
// the default network is lost before processing this message.
@@ -11566,6 +11655,7 @@
*/
private boolean setupDataActivityTracking(NetworkAgentInfo networkAgent) {
final String iface = networkAgent.linkProperties.getInterfaceName();
+ final int netId = networkAgent.network().netId;
final int timeout;
final int type;
@@ -11590,7 +11680,7 @@
if (timeout > 0 && iface != null) {
try {
- mActiveIdleTimers.put(iface, new IdleTimerParams(timeout, type));
+ mActiveIdleTimers.put(netId, new IdleTimerParams(timeout, type));
mNetd.idletimerAddInterface(iface, timeout, Integer.toString(type));
return true;
} catch (Exception e) {
@@ -11606,6 +11696,7 @@
*/
private void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
final String iface = networkAgent.linkProperties.getInterfaceName();
+ final int netId = networkAgent.network().netId;
final NetworkCapabilities caps = networkAgent.networkCapabilities;
if (iface == null) return;
@@ -11621,11 +11712,12 @@
try {
updateRadioPowerState(false /* isActive */, type);
- final IdleTimerParams params = mActiveIdleTimers.remove(iface);
+ final IdleTimerParams params = mActiveIdleTimers.get(netId);
if (params == null) {
// IdleTimer is not added if the configured timeout is 0 or negative value
return;
}
+ mActiveIdleTimers.remove(netId);
// The call fails silently if no idle timer setup for this interface
mNetd.idletimerRemoveInterface(iface, params.timeout,
Integer.toString(params.transportType));
@@ -11696,9 +11788,9 @@
pw.print("mIsDefaultNetworkActive="); pw.println(mIsDefaultNetworkActive);
pw.println("Idle timers:");
try {
- for (Map.Entry<String, IdleTimerParams> ent : mActiveIdleTimers.entrySet()) {
- pw.print(" "); pw.print(ent.getKey()); pw.println(":");
- final IdleTimerParams params = ent.getValue();
+ for (int i = 0; i < mActiveIdleTimers.size(); i++) {
+ pw.print(" "); pw.print(mActiveIdleTimers.keyAt(i)); pw.println(":");
+ final IdleTimerParams params = mActiveIdleTimers.valueAt(i);
pw.print(" timeout="); pw.print(params.timeout);
pw.print(" type="); pw.println(params.transportType);
}
@@ -12464,6 +12556,27 @@
}
}
+ @TargetApi(Build.VERSION_CODES.TIRAMISU)
+ @Override
+ public void setDataSaverEnabled(final boolean enable) {
+ enforceNetworkStackOrSettingsPermission();
+ try {
+ final boolean ret = mNetd.bandwidthEnableDataSaver(enable);
+ if (!ret) {
+ throw new IllegalStateException("Error when changing iptables: " + enable);
+ }
+ } catch (RemoteException e) {
+ // Lack of permission or binder errors.
+ throw new IllegalStateException(e);
+ }
+
+ try {
+ mBpfNetMaps.setDataSaverEnabled(enable);
+ } catch (ServiceSpecificException | UnsupportedOperationException e) {
+ Log.e(TAG, "Failed to set data saver " + enable + " : " + e);
+ }
+ }
+
@Override
public void updateMeteredNetworkAllowList(final int uid, final boolean add) {
enforceNetworkStackOrSettingsPermission();
@@ -12618,4 +12731,10 @@
enforceNetworkStackPermission(mContext);
return mCdmps;
}
+
+ @Override
+ public IBinder getRoutingCoordinatorService() {
+ enforceNetworkStackPermission(mContext);
+ return mRoutingCoordinatorService;
+ }
}
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index c1ba40e..cf6127f 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -16,9 +16,6 @@
package com.android.server.connectivity;
-import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_BIND;
-import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_BIND;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -31,11 +28,9 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.BpfBitmap;
-import com.android.net.module.util.BpfUtils;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.PermissionUtils;
-import java.io.IOException;
import java.util.ArrayList;
/**
@@ -45,11 +40,7 @@
public static final String SERVICE_NAME = "connectivity_native";
private static final String TAG = ConnectivityNativeService.class.getSimpleName();
- private static final String CGROUP_PATH = "/sys/fs/cgroup";
- private static final String V4_PROG_PATH =
- "/sys/fs/bpf/net_shared/prog_block_bind4_block_port";
- private static final String V6_PROG_PATH =
- "/sys/fs/bpf/net_shared/prog_block_bind6_block_port";
+
private static final String BLOCKED_PORTS_MAP_PATH =
"/sys/fs/bpf/net_shared/map_block_blocked_ports_map";
@@ -95,7 +86,6 @@
protected ConnectivityNativeService(final Context context, @NonNull Dependencies deps) {
mContext = context;
mBpfBlockedPortsMap = deps.getBlockPortsMap();
- attachProgram();
}
@Override
@@ -155,23 +145,4 @@
public String getInterfaceHash() {
return this.HASH;
}
-
- /**
- * Attach BPF program
- */
- private void attachProgram() {
- try {
- BpfUtils.attachProgram(BPF_CGROUP_INET4_BIND, V4_PROG_PATH, CGROUP_PATH, 0);
- } catch (IOException e) {
- throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET4_BIND: "
- + e);
- }
- try {
- BpfUtils.attachProgram(BPF_CGROUP_INET6_BIND, V6_PROG_PATH, CGROUP_PATH, 0);
- } catch (IOException e) {
- throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET6_BIND: "
- + e);
- }
- Log.d(TAG, "Attached BPF_CGROUP_INET4_BIND and BPF_CGROUP_INET6_BIND programs");
- }
}
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index 1493cae..81b2289 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -38,6 +38,7 @@
import android.net.InetAddresses;
import android.net.LinkProperties;
import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.ResolverParamsParcel;
import android.net.Uri;
import android.net.shared.PrivateDnsConfig;
@@ -251,7 +252,7 @@
// TODO: Replace the Map with SparseArrays.
private final Map<Integer, PrivateDnsValidationStatuses> mPrivateDnsValidationMap;
private final Map<Integer, LinkProperties> mLinkPropertiesMap;
- private final Map<Integer, int[]> mTransportsMap;
+ private final Map<Integer, NetworkCapabilities> mNetworkCapabilitiesMap;
private int mSampleValidity;
private int mSuccessThreshold;
@@ -265,7 +266,7 @@
mPrivateDnsMap = new ConcurrentHashMap<>();
mPrivateDnsValidationMap = new HashMap<>();
mLinkPropertiesMap = new HashMap<>();
- mTransportsMap = new HashMap<>();
+ mNetworkCapabilitiesMap = new HashMap<>();
// TODO: Create and register ContentObservers to track every setting
// used herein, posting messages to respond to changes.
@@ -278,7 +279,7 @@
public void removeNetwork(Network network) {
mPrivateDnsMap.remove(network.getNetId());
mPrivateDnsValidationMap.remove(network.getNetId());
- mTransportsMap.remove(network.getNetId());
+ mNetworkCapabilitiesMap.remove(network.getNetId());
mLinkPropertiesMap.remove(network.getNetId());
}
@@ -326,12 +327,12 @@
/**
* When creating a new network or transport types are changed in a specific network,
- * transport types are always saved to a hashMap before update dns config.
+ * capabilities are always saved to a hashMap before update dns config.
* When destroying network, the specific network will be removed from the hashMap.
* The hashMap is always accessed on the same thread.
*/
- public void updateTransportsForNetwork(int netId, @NonNull int[] transportTypes) {
- mTransportsMap.put(netId, transportTypes);
+ public void updateCapabilitiesForNetwork(int netId, @NonNull final NetworkCapabilities nc) {
+ mNetworkCapabilitiesMap.put(netId, nc);
sendDnsConfigurationForNetwork(netId);
}
@@ -351,8 +352,8 @@
*/
public void sendDnsConfigurationForNetwork(int netId) {
final LinkProperties lp = mLinkPropertiesMap.get(netId);
- final int[] transportTypes = mTransportsMap.get(netId);
- if (lp == null || transportTypes == null) return;
+ final NetworkCapabilities nc = mNetworkCapabilitiesMap.get(netId);
+ if (lp == null || nc == null) return;
updateParametersSettings();
final ResolverParamsParcel paramsParcel = new ResolverParamsParcel();
@@ -383,7 +384,7 @@
.collect(Collectors.toList()))
: useTls ? paramsParcel.servers // Opportunistic
: new String[0]; // Off
- paramsParcel.transportTypes = transportTypes;
+ paramsParcel.transportTypes = nc.getTransportTypes();
// Prepare to track the validation status of the DNS servers in the
// resolver config when private DNS is in opportunistic or strict mode.
if (useTls) {
diff --git a/service/src/com/android/server/connectivity/HandlerUtils.java b/service/src/com/android/server/connectivity/HandlerUtils.java
new file mode 100644
index 0000000..997ecbf
--- /dev/null
+++ b/service/src/com/android/server/connectivity/HandlerUtils.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+
+/**
+ * Helper class for Handler related utilities.
+ *
+ * @hide
+ */
+public class HandlerUtils {
+ // Note: @hide methods copied from android.os.Handler
+ /**
+ * Runs the specified task synchronously.
+ * <p>
+ * If the current thread is the same as the handler thread, then the runnable
+ * runs immediately without being enqueued. Otherwise, posts the runnable
+ * to the handler and waits for it to complete before returning.
+ * </p><p>
+ * This method is dangerous! Improper use can result in deadlocks.
+ * Never call this method while any locks are held or use it in a
+ * possibly re-entrant manner.
+ * </p><p>
+ * This method is occasionally useful in situations where a background thread
+ * must synchronously await completion of a task that must run on the
+ * handler's thread. However, this problem is often a symptom of bad design.
+ * Consider improving the design (if possible) before resorting to this method.
+ * </p><p>
+ * One example of where you might want to use this method is when you just
+ * set up a Handler thread and need to perform some initialization steps on
+ * it before continuing execution.
+ * </p><p>
+ * If timeout occurs then this method returns <code>false</code> but the runnable
+ * will remain posted on the handler and may already be in progress or
+ * complete at a later time.
+ * </p><p>
+ * When using this method, be sure to use {@link Looper#quitSafely} when
+ * quitting the looper. Otherwise {@link #runWithScissors} may hang indefinitely.
+ * (TODO: We should fix this by making MessageQueue aware of blocking runnables.)
+ * </p>
+ *
+ * @param h The target handler.
+ * @param r The Runnable that will be executed synchronously.
+ * @param timeout The timeout in milliseconds, or 0 to wait indefinitely.
+ *
+ * @return Returns true if the Runnable was successfully executed.
+ * Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ *
+ * @hide This method is prone to abuse and should probably not be in the API.
+ * If we ever do make it part of the API, we might want to rename it to something
+ * less funny like runUnsafe().
+ */
+ public static boolean runWithScissors(@NonNull Handler h, @NonNull Runnable r, long timeout) {
+ if (r == null) {
+ throw new IllegalArgumentException("runnable must not be null");
+ }
+ if (timeout < 0) {
+ throw new IllegalArgumentException("timeout must be non-negative");
+ }
+
+ if (Looper.myLooper() == h.getLooper()) {
+ r.run();
+ return true;
+ }
+
+ BlockingRunnable br = new BlockingRunnable(r);
+ return br.postAndWait(h, timeout);
+ }
+
+ private static final class BlockingRunnable implements Runnable {
+ private final Runnable mTask;
+ private boolean mDone;
+
+ BlockingRunnable(Runnable task) {
+ mTask = task;
+ }
+
+ @Override
+ public void run() {
+ try {
+ mTask.run();
+ } finally {
+ synchronized (this) {
+ mDone = true;
+ notifyAll();
+ }
+ }
+ }
+
+ public boolean postAndWait(Handler handler, long timeout) {
+ if (!handler.post(this)) {
+ return false;
+ }
+
+ synchronized (this) {
+ if (timeout > 0) {
+ final long expirationTime = SystemClock.uptimeMillis() + timeout;
+ while (!mDone) {
+ long delay = expirationTime - SystemClock.uptimeMillis();
+ if (delay <= 0) {
+ return false; // timeout
+ }
+ try {
+ wait(delay);
+ } catch (InterruptedException ex) {
+ }
+ }
+ } else {
+ while (!mDone) {
+ try {
+ wait();
+ } catch (InterruptedException ex) {
+ }
+ }
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index bdd841f..b0ad978 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -35,6 +35,7 @@
import android.net.INetworkAgentRegistry;
import android.net.INetworkMonitor;
import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
import android.net.NattKeepalivePacketData;
import android.net.Network;
import android.net.NetworkAgent;
@@ -64,7 +65,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.WakeupMessage;
-import com.android.modules.utils.build.SdkLevel;
import com.android.server.ConnectivityService;
import java.io.PrintWriter;
@@ -174,6 +174,7 @@
// TODO: make this private with a getter.
@NonNull public NetworkCapabilities networkCapabilities;
@NonNull public final NetworkAgentConfig networkAgentConfig;
+ @Nullable public LocalNetworkConfig localNetworkConfig;
// Underlying networks declared by the agent.
// The networks in this list might be declared by a VPN using setUnderlyingNetworks and are
@@ -453,6 +454,8 @@
* apply to the allowedUids field.
* They also should not mutate immutable capabilities, although for backward-compatibility
* this is not enforced and limited to just a log.
+ * Forbidden capabilities also make no sense for networks, so they are disallowed and
+ * will be ignored with a warning.
*
* @param carrierPrivilegeAuthenticator the authenticator, to check access UIDs.
*/
@@ -461,14 +464,15 @@
final NetworkCapabilities nc = new NetworkCapabilities(mDeclaredCapabilitiesUnsanitized);
if (nc.hasConnectivityManagedCapability()) {
Log.wtf(TAG, "BUG: " + this + " has CS-managed capability.");
+ nc.removeAllForbiddenCapabilities();
}
if (networkCapabilities.getOwnerUid() != nc.getOwnerUid()) {
Log.e(TAG, toShortString() + ": ignoring attempt to change owner from "
+ networkCapabilities.getOwnerUid() + " to " + nc.getOwnerUid());
nc.setOwnerUid(networkCapabilities.getOwnerUid());
}
- restrictCapabilitiesFromNetworkAgent(
- nc, creatorUid, mHasAutomotiveFeature, carrierPrivilegeAuthenticator);
+ restrictCapabilitiesFromNetworkAgent(nc, creatorUid, mHasAutomotiveFeature,
+ mConnServiceDeps, carrierPrivilegeAuthenticator);
return nc;
}
@@ -598,6 +602,7 @@
private static final String TAG = ConnectivityService.class.getSimpleName();
private static final boolean VDBG = false;
private final ConnectivityService mConnService;
+ private final ConnectivityService.Dependencies mConnServiceDeps;
private final Context mContext;
private final Handler mHandler;
private final QosCallbackTracker mQosCallbackTracker;
@@ -606,6 +611,7 @@
public NetworkAgentInfo(INetworkAgent na, Network net, NetworkInfo info,
@NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+ @Nullable LocalNetworkConfig localNetworkConfig,
@NonNull NetworkScore score, Context context,
Handler handler, NetworkAgentConfig config, ConnectivityService connService, INetd netd,
IDnsResolver dnsResolver, int factorySerialNumber, int creatorUid,
@@ -623,8 +629,10 @@
networkInfo = info;
linkProperties = lp;
networkCapabilities = nc;
+ this.localNetworkConfig = localNetworkConfig;
networkAgentConfig = config;
mConnService = connService;
+ mConnServiceDeps = deps;
setScore(score); // uses members connService, networkCapabilities and networkAgentConfig
clatd = new Nat464Xlat(this, netd, dnsResolver, deps);
mContext = context;
@@ -901,6 +909,12 @@
}
@Override
+ public void sendLocalNetworkConfig(@NonNull final LocalNetworkConfig config) {
+ mHandler.obtainMessage(NetworkAgent.EVENT_LOCAL_NETWORK_CONFIG_CHANGED,
+ new Pair<>(NetworkAgentInfo.this, config)).sendToTarget();
+ }
+
+ @Override
public void sendScore(@NonNull final NetworkScore score) {
mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_SCORE_CHANGED,
new Pair<>(NetworkAgentInfo.this, score)).sendToTarget();
@@ -1515,23 +1529,26 @@
*/
public static void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
final int creatorUid, final boolean hasAutomotiveFeature,
+ @NonNull final ConnectivityService.Dependencies deps,
@Nullable final CarrierPrivilegeAuthenticator authenticator) {
if (nc.hasTransport(TRANSPORT_TEST)) {
nc.restrictCapabilitiesForTestNetwork(creatorUid);
}
- if (!areAllowedUidsAcceptableFromNetworkAgent(nc, hasAutomotiveFeature, authenticator)) {
+ if (!areAllowedUidsAcceptableFromNetworkAgent(
+ nc, hasAutomotiveFeature, deps, authenticator)) {
nc.setAllowedUids(new ArraySet<>());
}
}
private static boolean areAllowedUidsAcceptableFromNetworkAgent(
@NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
+ @NonNull final ConnectivityService.Dependencies deps,
@Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) {
// NCs without access UIDs are fine.
if (!nc.hasAllowedUids()) return true;
// S and below must never accept access UIDs, even if an agent sends them, because netd
// didn't support the required feature in S.
- if (!SdkLevel.isAtLeastT()) return false;
+ if (!deps.isAtLeastT()) return false;
// On a non-restricted network, access UIDs make no sense
if (nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) return false;
diff --git a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
new file mode 100644
index 0000000..50e84d4
--- /dev/null
+++ b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static com.android.net.module.util.NetdUtils.toRouteInfoParcel;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.INetd;
+import android.net.IRoutingCoordinator;
+import android.net.RouteInfo;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+
+/**
+ * Class to coordinate routing across multiple clients.
+ *
+ * At present this is just a wrapper for netd methods, but it will be used to host some more
+ * coordination logic in the near future. It can be used to pull up some of the routing logic
+ * from netd into Java land.
+ *
+ * Note that usage of this class is not thread-safe. Clients are responsible for their own
+ * synchronization.
+ */
+public class RoutingCoordinatorService extends IRoutingCoordinator.Stub {
+ private final INetd mNetd;
+
+ public RoutingCoordinatorService(@NonNull INetd netd) {
+ mNetd = netd;
+ }
+
+ /**
+ * Add a route for specific network
+ *
+ * @param netId the network to add the route to
+ * @param route the route to add
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ @Override
+ public void addRoute(final int netId, final RouteInfo route)
+ throws ServiceSpecificException, RemoteException {
+ mNetd.networkAddRouteParcel(netId, toRouteInfoParcel(route));
+ }
+
+ /**
+ * Remove a route for specific network
+ *
+ * @param netId the network to remove the route from
+ * @param route the route to remove
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ @Override
+ public void removeRoute(final int netId, final RouteInfo route)
+ throws ServiceSpecificException, RemoteException {
+ mNetd.networkRemoveRouteParcel(netId, toRouteInfoParcel(route));
+ }
+
+ /**
+ * Update a route for specific network
+ *
+ * @param netId the network to update the route for
+ * @param route parcelable with route information
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ @Override
+ public void updateRoute(final int netId, final RouteInfo route)
+ throws ServiceSpecificException, RemoteException {
+ mNetd.networkUpdateRouteParcel(netId, toRouteInfoParcel(route));
+ }
+
+ /**
+ * Adds an interface to a network. The interface must not be assigned to any network, including
+ * the specified network.
+ *
+ * @param netId the network to add the interface to.
+ * @param iface the name of the interface to add.
+ *
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ @Override
+ public void addInterfaceToNetwork(final int netId, final String iface)
+ throws ServiceSpecificException, RemoteException {
+ mNetd.networkAddInterface(netId, iface);
+ }
+
+ /**
+ * Removes an interface from a network. The interface must be assigned to the specified network.
+ *
+ * @param netId the network to remove the interface from.
+ * @param iface the name of the interface to remove.
+ *
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ @Override
+ public void removeInterfaceFromNetwork(final int netId, final String iface)
+ throws ServiceSpecificException, RemoteException {
+ mNetd.networkRemoveInterface(netId, iface);
+ }
+}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 621759e..0bcb757 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -181,6 +181,8 @@
},
}
+// The net-utils-device-common-netlink library requires the callers to contain
+// net-utils-device-common-struct.
java_library {
name: "net-utils-device-common-netlink",
srcs: [
@@ -192,12 +194,13 @@
"//packages/modules/Connectivity:__subpackages__",
"//packages/modules/NetworkStack:__subpackages__",
],
- static_libs: [
- "net-utils-device-common-struct",
- ],
libs: [
"androidx.annotation_annotation",
"framework-connectivity.stubs.module_lib",
+ // For libraries which are statically linked in framework-connectivity, do not
+ // statically link here because callers of this library might already have a static
+ // version linked.
+ "net-utils-device-common-struct",
],
apex_available: [
"com.android.tethering",
@@ -209,6 +212,8 @@
},
}
+// The net-utils-device-common-ip library requires the callers to contain
+// net-utils-device-common-struct.
java_library {
// TODO : this target should probably be folded into net-utils-device-common
name: "net-utils-device-common-ip",
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
index 98fda56..1d8b4eb 100644
--- a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -28,6 +28,7 @@
import android.net.InterfaceConfigurationParcel;
import android.net.IpPrefix;
import android.net.RouteInfo;
+import android.net.RouteInfoParcel;
import android.net.TetherConfigParcel;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
@@ -159,9 +160,11 @@
throws RemoteException, ServiceSpecificException {
netd.tetherInterfaceAdd(iface);
networkAddInterface(netd, iface, maxAttempts, pollingIntervalMs);
- List<RouteInfo> routes = new ArrayList<>();
- routes.add(new RouteInfo(dest, null, iface, RTN_UNICAST));
- addRoutesToLocalNetwork(netd, iface, routes);
+ // Activate a route to dest and IPv6 link local.
+ modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+ new RouteInfo(dest, null, iface, RTN_UNICAST));
+ modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+ new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
}
/**
@@ -255,7 +258,7 @@
}
/** Add or remove |route|. */
- public static void modifyRoute(final INetd netd, final ModifyOperation op, final int netId,
+ private static void modifyRoute(final INetd netd, final ModifyOperation op, final int netId,
final RouteInfo route) {
final String ifName = route.getInterface();
final String dst = route.getDestination().toString();
@@ -276,4 +279,38 @@
throw new IllegalStateException(e);
}
}
+
+ /**
+ * Convert a RouteInfo into a RouteInfoParcel.
+ */
+ public static RouteInfoParcel toRouteInfoParcel(RouteInfo route) {
+ final String nextHop;
+
+ switch (route.getType()) {
+ case RouteInfo.RTN_UNICAST:
+ if (route.hasGateway()) {
+ nextHop = route.getGateway().getHostAddress();
+ } else {
+ nextHop = INetd.NEXTHOP_NONE;
+ }
+ break;
+ case RouteInfo.RTN_UNREACHABLE:
+ nextHop = INetd.NEXTHOP_UNREACHABLE;
+ break;
+ case RouteInfo.RTN_THROW:
+ nextHop = INetd.NEXTHOP_THROW;
+ break;
+ default:
+ nextHop = INetd.NEXTHOP_NONE;
+ break;
+ }
+
+ final RouteInfoParcel rip = new RouteInfoParcel();
+ rip.ifName = route.getInterface();
+ rip.destination = route.getDestination().toString();
+ rip.nextHop = nextHop;
+ rip.mtu = route.getMtu();
+
+ return rip;
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index d45cace..595ac74 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -187,12 +187,6 @@
return nativeDeleteMapEntry(mMapFd.getFd(), key.writeToBytes());
}
- /** Returns {@code true} if this map contains no elements. */
- @Override
- public boolean isEmpty() throws ErrnoException {
- return getFirstKey() == null;
- }
-
private K getNextKeyInternal(@Nullable K key) throws ErrnoException {
byte[] rawKey = new byte[mKeySize];
@@ -245,49 +239,6 @@
return Struct.parse(mValueClass, buffer);
}
- /**
- * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
- * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any
- * other structural modifications to the map, such as adding entries or deleting other entries.
- * Otherwise, iteration will result in undefined behaviour.
- */
- @Override
- public void forEach(ThrowingBiConsumer<K, V> action) throws ErrnoException {
- @Nullable K nextKey = getFirstKey();
-
- while (nextKey != null) {
- @NonNull final K curKey = nextKey;
- @NonNull final V value = getValue(curKey);
-
- nextKey = getNextKey(curKey);
- action.accept(curKey, value);
- }
- }
-
- /* Empty implementation to implement AutoCloseable, so we can use BpfMaps
- * with try with resources, but due to persistent FD cache, there is no actual
- * need to close anything. File descriptors will actually be closed when we
- * unlock the BpfMap class and destroy the ParcelFileDescriptor objects.
- */
- @Override
- public void close() throws IOException {
- }
-
- /**
- * Clears the map. The map may already be empty.
- *
- * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
- * or if a non-ENOENT error occurred when deleting a key.
- */
- @Override
- public void clear() throws ErrnoException {
- K key = getFirstKey();
- while (key != null) {
- deleteEntry(key); // ignores ENOENT.
- key = getFirstKey();
- }
- }
-
private static native int nativeBpfFdGet(String path, int mode, int keySize, int valueSize)
throws ErrnoException, NullPointerException;
diff --git a/staticlibs/device/com/android/net/module/util/BpfUtils.java b/staticlibs/device/com/android/net/module/util/BpfUtils.java
index f1546c0..10a8f60 100644
--- a/staticlibs/device/com/android/net/module/util/BpfUtils.java
+++ b/staticlibs/device/com/android/net/module/util/BpfUtils.java
@@ -32,40 +32,27 @@
// Defined in include/uapi/linux/bpf.h. Only adding the CGROUPS currently being used for now.
public static final int BPF_CGROUP_INET_INGRESS = 0;
public static final int BPF_CGROUP_INET_EGRESS = 1;
+ public static final int BPF_CGROUP_INET_SOCK_CREATE = 2;
public static final int BPF_CGROUP_INET4_BIND = 8;
public static final int BPF_CGROUP_INET6_BIND = 9;
+ // Note: This is only guaranteed to be accurate on U+ devices. It is likely to be accurate
+ // on T+ devices as well, but this is not guaranteed.
+ public static final String CGROUP_PATH = "/sys/fs/cgroup/";
/**
- * Attach BPF program to CGROUP
+ * Get BPF program Id from CGROUP.
+ *
+ * Note: This requires a 4.19 kernel which is only guaranteed on V+.
+ *
+ * @param attachType Bpf attach type. See bpf_attach_type in include/uapi/linux/bpf.h.
+ * @return Positive integer for a Program Id. 0 if no program is attached.
+ * @throws IOException if failed to open the cgroup directory or query bpf program.
*/
- public static void attachProgram(int type, @NonNull String programPath,
- @NonNull String cgroupPath, int flags) throws IOException {
- native_attachProgramToCgroup(type, programPath, cgroupPath, flags);
+ public static int getProgramId(int attachType) throws IOException {
+ return native_getProgramIdFromCgroup(attachType, CGROUP_PATH);
}
- /**
- * Detach BPF program from CGROUP
- */
- public static void detachProgram(int type, @NonNull String cgroupPath)
- throws IOException {
- native_detachProgramFromCgroup(type, cgroupPath);
- }
-
- /**
- * Detach single BPF program from CGROUP
- */
- public static void detachSingleProgram(int type, @NonNull String programPath,
- @NonNull String cgroupPath) throws IOException {
- native_detachSingleProgramFromCgroup(type, programPath, cgroupPath);
- }
-
- private static native boolean native_attachProgramToCgroup(int type, String programPath,
- String cgroupPath, int flags) throws IOException;
- private static native boolean native_detachProgramFromCgroup(int type, String cgroupPath)
- throws IOException;
- private static native boolean native_detachSingleProgramFromCgroup(int type,
- String programPath, String cgroupPath) throws IOException;
private static native int native_getProgramIdFromCgroup(int type, String cgroupPath)
throws IOException;
}
diff --git a/staticlibs/device/com/android/net/module/util/IBpfMap.java b/staticlibs/device/com/android/net/module/util/IBpfMap.java
index 83ff875..ca56830 100644
--- a/staticlibs/device/com/android/net/module/util/IBpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/IBpfMap.java
@@ -18,6 +18,7 @@
import android.system.ErrnoException;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import java.io.IOException;
import java.util.NoSuchElementException;
@@ -49,15 +50,17 @@
/** Remove existing key from eBpf map. Return true if something was deleted. */
boolean deleteEntry(K key) throws ErrnoException;
- /** Returns {@code true} if this map contains no elements. */
- boolean isEmpty() throws ErrnoException;
-
/** Get the key after the passed-in key. */
K getNextKey(@NonNull K key) throws ErrnoException;
/** Get the first key of the eBpf map. */
K getFirstKey() throws ErrnoException;
+ /** Returns {@code true} if this map contains no elements. */
+ default boolean isEmpty() throws ErrnoException {
+ return getFirstKey() == null;
+ }
+
/** Check whether a key exists in the map. */
boolean containsKey(@NonNull K key) throws ErrnoException;
@@ -70,13 +73,38 @@
/**
* Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
+ * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any
+ * other structural modifications to the map, such as adding entries or deleting other entries.
+ * Otherwise, iteration will result in undefined behaviour.
*/
- void forEach(ThrowingBiConsumer<K, V> action) throws ErrnoException;
+ default public void forEach(ThrowingBiConsumer<K, V> action) throws ErrnoException {
+ @Nullable K nextKey = getFirstKey();
- /** Clears the map. */
- void clear() throws ErrnoException;
+ while (nextKey != null) {
+ @NonNull final K curKey = nextKey;
+ @NonNull final V value = getValue(curKey);
+
+ nextKey = getNextKey(curKey);
+ action.accept(curKey, value);
+ }
+ }
+
+ /**
+ * Clears the map. The map may already be empty.
+ *
+ * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
+ * or if a non-ENOENT error occurred when deleting a key.
+ */
+ default public void clear() throws ErrnoException {
+ K key = getFirstKey();
+ while (key != null) {
+ deleteEntry(key); // ignores ENOENT.
+ key = getFirstKey();
+ }
+ }
/** Close for AutoCloseable. */
@Override
- void close() throws IOException;
+ default void close() throws IOException {
+ };
}
diff --git a/staticlibs/device/com/android/net/module/util/Struct.java b/staticlibs/device/com/android/net/module/util/Struct.java
index b638a46..dc0d19b 100644
--- a/staticlibs/device/com/android/net/module/util/Struct.java
+++ b/staticlibs/device/com/android/net/module/util/Struct.java
@@ -146,6 +146,14 @@
int arraysize() default 0;
}
+ /**
+ * Indicates that this field contains a computed value and is ignored for the purposes of Struct
+ * parsing.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ public @interface Computed {}
+
private static class FieldInfo {
@NonNull
public final Field annotation;
@@ -533,6 +541,7 @@
final FieldInfo[] annotationFields = new FieldInfo[getAnnotationFieldCount(clazz)];
for (java.lang.reflect.Field field : clazz.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())) continue;
+ if (field.getAnnotation(Computed.class) != null) continue;
final Field annotation = field.getAnnotation(Field.class);
if (annotation == null) {
diff --git a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
index 59d655c..b0f19e2 100644
--- a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
+++ b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
@@ -18,10 +18,17 @@
import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IAPREFIX;
+import android.net.IpPrefix;
+import android.util.Log;
+
import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Computed;
import com.android.net.module.util.Struct.Field;
import com.android.net.module.util.Struct.Type;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -52,6 +59,7 @@
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public class IaPrefixOption extends Struct {
+ private static final String TAG = IaPrefixOption.class.getSimpleName();
public static final int LENGTH = 25; // option length excluding IAprefix-options
@Field(order = 0, type = Type.S16)
@@ -67,7 +75,11 @@
@Field(order = 5, type = Type.ByteArray, arraysize = 16)
public final byte[] prefix;
- public IaPrefixOption(final short code, final short length, final long preferred,
+ @Computed
+ private final IpPrefix mIpPrefix;
+
+ // Constructor used by Struct.parse()
+ protected IaPrefixOption(final short code, final short length, final long preferred,
final long valid, final byte prefixLen, final byte[] prefix) {
this.code = code;
this.length = length;
@@ -75,6 +87,57 @@
this.valid = valid;
this.prefixLen = prefixLen;
this.prefix = prefix.clone();
+
+ try {
+ final Inet6Address addr = (Inet6Address) InetAddress.getByAddress(prefix);
+ mIpPrefix = new IpPrefix(addr, prefixLen);
+ } catch (UnknownHostException | ClassCastException e) {
+ // UnknownHostException should never happen unless prefix is null.
+ // ClassCastException can occur when prefix is an IPv6 mapped IPv4 address.
+ // Both scenarios should throw an exception in the context of Struct#parse().
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public IaPrefixOption(final short length, final long preferred, final long valid,
+ final byte prefixLen, final byte[] prefix) {
+ this((byte) DHCP6_OPTION_IAPREFIX, length, preferred, valid, prefixLen, prefix);
+ }
+
+ /**
+ * Check whether or not IA Prefix option in IA_PD option is valid per RFC8415#section-21.22.
+ *
+ * Note: an expired prefix can still be valid.
+ */
+ public boolean isValid() {
+ if (preferred < 0) {
+ Log.w(TAG, "Invalid preferred lifetime: " + this);
+ return false;
+ }
+ if (valid < 0) {
+ Log.w(TAG, "Invalid valid lifetime: " + this);
+ return false;
+ }
+ if (preferred > valid) {
+ Log.w(TAG, "Invalid lifetime. Preferred lifetime > valid lifetime: " + this);
+ return false;
+ }
+ if (prefixLen > 64) {
+ Log.w(TAG, "Invalid prefix length: " + this);
+ return false;
+ }
+ return true;
+ }
+
+ public IpPrefix getIpPrefix() {
+ return mIpPrefix;
+ }
+
+ /**
+ * Check whether or not IA Prefix option has 0 preferred and valid lifetimes.
+ */
+ public boolean withZeroLifetimes() {
+ return preferred == 0 && valid == 0;
}
/**
@@ -82,8 +145,14 @@
*/
public static ByteBuffer build(final short length, final long preferred, final long valid,
final byte prefixLen, final byte[] prefix) {
- final IaPrefixOption option = new IaPrefixOption((byte) DHCP6_OPTION_IAPREFIX,
+ final IaPrefixOption option = new IaPrefixOption(
length /* 25 + IAPrefix options length */, preferred, valid, prefixLen, prefix);
return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
}
+
+ @Override
+ public String toString() {
+ return "IA Prefix, length " + length + ": " + mIpPrefix + ", pref " + preferred + ", valid "
+ + valid;
+ }
}
diff --git a/staticlibs/framework/com/android/net/module/util/SdkUtil.java b/staticlibs/framework/com/android/net/module/util/SdkUtil.java
new file mode 100644
index 0000000..5006ba9
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/SdkUtil.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 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.net.module.util;
+
+import android.annotation.Nullable;
+
+/**
+ * Utilities to deal with multiple SDKs in a single mainline module.
+ * @hide
+ */
+public class SdkUtil {
+ /**
+ * Holder class taking advantage of erasure to avoid reflection running into class not found
+ * exceptions.
+ *
+ * This is useful to store a reference to a class that might not be present at runtime when
+ * fields are examined through reflection. An example is the MessageUtils class, which tries
+ * to get all fields in a class and therefore will try to load any class for which there
+ * is a member. Another example would be arguments or return values of methods in tests,
+ * when the testing framework uses reflection to list methods and their arguments.
+ *
+ * In these cases, LateSdk<T> can be used to hide type T from reflection, since it's erased
+ * and it becomes a vanilla LateSdk in Java bytecode. The T still can't be instantiated at
+ * runtime of course, but runtime tests will avoid that.
+ *
+ * @param <T> The held type
+ * @hide
+ */
+ public static class LateSdk<T> {
+ @Nullable public final T value;
+ public LateSdk(@Nullable final T value) {
+ this.value = value;
+ }
+ }
+}
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
index 1ae671e..81be37d 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
@@ -170,13 +170,13 @@
// HOPOPTS/DSTOPS follow up with 'u8 len', counting 8 byte units, (0->8, 1->16)
// *THREE* instructions
#define BPF3_LOAD_NETX_RELATIVE_V6EXTHDR_LEN \
- BPF_LOAD_NETX_RELATIVE_L4_U8(1) \
- BPF_STMT(BPF_ALU | BPF_ADD | BPF_K, 1) \
+ BPF_LOAD_NETX_RELATIVE_L4_U8(1), \
+ BPF_STMT(BPF_ALU | BPF_ADD | BPF_K, 1), \
BPF_STMT(BPF_ALU | BPF_LSH | BPF_K, 3)
// *TWO* instructions: A += X; X := A
#define BPF2_ADD_A_TO_X \
- BPF_STMT(BPF_ALU | BPF_ADD | BPF_X, 0) \
+ BPF_STMT(BPF_ALU | BPF_ADD | BPF_X, 0), \
BPF_STMT(BPF_MISC | BPF_TAX, 0)
// UDP/UDPLITE/TCP/SCTP/DCCP all start with be16 srcport, dstport
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index 67ac0e4..baff09b 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -105,9 +105,19 @@
* implemented in the kernel sources.
*/
-#define KVER_NONE 0
-#define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c))
-#define KVER_INF 0xFFFFFFFFu
+struct kver_uint { unsigned int kver; };
+#define KVER_(v) ((struct kver_uint){ .kver = (v) })
+#define KVER(a, b, c) KVER_(((a) << 24) + ((b) << 16) + (c))
+#define KVER_NONE KVER_(0)
+#define KVER_4_14 KVER(4, 14, 0)
+#define KVER_4_19 KVER(4, 19, 0)
+#define KVER_5_4 KVER(5, 4, 0)
+#define KVER_5_8 KVER(5, 8, 0)
+#define KVER_5_9 KVER(5, 9, 0)
+#define KVER_5_15 KVER(5, 15, 0)
+#define KVER_INF KVER_(0xFFFFFFFFu)
+
+#define KVER_IS_AT_LEAST(kver, a, b, c) ((kver).kver >= KVER(a, b, c).kver)
/*
* BPFFS (ie. /sys/fs/bpf) labelling is as follows:
@@ -188,10 +198,12 @@
__attribute__ ((section(".maps." #name), used)) \
____btf_map_##name = { }
-#define BPF_ASSERT_LOADER_VERSION(min_loader, ignore_eng, ignore_user, ignore_userdebug) \
- _Static_assert( \
- (min_loader) >= BPFLOADER_IGNORED_ON_VERSION || \
- !((ignore_eng) || (ignore_user) || (ignore_userdebug)), \
+#define BPF_ASSERT_LOADER_VERSION(min_loader, ignore_eng, ignore_user, ignore_userdebug) \
+ _Static_assert( \
+ (min_loader) >= BPFLOADER_IGNORED_ON_VERSION || \
+ !((ignore_eng).ignore_on_eng || \
+ (ignore_user).ignore_on_user || \
+ (ignore_userdebug).ignore_on_userdebug), \
"bpfloader min version must be >= 0.33 in order to use ignored_on");
#define DEFINE_BPF_MAP_BASE(the_map, TYPE, keysize, valuesize, num_entries, \
@@ -209,14 +221,14 @@
.mode = (md), \
.bpfloader_min_ver = (minloader), \
.bpfloader_max_ver = (maxloader), \
- .min_kver = (minkver), \
- .max_kver = (maxkver), \
+ .min_kver = (minkver).kver, \
+ .max_kver = (maxkver).kver, \
.selinux_context = (selinux), \
.pin_subdir = (pindir), \
- .shared = (share), \
- .ignore_on_eng = (ignore_eng), \
- .ignore_on_user = (ignore_user), \
- .ignore_on_userdebug = (ignore_userdebug), \
+ .shared = (share).shared, \
+ .ignore_on_eng = (ignore_eng).ignore_on_eng, \
+ .ignore_on_user = (ignore_user).ignore_on_user, \
+ .ignore_on_userdebug = (ignore_userdebug).ignore_on_userdebug, \
}; \
BPF_ASSERT_LOADER_VERSION(minloader, ignore_eng, ignore_user, ignore_userdebug);
@@ -230,7 +242,7 @@
selinux, pindir, share, min_loader, max_loader, \
ignore_eng, ignore_user, ignore_userdebug) \
DEFINE_BPF_MAP_BASE(the_map, RINGBUF, 0, 0, size_bytes, usr, grp, md, \
- selinux, pindir, share, KVER(5, 8, 0), KVER_INF, \
+ selinux, pindir, share, KVER_5_8, KVER_INF, \
min_loader, max_loader, ignore_eng, ignore_user, \
ignore_userdebug); \
\
@@ -312,11 +324,11 @@
#error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
#endif
-#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
- DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
- DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, false, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, /*ignore_on_eng*/false, \
- /*ignore_on_user*/false, /*ignore_on_userdebug*/false)
+#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
+ DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, PRIVATE, \
+ BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG, \
+ LOAD_ON_USER, LOAD_ON_USERDEBUG)
#define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -362,16 +374,16 @@
const struct bpf_prog_def SECTION("progs") the_prog##_def = { \
.uid = (prog_uid), \
.gid = (prog_gid), \
- .min_kver = (min_kv), \
- .max_kver = (max_kv), \
- .optional = (opt), \
+ .min_kver = (min_kv).kver, \
+ .max_kver = (max_kv).kver, \
+ .optional = (opt).optional, \
.bpfloader_min_ver = (min_loader), \
.bpfloader_max_ver = (max_loader), \
.selinux_context = (selinux), \
.pin_subdir = (pindir), \
- .ignore_on_eng = (ignore_eng), \
- .ignore_on_user = (ignore_user), \
- .ignore_on_userdebug = (ignore_userdebug), \
+ .ignore_on_eng = (ignore_eng).ignore_on_eng, \
+ .ignore_on_user = (ignore_user).ignore_on_user, \
+ .ignore_on_userdebug = (ignore_userdebug).ignore_on_userdebug, \
}; \
SECTION(SECTION_NAME) \
int the_prog
@@ -389,7 +401,7 @@
DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt, \
DEFAULT_BPF_PROG_SELINUX_CONTEXT, DEFAULT_BPF_PROG_PIN_SUBDIR, \
- false, false, false)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
// Programs (here used in the sense of functions/sections) marked optional are allowed to fail
// to load (for example due to missing kernel patches).
@@ -405,21 +417,24 @@
// programs requiring a kernel version >= min_kv && < max_kv
#define DEFINE_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
- false)
+ MANDATORY)
#define DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, \
max_kv) \
- DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, true)
+ DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
+ OPTIONAL)
// programs requiring a kernel version >= min_kv
#define DEFINE_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
- false)
+ MANDATORY)
#define DEFINE_OPTIONAL_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
- true)
+ OPTIONAL)
// programs with no kernel version requirements
#define DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, 0, KVER_INF, false)
+ DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+ MANDATORY)
#define DEFINE_OPTIONAL_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, 0, KVER_INF, true)
+ DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+ OPTIONAL)
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
index e7428b6..ef03c4d 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -114,6 +114,31 @@
// BPF wants 8, but 32-bit x86 wants 4
//_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+
+// for maps:
+struct shared_bool { bool shared; };
+#define PRIVATE ((struct shared_bool){ .shared = false })
+#define SHARED ((struct shared_bool){ .shared = true })
+
+// for programs:
+struct optional_bool { bool optional; };
+#define MANDATORY ((struct optional_bool){ .optional = false })
+#define OPTIONAL ((struct optional_bool){ .optional = true })
+
+// for both maps and programs:
+struct ignore_on_eng_bool { bool ignore_on_eng; };
+#define LOAD_ON_ENG ((struct ignore_on_eng_bool){ .ignore_on_eng = false })
+#define IGNORE_ON_ENG ((struct ignore_on_eng_bool){ .ignore_on_eng = true })
+
+struct ignore_on_user_bool { bool ignore_on_user; };
+#define LOAD_ON_USER ((struct ignore_on_user_bool){ .ignore_on_user = false })
+#define IGNORE_ON_USER ((struct ignore_on_user_bool){ .ignore_on_user = true })
+
+struct ignore_on_userdebug_bool { bool ignore_on_userdebug; };
+#define LOAD_ON_USERDEBUG ((struct ignore_on_userdebug_bool){ .ignore_on_userdebug = false })
+#define IGNORE_ON_USERDEBUG ((struct ignore_on_userdebug_bool){ .ignore_on_userdebug = true })
+
+
// Length of strings (incl. selinux_context and pin_subdir)
// in the bpf_map_def and bpf_prog_def structs.
//
diff --git a/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp b/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
index cf09379..bcc3ded 100644
--- a/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
+++ b/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
@@ -30,86 +30,6 @@
using base::unique_fd;
-// If attach fails throw error and return false.
-static jboolean com_android_net_module_util_BpfUtil_attachProgramToCgroup(JNIEnv *env,
- jclass clazz, jint type, jstring bpfProgPath, jstring cgroupPath, jint flags) {
-
- ScopedUtfChars dirPath(env, cgroupPath);
- unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
- if (cg_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to open the cgroup directory %s: %s",
- dirPath.c_str(), strerror(errno));
- return false;
- }
-
- ScopedUtfChars bpfProg(env, bpfProgPath);
- unique_fd bpf_fd(bpf::retrieveProgram(bpfProg.c_str()));
- if (bpf_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to retrieve bpf program from %s: %s",
- bpfProg.c_str(), strerror(errno));
- return false;
- }
- if (bpf::attachProgram((bpf_attach_type) type, bpf_fd, cg_fd, flags)) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to attach bpf program %s to %s: %s",
- bpfProg.c_str(), dirPath.c_str(), strerror(errno));
- return false;
- }
- return true;
-}
-
-// If detach fails throw error and return false.
-static jboolean com_android_net_module_util_BpfUtil_detachProgramFromCgroup(JNIEnv *env,
- jclass clazz, jint type, jstring cgroupPath) {
-
- ScopedUtfChars dirPath(env, cgroupPath);
- unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
- if (cg_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to open the cgroup directory %s: %s",
- dirPath.c_str(), strerror(errno));
- return false;
- }
-
- if (bpf::detachProgram((bpf_attach_type) type, cg_fd)) {
- jniThrowExceptionFmt(env, "Failed to detach bpf program from %s: %s",
- dirPath.c_str(), strerror(errno));
- return false;
- }
- return true;
-}
-
-// If detach single program fails throw error and return false.
-static jboolean com_android_net_module_util_BpfUtil_detachSingleProgramFromCgroup(JNIEnv *env,
- jclass clazz, jint type, jstring bpfProgPath, jstring cgroupPath) {
-
- ScopedUtfChars dirPath(env, cgroupPath);
- unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
- if (cg_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to open the cgroup directory %s: %s",
- dirPath.c_str(), strerror(errno));
- return false;
- }
-
- ScopedUtfChars bpfProg(env, bpfProgPath);
- unique_fd bpf_fd(bpf::retrieveProgram(bpfProg.c_str()));
- if (bpf_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to retrieve bpf program from %s: %s",
- bpfProg.c_str(), strerror(errno));
- return false;
- }
- if (bpf::detachSingleProgram((bpf_attach_type) type, bpf_fd, cg_fd)) {
- jniThrowExceptionFmt(env, "Failed to detach bpf program %s from %s: %s",
- bpfProg.c_str(), dirPath.c_str(), strerror(errno));
- return false;
- }
- return true;
-}
-
static jint com_android_net_module_util_BpfUtil_getProgramIdFromCgroup(JNIEnv *env,
jclass clazz, jint type, jstring cgroupPath) {
@@ -138,12 +58,6 @@
*/
static const JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
- { "native_attachProgramToCgroup", "(ILjava/lang/String;Ljava/lang/String;I)Z",
- (void*) com_android_net_module_util_BpfUtil_attachProgramToCgroup },
- { "native_detachProgramFromCgroup", "(ILjava/lang/String;)Z",
- (void*) com_android_net_module_util_BpfUtil_detachProgramFromCgroup },
- { "native_detachSingleProgramFromCgroup", "(ILjava/lang/String;Ljava/lang/String;)Z",
- (void*) com_android_net_module_util_BpfUtil_detachSingleProgramFromCgroup },
{ "native_getProgramIdFromCgroup", "(ILjava/lang/String;)I",
(void*) com_android_net_module_util_BpfUtil_getProgramIdFromCgroup },
};
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 5fe7ac3..a5e5afb 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -38,6 +38,7 @@
"net-utils-device-common",
"net-utils-device-common-async",
"net-utils-device-common-netlink",
+ "net-utils-device-common-struct",
"net-utils-device-common-wear",
"modules-utils-build_system",
],
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
index 35f22b9..46229b0 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
@@ -27,6 +27,9 @@
@Deprecated("Use Build.VERSION_CODES", ReplaceWith("Build.VERSION_CODES.S_V2"))
const val SC_V2 = Build.VERSION_CODES.S_V2
+// TODO: Remove this when Build.VERSION_CODES.VANILLA_ICE_CREAM is available in all branches
+// where this code builds
+const val VANILLA_ICE_CREAM = 35 // Bui1ld.VERSION_CODES.VANILLA_ICE_CREAM
private val MAX_TARGET_SDK_ANNOTATION_RE = Pattern.compile("MaxTargetSdk([0-9]+)$")
private val targetSdk = InstrumentationRegistry.getContext().applicationInfo.targetSdkVersion
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
index 733bd98..70f20d6 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
@@ -43,6 +43,8 @@
public class TestBpfMap<K extends Struct, V extends Struct> implements IBpfMap<K, V> {
private final ConcurrentHashMap<K, V> mMap = new ConcurrentHashMap<>();
+ public TestBpfMap() {}
+
// TODO: Remove this constructor
public TestBpfMap(final Class<K> key, final Class<V> value) {
}
diff --git a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
index eb94781..600a623 100644
--- a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
+++ b/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -128,7 +128,7 @@
if (testInfo.device.getApiLevel() < 31) return
testInfo.exec("cmd connectivity set-chain3-enabled $enableChain")
enablePkgs.forEach { (pkg, allow) ->
- testInfo.exec("cmd connectivity set-package-networking-enabled $pkg $allow")
+ testInfo.exec("cmd connectivity set-package-networking-enabled $allow $pkg")
}
}
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index 77383ad..6ea5347 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -29,6 +29,7 @@
"src/**/*.kt",
"src/**/*.aidl",
],
+ asset_dirs: ["assets"],
static_libs: [
"androidx.test.rules",
"mockito-target-minus-junit4",
diff --git a/tests/benchmark/assets/dataset/A052701.zip b/tests/benchmark/assets/dataset/A052701.zip
new file mode 100644
index 0000000..fdde1ad
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052701.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052801.zip b/tests/benchmark/assets/dataset/A052801.zip
new file mode 100644
index 0000000..7f908b7
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052801.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052802.zip b/tests/benchmark/assets/dataset/A052802.zip
new file mode 100644
index 0000000..180ad3e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052802.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052803.zip b/tests/benchmark/assets/dataset/A052803.zip
new file mode 100644
index 0000000..321a79b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052803.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052804.zip b/tests/benchmark/assets/dataset/A052804.zip
new file mode 100644
index 0000000..298ec04
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052804.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052901.zip b/tests/benchmark/assets/dataset/A052901.zip
new file mode 100644
index 0000000..0f49543
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052901.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052902.zip b/tests/benchmark/assets/dataset/A052902.zip
new file mode 100644
index 0000000..ec22456
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052902.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053001.zip b/tests/benchmark/assets/dataset/A053001.zip
new file mode 100644
index 0000000..ad5d82e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053001.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053002.zip b/tests/benchmark/assets/dataset/A053002.zip
new file mode 100644
index 0000000..8a4bb0c
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053002.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053003.zip b/tests/benchmark/assets/dataset/A053003.zip
new file mode 100644
index 0000000..24d2057
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053003.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053004.zip b/tests/benchmark/assets/dataset/A053004.zip
new file mode 100644
index 0000000..352f93f
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053004.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053005.zip b/tests/benchmark/assets/dataset/A053005.zip
new file mode 100644
index 0000000..2b49a1b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053005.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053006.zip b/tests/benchmark/assets/dataset/A053006.zip
new file mode 100644
index 0000000..a59f2ec
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053006.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053007.zip b/tests/benchmark/assets/dataset/A053007.zip
new file mode 100644
index 0000000..df7ae74
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053007.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053101.zip b/tests/benchmark/assets/dataset/A053101.zip
new file mode 100644
index 0000000..c10ed64
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053102.zip b/tests/benchmark/assets/dataset/A053102.zip
new file mode 100644
index 0000000..8c9f9cf
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053102.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053103.zip b/tests/benchmark/assets/dataset/A053103.zip
new file mode 100644
index 0000000..9202c50
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053103.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053104.zip b/tests/benchmark/assets/dataset/A053104.zip
new file mode 100644
index 0000000..3c77724
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053104.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060101.zip b/tests/benchmark/assets/dataset/A060101.zip
new file mode 100644
index 0000000..86443a7
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060102.zip b/tests/benchmark/assets/dataset/A060102.zip
new file mode 100644
index 0000000..4f2cf49
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060102.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060201.zip b/tests/benchmark/assets/dataset/A060201.zip
new file mode 100644
index 0000000..3c28bec
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060201.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060202.zip b/tests/benchmark/assets/dataset/A060202.zip
new file mode 100644
index 0000000..e39e493
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060202.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B053001.zip b/tests/benchmark/assets/dataset/B053001.zip
new file mode 100644
index 0000000..8408744
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B053001.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B053002.zip b/tests/benchmark/assets/dataset/B053002.zip
new file mode 100644
index 0000000..5245f70
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B053002.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060101.zip b/tests/benchmark/assets/dataset/B060101.zip
new file mode 100644
index 0000000..242c0d1
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060201.zip b/tests/benchmark/assets/dataset/B060201.zip
new file mode 100644
index 0000000..29df25a
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060201.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060202.zip b/tests/benchmark/assets/dataset/B060202.zip
new file mode 100644
index 0000000..bda9edd
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060202.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060203.zip b/tests/benchmark/assets/dataset/B060203.zip
new file mode 100644
index 0000000..b9fccfe
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060203.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060204.zip b/tests/benchmark/assets/dataset/B060204.zip
new file mode 100644
index 0000000..66227d2
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060204.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060205.zip b/tests/benchmark/assets/dataset/B060205.zip
new file mode 100644
index 0000000..6aaa06b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060205.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060206.zip b/tests/benchmark/assets/dataset/B060206.zip
new file mode 100644
index 0000000..18445b0
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060206.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060207.zip b/tests/benchmark/assets/dataset/B060207.zip
new file mode 100644
index 0000000..20f7c5b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060207.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060101.zip b/tests/benchmark/assets/dataset/C060101.zip
new file mode 100644
index 0000000..0b1c29f
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060102.zip b/tests/benchmark/assets/dataset/C060102.zip
new file mode 100644
index 0000000..8064905
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060102.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060103.zip b/tests/benchmark/assets/dataset/C060103.zip
new file mode 100644
index 0000000..d0e819f
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060103.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060104.zip b/tests/benchmark/assets/dataset/C060104.zip
new file mode 100644
index 0000000..f87ca8d
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060104.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060105.zip b/tests/benchmark/assets/dataset/C060105.zip
new file mode 100644
index 0000000..e869895
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060105.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060106.zip b/tests/benchmark/assets/dataset/C060106.zip
new file mode 100644
index 0000000..6d25a98
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060106.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060107.zip b/tests/benchmark/assets/dataset/C060107.zip
new file mode 100644
index 0000000..a7cb31c
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060107.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060108.zip b/tests/benchmark/assets/dataset/C060108.zip
new file mode 100644
index 0000000..c1a5898
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060108.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060109.zip b/tests/benchmark/assets/dataset/C060109.zip
new file mode 100644
index 0000000..bb9116e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060109.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060110.zip b/tests/benchmark/assets/dataset/C060110.zip
new file mode 100644
index 0000000..5ca0f96
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060110.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060111.zip b/tests/benchmark/assets/dataset/C060111.zip
new file mode 100644
index 0000000..6a12d7e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060111.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060112.zip b/tests/benchmark/assets/dataset/C060112.zip
new file mode 100644
index 0000000..fa2c30b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060112.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060113.zip b/tests/benchmark/assets/dataset/C060113.zip
new file mode 100644
index 0000000..63a34ba
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060113.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060114.zip b/tests/benchmark/assets/dataset/C060114.zip
new file mode 100644
index 0000000..bd60927
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060114.zip
Binary files differ
diff --git a/tests/benchmark/res/raw/netstats-many-uids-zip b/tests/benchmark/assets/dataset/netstats-many-uids.zip
similarity index 98%
rename from tests/benchmark/res/raw/netstats-many-uids-zip
rename to tests/benchmark/assets/dataset/netstats-many-uids.zip
index 22e8254..9554aaa 100644
--- a/tests/benchmark/res/raw/netstats-many-uids-zip
+++ b/tests/benchmark/assets/dataset/netstats-many-uids.zip
Binary files differ
diff --git a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
index e80548b..585157f 100644
--- a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
+++ b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
@@ -20,10 +20,9 @@
import android.net.NetworkStatsCollection
import android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID
import android.os.DropBoxManager
-import androidx.test.InstrumentationRegistry
+import androidx.test.platform.app.InstrumentationRegistry
import com.android.internal.util.FileRotator
import com.android.internal.util.FileRotator.Reader
-import com.android.server.connectivity.benchmarktests.R
import com.android.server.net.NetworkStatsRecorder
import java.io.BufferedInputStream
import java.io.DataInputStream
@@ -44,23 +43,22 @@
companion object {
private val DEFAULT_BUFFER_SIZE = 8192
private val FILE_CACHE_WARM_UP_REPEAT_COUNT = 10
- private val TEST_REPEAT_COUNT = 10
private val UID_COLLECTION_BUCKET_DURATION_MS = TimeUnit.HOURS.toMillis(2)
private val UID_RECORDER_ROTATE_AGE_MS = TimeUnit.DAYS.toMillis(15)
private val UID_RECORDER_DELETE_AGE_MS = TimeUnit.DAYS.toMillis(90)
+ private val TEST_DATASET_SUBFOLDER = "dataset/"
- private val testFilesDir by lazy {
- // These file generated by using real user dataset which has many uid records
- // and agreed to share the dataset for testing purpose. These dataset can be
- // extracted from rooted devices by using
- // "adb pull /data/misc/apexdata/com.android.tethering/netstats" command.
- val zipInputStream =
- ZipInputStream(getInputStreamForResource(R.raw.netstats_many_uids_zip))
- unzipToTempDir(zipInputStream)
- }
-
- private val uidTestFiles: List<File> by lazy {
- getSortedListForPrefix(testFilesDir, "uid")
+ // These files are generated by using real user dataset which has many uid records
+ // and agreed to share the dataset for testing purpose. These dataset can be
+ // extracted from rooted devices by using
+ // "adb pull /data/misc/apexdata/com.android.tethering/netstats" command.
+ private val testFilesAssets by lazy {
+ val zipFiles = context.assets.list(TEST_DATASET_SUBFOLDER)!!.asList()
+ zipFiles.map {
+ val zipInputStream =
+ ZipInputStream((TEST_DATASET_SUBFOLDER + it).toAssetInputStream())
+ File(unzipToTempDir(zipInputStream), "netstats")
+ }
}
// Test results shows the test cases who read the file first will take longer time to
@@ -72,24 +70,34 @@
@BeforeClass
fun setUpOnce() {
repeat(FILE_CACHE_WARM_UP_REPEAT_COUNT) {
- val collection = NetworkStatsCollection(UID_COLLECTION_BUCKET_DURATION_MS)
- for (file in uidTestFiles) {
- readFile(file, collection)
+ testFilesAssets.forEach {
+ val uidTestFiles = getSortedListForPrefix(it, "uid")
+ val collection = NetworkStatsCollection(UID_COLLECTION_BUCKET_DURATION_MS)
+ for (file in uidTestFiles) {
+ readFile(file, collection)
+ }
}
}
}
- private fun getInputStreamForResource(resourceId: Int): DataInputStream =
- DataInputStream(
- InstrumentationRegistry.getContext()
- .getResources().openRawResource(resourceId)
- )
+ val context get() = InstrumentationRegistry.getInstrumentation().getContext()
+ private fun String.toAssetInputStream() = DataInputStream(context.assets.open(this))
private fun unzipToTempDir(zis: ZipInputStream): File {
val statsDir =
Files.createTempDirectory(NetworkStatsTest::class.simpleName).toFile()
generateSequence { zis.nextEntry }.forEach { entry ->
- FileOutputStream(File(statsDir, entry.name)).use {
+ val entryFile = File(statsDir, entry.name)
+ if (entry.isDirectory) {
+ entryFile.mkdirs()
+ return@forEach
+ }
+
+ // Make sure all folders exists. There is no guarantee anywhere.
+ entryFile.parentFile!!.mkdirs()
+
+ // If the entry is a file extract it.
+ FileOutputStream(entryFile).use {
zis.copyTo(it, DEFAULT_BUFFER_SIZE)
}
}
@@ -99,7 +107,7 @@
// List [xt|uid|uid_tag].<start>-<end> files under the given directory.
private fun getSortedListForPrefix(statsDir: File, prefix: String): List<File> {
assertTrue(statsDir.exists())
- return statsDir.list() { dir, name -> name.startsWith("$prefix.") }
+ return statsDir.list { _, name -> name.startsWith("$prefix.") }
.orEmpty()
.map { it -> File(statsDir, it) }
.sorted()
@@ -115,7 +123,8 @@
fun testReadCollection_manyUids() {
// The file cache is warmed up by the @BeforeClass method, so now the test can repeat
// this a number of time to have a stable number.
- repeat(TEST_REPEAT_COUNT) {
+ testFilesAssets.forEach {
+ val uidTestFiles = getSortedListForPrefix(it, "uid")
val collection = NetworkStatsCollection(UID_COLLECTION_BUCKET_DURATION_MS)
for (file in uidTestFiles) {
readFile(file, collection)
@@ -127,10 +136,10 @@
fun testReadFromRecorder_manyUids() {
val mockObserver = mock<NonMonotonicObserver<String>>()
val mockDropBox = mock<DropBoxManager>()
- repeat(TEST_REPEAT_COUNT) {
+ testFilesAssets.forEach {
val recorder = NetworkStatsRecorder(
FileRotator(
- testFilesDir, PREFIX_UID, UID_RECORDER_ROTATE_AGE_MS, UID_RECORDER_DELETE_AGE_MS
+ it, PREFIX_UID, UID_RECORDER_ROTATE_AGE_MS, UID_RECORDER_DELETE_AGE_MS
),
mockObserver,
mockDropBox,
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index aae3425..bec9a4a 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -26,6 +26,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -63,6 +64,7 @@
import static com.android.modules.utils.build.SdkLevel.isAtLeastR;
import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.MiscAsserts.assertEmpty;
import static com.android.testutils.MiscAsserts.assertThrows;
@@ -369,6 +371,9 @@
.addCapability(NET_CAPABILITY_INTERNET)
.addCapability(NET_CAPABILITY_EIMS)
.addCapability(NET_CAPABILITY_NOT_METERED);
+ if (isAtLeastV()) {
+ netCap.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ }
if (isAtLeastS()) {
final ArraySet<Integer> allowedUids = new ArraySet<>();
allowedUids.add(4);
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index 0610774..93cc911 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -27,7 +27,7 @@
import android.os.RemoteException;
public class MyServiceClient {
- private static final int TIMEOUT_MS = 5000;
+ private static final int TIMEOUT_MS = 20_000;
private static final String PACKAGE = MyServiceClient.class.getPackage().getName();
private static final String APP2_PACKAGE = PACKAGE + ".app2";
private static final String SERVICE_NAME = APP2_PACKAGE + ".MyService";
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 6de663a..b86de25 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -55,7 +55,7 @@
"junit-params",
"modules-utils-build",
"net-utils-framework-common",
- "truth-prebuilt",
+ "truth",
"TetheringIntegrationTestsBaseLib",
],
@@ -74,7 +74,10 @@
// devices.
android_test {
name: "CtsNetTestCases",
- defaults: ["CtsNetTestCasesDefaults", "ConnectivityNextEnableDefaults"],
+ defaults: [
+ "CtsNetTestCasesDefaults",
+ "ConnectivityNextEnableDefaults",
+ ],
static_libs: [
"DhcpPacketLib",
"NetworkStackApiCurrentShims",
@@ -129,7 +132,7 @@
}
android_test {
- name: "CtsNetTestCasesMaxTargetSdk33", // Must match CtsNetTestCasesMaxTargetSdk33 annotation.
+ name: "CtsNetTestCasesMaxTargetSdk33", // Must match CtsNetTestCasesMaxTargetSdk33 annotation.
defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
target_sdk_version: "33",
package_name: "android.net.cts.maxtargetsdk33",
@@ -137,17 +140,17 @@
}
android_test {
- name: "CtsNetTestCasesMaxTargetSdk31", // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
+ name: "CtsNetTestCasesMaxTargetSdk31", // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
target_sdk_version: "31",
- package_name: "android.net.cts.maxtargetsdk31", // CTS package names must be unique.
+ package_name: "android.net.cts.maxtargetsdk31", // CTS package names must be unique.
instrumentation_target_package: "android.net.cts.maxtargetsdk31",
}
android_test {
- name: "CtsNetTestCasesMaxTargetSdk30", // Must match CtsNetTestCasesMaxTargetSdk30 annotation.
+ name: "CtsNetTestCasesMaxTargetSdk30", // Must match CtsNetTestCasesMaxTargetSdk30 annotation.
defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
target_sdk_version: "30",
- package_name: "android.net.cts.maxtargetsdk30", // CTS package names must be unique.
+ package_name: "android.net.cts.maxtargetsdk30", // CTS package names must be unique.
instrumentation_target_package: "android.net.cts.maxtargetsdk30",
}
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 9b81a56..1f1dd5d 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -41,7 +41,7 @@
"mockwebserver",
"junit",
"junit-params",
- "truth-prebuilt",
+ "truth",
],
platform_apis: true,
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 5937655..392cba9 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -704,6 +704,7 @@
argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
any(LinkProperties::class.java),
any(NetworkCapabilities::class.java),
+ any(), // LocalNetworkConfig TODO : specify when it's public
any(NetworkScore::class.java),
any(NetworkAgentConfig::class.java),
eq(NetworkProvider.ID_NONE))
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 637ed26..594f3fb 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -19,8 +19,10 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
@@ -28,6 +30,8 @@
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static com.android.testutils.DevSdkIgnoreRuleKt.VANILLA_ICE_CREAM;
+
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertArrayEquals;
@@ -104,6 +108,23 @@
verifyNoCapabilities(nr);
}
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testForbiddenCapabilities() {
+ final NetworkRequest.Builder builder = new NetworkRequest.Builder();
+ builder.addForbiddenCapability(NET_CAPABILITY_MMS);
+ assertTrue(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+ builder.removeForbiddenCapability(NET_CAPABILITY_MMS);
+ assertFalse(builder.build().hasCapability(NET_CAPABILITY_MMS));
+ builder.addCapability(NET_CAPABILITY_MMS);
+ assertFalse(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+ assertTrue(builder.build().hasCapability(NET_CAPABILITY_MMS));
+ builder.addForbiddenCapability(NET_CAPABILITY_MMS);
+ assertTrue(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+ assertFalse(builder.build().hasCapability(NET_CAPABILITY_MMS));
+ builder.clearCapabilities();
+ verifyNoCapabilities(builder.build());
+ }
+
@Test @IgnoreUpTo(Build.VERSION_CODES.Q)
public void testTemporarilyNotMeteredCapability() {
assertTrue(new NetworkRequest.Builder()
@@ -472,6 +493,32 @@
assertArrayEquals(netCapabilities, nr.getCapabilities());
}
+ @Test @IgnoreUpTo(VANILLA_ICE_CREAM)
+ public void testDefaultCapabilities() {
+ final NetworkRequest defaultNR = new NetworkRequest.Builder().build();
+ assertTrue(defaultNR.hasForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK));
+ assertFalse(defaultNR.hasCapability(NET_CAPABILITY_LOCAL_NETWORK));
+ assertTrue(defaultNR.hasCapability(NET_CAPABILITY_NOT_VPN));
+
+ final NetworkCapabilities emptyNC =
+ NetworkCapabilities.Builder.withoutDefaultCapabilities().build();
+ assertFalse(defaultNR.canBeSatisfiedBy(emptyNC));
+
+ // defaultNC represent the capabilities of a network agent, so they must not contain
+ // forbidden capabilities by default.
+ final NetworkCapabilities defaultNC = new NetworkCapabilities.Builder().build();
+ assertArrayEquals(new int[0], defaultNC.getForbiddenCapabilities());
+ // A default NR can be satisfied by default NC.
+ assertTrue(defaultNR.canBeSatisfiedBy(defaultNC));
+
+ // Conversely, network requests have forbidden capabilities by default to manage
+ // backward compatibility, so test that these forbidden capabilities are in place.
+ // Starting in V, NET_CAPABILITY_LOCAL_NETWORK is introduced but is not seen by
+ // default, thanks to a default forbidden capability in NetworkRequest.
+ defaultNC.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ assertFalse(defaultNR.canBeSatisfiedBy(defaultNC));
+ }
+
@Test
public void testBuildRequestFromExistingRequestWithBuilder() {
assumeTrue(TestUtils.shouldTestSApis());
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 12919ae..f705e34 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -45,6 +45,7 @@
// order-dependent setup.
"NetworkStackApiStableLib",
"androidx.test.ext.junit",
+ "compatibility-device-util-axt",
"frameworks-net-integration-testutils",
"kotlin-reflect",
"mockito-target-extended-minus-junit4",
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index e264b55..9b082a4 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -40,11 +40,14 @@
import android.os.IBinder
import android.os.SystemConfigManager
import android.os.UserHandle
+import android.os.VintfRuntimeInfo
import android.testing.TestableContext
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil
import com.android.connectivity.resources.R
+import com.android.net.module.util.BpfUtils
import com.android.server.BpfNetMaps
import com.android.server.ConnectivityService
import com.android.server.NetworkAgentWrapper
@@ -53,6 +56,7 @@
import com.android.server.connectivity.MockableSystemProperties
import com.android.server.connectivity.MultinetworkPolicyTracker
import com.android.server.connectivity.ProxyTracker
+import com.android.testutils.DeviceInfoUtils
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestableNetworkCallback
import kotlin.test.assertEquals
@@ -60,6 +64,7 @@
import kotlin.test.assertTrue
import kotlin.test.fail
import org.junit.After
+import org.junit.Assume
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
@@ -302,4 +307,25 @@
!it.hasCapability(NET_CAPABILITY_VALIDATED)
}
}
+
+ private fun isBpfGetCgroupProgramIdSupportedByKernel(): Boolean {
+ val kVersionString = VintfRuntimeInfo.getKernelVersion()
+ return DeviceInfoUtils.compareMajorMinorVersion(kVersionString, "4.19") >= 0
+ }
+
+ @Test
+ fun testBpfProgramAttachStatus() {
+ Assume.assumeTrue(isBpfGetCgroupProgramIdSupportedByKernel())
+
+ listOf(
+ BpfUtils.BPF_CGROUP_INET_INGRESS,
+ BpfUtils.BPF_CGROUP_INET_EGRESS,
+ BpfUtils.BPF_CGROUP_INET_SOCK_CREATE
+ ).forEach {
+ val ret = SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
+ "cmd connectivity bpf-get-cgroup-program-id $it").trim()
+
+ assertTrue(Integer.parseInt(ret) > 0, "Unexpected output $ret for type $it")
+ }
+ }
}
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index 0c424e9..51a4eca 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -40,6 +40,7 @@
#define TETHERING "/sys/fs/bpf/tethering/"
#define PRIVATE "/sys/fs/bpf/net_private/"
#define SHARED "/sys/fs/bpf/net_shared/"
+#define NETD_RO "/sys/fs/bpf/netd_readonly/"
#define NETD "/sys/fs/bpf/netd_shared/"
class BpfExistenceTest : public ::testing::Test {
@@ -93,6 +94,7 @@
NETD "map_netd_app_uid_stats_map",
NETD "map_netd_configuration_map",
NETD "map_netd_cookie_tag_map",
+ NETD "map_netd_data_saver_enabled_map",
NETD "map_netd_iface_index_name_map",
NETD "map_netd_iface_stats_map",
NETD "map_netd_ingress_discard_map",
@@ -119,9 +121,9 @@
};
// Provided by *current* mainline module for T+ devices with 5.4+ kernels
-static const set<string> MAINLINE_FOR_T_5_4_PLUS = {
- SHARED "prog_block_bind4_block_port",
- SHARED "prog_block_bind6_block_port",
+static const set<string> MAINLINE_FOR_T_4_19_PLUS = {
+ NETD_RO "prog_block_bind4_block_port",
+ NETD_RO "prog_block_bind6_block_port",
};
// Provided by *current* mainline module for T+ devices with 5.15+ kernels
@@ -176,7 +178,7 @@
// T still only requires Linux Kernel 4.9+.
DO_EXPECT(IsAtLeastT(), MAINLINE_FOR_T_PLUS);
DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
- DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_T_5_4_PLUS);
+ DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
// U requires Linux Kernel 4.14+, but nothing (as yet) added or removed in U.
diff --git a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
new file mode 100644
index 0000000..facb932
--- /dev/null
+++ b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 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
+
+import android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY
+import android.net.BpfNetMapsUtils.getMatchByFirewallChain
+import android.os.Build
+import com.android.net.module.util.IBpfMap
+import com.android.net.module.util.Struct.S32
+import com.android.net.module.util.Struct.U32
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestBpfMap
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// pre-T devices does not support Bpf.
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class BpfNetMapsReaderTest {
+ private val testConfigurationMap: IBpfMap<S32, U32> = TestBpfMap()
+ private val testUidOwnerMap: IBpfMap<S32, UidOwnerValue> = TestBpfMap()
+ private val bpfNetMapsReader = BpfNetMapsReader(
+ TestDependencies(testConfigurationMap, testUidOwnerMap))
+
+ class TestDependencies(
+ private val configMap: IBpfMap<S32, U32>,
+ private val uidOwnerMap: IBpfMap<S32, UidOwnerValue>
+ ) : BpfNetMapsReader.Dependencies() {
+ override fun getConfigurationMap() = configMap
+ override fun getUidOwnerMap() = uidOwnerMap
+ }
+
+ private fun doTestIsChainEnabled(chain: Int) {
+ testConfigurationMap.updateEntry(
+ UID_RULES_CONFIGURATION_KEY,
+ U32(getMatchByFirewallChain(chain))
+ )
+ assertTrue(bpfNetMapsReader.isChainEnabled(chain))
+ testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
+ assertFalse(bpfNetMapsReader.isChainEnabled(chain))
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testIsChainEnabled() {
+ doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE)
+ doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_STANDBY)
+ doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_POWERSAVE)
+ doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_RESTRICTED)
+ doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY)
+ }
+}
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index 2170882..1e1fd35 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -54,6 +54,7 @@
import com.android.frameworks.tests.net.R;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.SkipPresubmit;
import org.junit.After;
import org.junit.Test;
@@ -343,6 +344,7 @@
}
+ @SkipPresubmit(reason = "Flaky: b/302325928; add to presubmit after fixing")
@Test
public void testFuzzing() throws Exception {
try {
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 5f280c6..ea2b261 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -17,6 +17,9 @@
package com.android.server;
import static android.net.BpfNetMapsConstants.CURRENT_STATS_MAP_CONFIGURATION_KEY;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_DISABLED;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
import static android.net.BpfNetMapsConstants.DOZABLE_MATCH;
import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
import static android.net.BpfNetMapsConstants.IIF_MATCH;
@@ -66,6 +69,7 @@
import android.content.Context;
import android.net.BpfNetMapsUtils;
import android.net.INetd;
+import android.net.UidOwnerValue;
import android.os.Build;
import android.os.ServiceSpecificException;
import android.system.ErrnoException;
@@ -140,6 +144,7 @@
private final IBpfMap<S32, U8> mUidPermissionMap = new TestBpfMap<>(S32.class, U8.class);
private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap =
spy(new TestBpfMap<>(CookieTagMapKey.class, CookieTagMapValue.class));
+ private final IBpfMap<S32, U8> mDataSaverEnabledMap = new TestBpfMap<>(S32.class, U8.class);
@Before
public void setUp() throws Exception {
@@ -154,6 +159,8 @@
BpfNetMaps.setUidOwnerMapForTest(mUidOwnerMap);
BpfNetMaps.setUidPermissionMapForTest(mUidPermissionMap);
BpfNetMaps.setCookieTagMapForTest(mCookieTagMap);
+ BpfNetMaps.setDataSaverEnabledMapForTest(mDataSaverEnabledMap);
+ mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
mBpfNetMaps = new BpfNetMaps(mContext, mNetd, mDeps);
}
@@ -1154,6 +1161,21 @@
assertDumpContains(getDump(), "cookie=123 tag=0x789 uid=456");
}
+ private void doTestDumpDataSaverConfig(final short value, final boolean expected)
+ throws Exception {
+ mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(value));
+ assertDumpContains(getDump(),
+ "sDataSaverEnabledMap: " + expected);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testDumpDataSaverConfig() throws Exception {
+ doTestDumpDataSaverConfig(DATA_SAVER_DISABLED, false);
+ doTestDumpDataSaverConfig(DATA_SAVER_ENABLED, true);
+ doTestDumpDataSaverConfig((short) 2, true);
+ }
+
@Test
public void testGetUids() throws ErrnoException {
final int uid0 = TEST_UIDS[0];
@@ -1182,4 +1204,23 @@
assertThrows(expected,
() -> mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(FIREWALL_CHAIN_OEM_DENY_1));
}
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
+ public void testSetDataSaverEnabledBeforeT() {
+ for (boolean enable : new boolean[] {true, false}) {
+ assertThrows(UnsupportedOperationException.class,
+ () -> mBpfNetMaps.setDataSaverEnabled(enable));
+ }
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testSetDataSaverEnabled() throws Exception {
+ for (boolean enable : new boolean[] {true, false}) {
+ mBpfNetMaps.setDataSaverEnabled(enable);
+ assertEquals(enable ? DATA_SAVER_ENABLED : DATA_SAVER_DISABLED,
+ mDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val);
+ }
+ }
}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index eb03157..82fb651 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -77,6 +77,7 @@
import static android.net.ConnectivityManager.TYPE_ETHERNET;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
+import static android.net.ConnectivityManager.TYPE_NONE;
import static android.net.ConnectivityManager.TYPE_VPN;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
@@ -418,7 +419,6 @@
import com.android.server.connectivity.UidRangeUtils;
import com.android.server.connectivity.Vpn;
import com.android.server.connectivity.VpnProfileStore;
-import com.android.server.net.LockdownVpnTracker;
import com.android.server.net.NetworkPinner;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -470,6 +470,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@@ -934,24 +935,39 @@
return appUid + (firstSdkSandboxUid - Process.FIRST_APPLICATION_UID);
}
- // This function assumes the UID range for user 0 ([1, 99999])
- private static UidRangeParcel[] uidRangeParcelsExcludingUids(Integer... excludedUids) {
- int start = 1;
- Arrays.sort(excludedUids);
- List<UidRangeParcel> parcels = new ArrayList<UidRangeParcel>();
+ // Create the list of ranges for the primary user (User 0), excluding excludedUids.
+ private static List<Range<Integer>> intRangesPrimaryExcludingUids(List<Integer> excludedUids) {
+ final List<Integer> excludedUidsList = new ArrayList<>(excludedUids);
+ // Uid 0 is always excluded
+ if (!excludedUidsList.contains(0)) {
+ excludedUidsList.add(0);
+ }
+ return intRangesExcludingUids(PRIMARY_USER, excludedUidsList);
+ }
+
+ private static List<Range<Integer>> intRangesExcludingUids(int userId,
+ List<Integer> excludedAppIds) {
+ final List<Integer> excludedUids = CollectionUtils.map(excludedAppIds,
+ appId -> UserHandle.getUid(userId, appId));
+ final int userBase = userId * UserHandle.PER_USER_RANGE;
+ final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
+
+ int start = userBase;
+ Collections.sort(excludedUids);
+ final List<Range<Integer>> ranges = new ArrayList<>();
for (int excludedUid : excludedUids) {
if (excludedUid == start) {
start++;
} else {
- parcels.add(new UidRangeParcel(start, excludedUid - 1));
+ ranges.add(new Range<>(start, excludedUid - 1));
start = excludedUid + 1;
}
}
- if (start <= 99999) {
- parcels.add(new UidRangeParcel(start, 99999));
+ if (start <= maxUid) {
+ ranges.add(new Range<>(start, maxUid));
}
- return parcels.toArray(new UidRangeParcel[0]);
+ return ranges;
}
private void waitForIdle() {
@@ -1499,15 +1515,7 @@
private int mVpnType = VpnManager.TYPE_VPN_SERVICE;
private UnderlyingNetworkInfo mUnderlyingNetworkInfo;
-
- // These ConditionVariables allow tests to wait for LegacyVpnRunner to be stopped/started.
- // TODO: this scheme is ad-hoc and error-prone because it does not fail if, for example, the
- // test expects two starts in a row, or even if the production code calls start twice in a
- // row. find a better solution. Simply putting a method to create a LegacyVpnRunner into
- // Vpn.Dependencies doesn't work because LegacyVpnRunner is not a static class and has
- // extensive access into the internals of Vpn.
- private ConditionVariable mStartLegacyVpnCv = new ConditionVariable();
- private ConditionVariable mStopVpnRunnerCv = new ConditionVariable();
+ private String mSessionKey;
public MockVpn(int userId) {
super(startHandlerThreadAndReturnLooper(), mServiceContext,
@@ -1668,36 +1676,52 @@
mInterface = null;
}
- @Override
- public void startLegacyVpnRunner() {
- mStartLegacyVpnCv.open();
+ private synchronized void startLegacyVpn() {
+ updateState(DetailedState.CONNECTING, "startLegacyVpn");
}
- public void expectStartLegacyVpnRunner() {
- assertTrue("startLegacyVpnRunner not called after " + TIMEOUT_MS + " ms",
- mStartLegacyVpnCv.block(TIMEOUT_MS));
-
- // startLegacyVpn calls stopVpnRunnerPrivileged, which will open mStopVpnRunnerCv, just
- // before calling startLegacyVpnRunner. Restore mStopVpnRunnerCv, so the test can expect
- // that the VpnRunner is stopped and immediately restarted by calling
- // expectStartLegacyVpnRunner() and expectStopVpnRunnerPrivileged() back-to-back.
- mStopVpnRunnerCv = new ConditionVariable();
+ // Mock the interaction of IkeV2VpnRunner start. In the context of ConnectivityService,
+ // setVpnDefaultForUids() is the main interaction and a sessionKey is stored.
+ private synchronized void startPlatformVpn() {
+ updateState(DetailedState.CONNECTING, "startPlatformVpn");
+ mSessionKey = UUID.randomUUID().toString();
+ // Assuming no disallowed applications
+ final Set<Range<Integer>> ranges = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
+ mCm.setVpnDefaultForUids(mSessionKey, ranges);
+ // Wait for vpn network preference updates.
+ waitForIdle();
}
@Override
- public void stopVpnRunnerPrivileged() {
- if (mVpnRunner != null) {
- super.stopVpnRunnerPrivileged();
- disconnect();
- mStartLegacyVpnCv = new ConditionVariable();
+ public void startLegacyVpnPrivileged(VpnProfile profile,
+ @Nullable Network underlying, @NonNull LinkProperties egress) {
+ switch (profile.type) {
+ case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
+ case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
+ case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
+ case VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS:
+ startPlatformVpn();
+ break;
+ case VpnProfile.TYPE_L2TP_IPSEC_PSK:
+ case VpnProfile.TYPE_L2TP_IPSEC_RSA:
+ case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
+ case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
+ case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
+ startLegacyVpn();
+ break;
+ default:
+ fail("Unknown VPN profile type");
}
- mVpnRunner = null;
- mStopVpnRunnerCv.open();
}
- public void expectStopVpnRunnerPrivileged() {
- assertTrue("stopVpnRunnerPrivileged not called after " + TIMEOUT_MS + " ms",
- mStopVpnRunnerCv.block(TIMEOUT_MS));
+ @Override
+ public synchronized void stopVpnRunnerPrivileged() {
+ if (mSessionKey != null) {
+ // Clear vpn network preference.
+ mCm.setVpnDefaultForUids(mSessionKey, Collections.EMPTY_LIST);
+ mSessionKey = null;
+ }
+ disconnect();
}
@Override
@@ -1711,6 +1735,14 @@
UnderlyingNetworkInfo underlyingNetworkInfo) {
mUnderlyingNetworkInfo = underlyingNetworkInfo;
}
+
+ @Override
+ public synchronized boolean setUnderlyingNetworks(@Nullable Network[] networks) {
+ if (!mAgentRegistered) return false;
+ mMockNetworkAgent.setUnderlyingNetworks(
+ (networks == null) ? null : Arrays.asList(networks));
+ return true;
+ }
}
private UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -1722,6 +1754,12 @@
return ranges.stream().map(r -> new UidRangeParcel(r, r)).toArray(UidRangeParcel[]::new);
}
+ private static UidRangeParcel[] intToUidRangeStableParcels(
+ final @NonNull List<Range<Integer>> ranges) {
+ return ranges.stream().map(
+ r -> new UidRangeParcel(r.getLower(), r.getUpper())).toArray(UidRangeParcel[]::new);
+ }
+
private void assertVpnTransportInfo(NetworkCapabilities nc, int type) {
assertNotNull(nc);
final TransportInfo ti = nc.getTransportInfo();
@@ -1854,6 +1892,8 @@
private static final UserHandle TERTIARY_USER_HANDLE = new UserHandle(TERTIARY_USER);
private static final int RESTRICTED_USER = 1;
+ private static final UidRange RESTRICTED_USER_UIDRANGE =
+ UidRange.createForUser(UserHandle.of(RESTRICTED_USER));
private static final UserInfo RESTRICTED_USER_INFO = new UserInfo(RESTRICTED_USER, "",
UserInfo.FLAG_RESTRICTED);
static {
@@ -2259,6 +2299,11 @@
}
@Override
+ public int getBpfProgramId(final int attachType) {
+ return 0;
+ }
+
+ @Override
public BroadcastOptionsShim makeBroadcastOptionsShim(BroadcastOptions options) {
reset(mBroadcastOptionsShim);
return mBroadcastOptionsShim;
@@ -9404,11 +9449,11 @@
&& c.hasTransport(TRANSPORT_WIFI));
callback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
- doReturn(UserHandle.getUid(RESTRICTED_USER, VPN_UID)).when(mPackageManager)
- .getPackageUidAsUser(ALWAYS_ON_PACKAGE, RESTRICTED_USER);
-
- // New user added
- mMockVpn.onUserAdded(RESTRICTED_USER);
+ // New user added, this updates the Vpn uids, coverage in VpnTest.
+ // This is equivalent to `mMockVpn.onUserAdded(RESTRICTED_USER);`
+ final Set<UidRange> ranges = uidRangesForUids(uid);
+ ranges.add(RESTRICTED_USER_UIDRANGE);
+ mMockVpn.setUids(ranges);
// Expect that the VPN UID ranges contain both |uid| and the UID range for the newly-added
// restricted user.
@@ -9433,7 +9478,9 @@
&& !c.hasTransport(TRANSPORT_WIFI));
// User removed and expect to lose the UID range for the restricted user.
- mMockVpn.onUserRemoved(RESTRICTED_USER);
+ // This updates the Vpn uids, coverage in VpnTest.
+ // This is equivalent to `mMockVpn.onUserRemoved(RESTRICTED_USER);`
+ mMockVpn.setUids(uidRangesForUids(uid));
// Expect that the VPN gains the UID range for the restricted user, and that the capability
// change made just before that (i.e., loss of TRANSPORT_WIFI) is preserved.
@@ -9474,8 +9521,16 @@
assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
// Enable always-on VPN lockdown. The main user loses network access because no VPN is up.
- final ArrayList<String> allowList = new ArrayList<>();
- mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+ // Coverage in VpnTest.
+ final List<Integer> excludedUids = new ArrayList<>();
+ excludedUids.add(VPN_UID);
+ if (mDeps.isAtLeastT()) {
+ // On T onwards, the corresponding SDK sandbox UID should also be excluded
+ excludedUids.add(toSdkSandboxUid(VPN_UID));
+ }
+ final List<Range<Integer>> primaryRanges = intRangesPrimaryExcludingUids(excludedUids);
+ mCm.setRequireVpnForUids(true, primaryRanges);
+
waitForIdle();
assertNull(mCm.getActiveNetworkForUid(uid));
// This is arguably overspecified: a UID that is not running doesn't have an active network.
@@ -9484,32 +9539,28 @@
assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
// Start the restricted profile, and check that the UID within it loses network access.
- doReturn(UserHandle.getUid(RESTRICTED_USER, VPN_UID)).when(mPackageManager)
- .getPackageUidAsUser(ALWAYS_ON_PACKAGE, RESTRICTED_USER);
- doReturn(asList(PRIMARY_USER_INFO, RESTRICTED_USER_INFO)).when(mUserManager)
- .getAliveUsers();
// TODO: check that VPN app within restricted profile still has access, etc.
- mMockVpn.onUserAdded(RESTRICTED_USER);
- final Intent addedIntent = new Intent(ACTION_USER_ADDED);
- addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER));
- addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER);
- processBroadcast(addedIntent);
+ // Add a restricted user.
+ // This is equivalent to `mMockVpn.onUserAdded(RESTRICTED_USER);`, coverage in VpnTest.
+ final List<Range<Integer>> restrictedRanges =
+ intRangesExcludingUids(RESTRICTED_USER, excludedUids);
+ mCm.setRequireVpnForUids(true, restrictedRanges);
+ waitForIdle();
+
assertNull(mCm.getActiveNetworkForUid(uid));
assertNull(mCm.getActiveNetworkForUid(restrictedUid));
// Stop the restricted profile, and check that the UID within it has network access again.
- doReturn(asList(PRIMARY_USER_INFO)).when(mUserManager).getAliveUsers();
+ // Remove the restricted user.
+ // This is equivalent to `mMockVpn.onUserRemoved(RESTRICTED_USER);`, coverage in VpnTest.
+ mCm.setRequireVpnForUids(false, restrictedRanges);
+ waitForIdle();
- // Send a USER_REMOVED broadcast and expect to lose the UID range for the restricted user.
- mMockVpn.onUserRemoved(RESTRICTED_USER);
- final Intent removedIntent = new Intent(ACTION_USER_REMOVED);
- removedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER));
- removedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER);
- processBroadcast(removedIntent);
assertNull(mCm.getActiveNetworkForUid(uid));
assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
- mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
+ mCm.setRequireVpnForUids(false, primaryRanges);
+
waitForIdle();
}
@@ -9962,18 +10013,20 @@
new Handler(ConnectivityThread.getInstanceLooper()));
final int uid = Process.myUid();
- final ArrayList<String> allowList = new ArrayList<>();
- mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
- waitForIdle();
- final Set<Integer> excludedUids = new ArraySet<Integer>();
+ // Enable always-on VPN lockdown, coverage in VpnTest.
+ final List<Integer> excludedUids = new ArrayList<Integer>();
excludedUids.add(VPN_UID);
if (mDeps.isAtLeastT()) {
// On T onwards, the corresponding SDK sandbox UID should also be excluded
excludedUids.add(toSdkSandboxUid(VPN_UID));
}
- final UidRangeParcel[] uidRangeParcels = uidRangeParcelsExcludingUids(
- excludedUids.toArray(new Integer[0]));
+
+ final List<Range<Integer>> primaryRanges = intRangesPrimaryExcludingUids(excludedUids);
+ mCm.setRequireVpnForUids(true, primaryRanges);
+ waitForIdle();
+
+ final UidRangeParcel[] uidRangeParcels = intToUidRangeStableParcels(primaryRanges);
InOrder inOrder = inOrder(mMockNetd);
expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
@@ -9993,7 +10046,8 @@
assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
// Disable lockdown, expect to see the network unblocked.
- mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
+ mCm.setRequireVpnForUids(false, primaryRanges);
+ waitForIdle();
callback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> !cb.getBlocked());
defaultCallback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> !cb.getBlocked());
vpnUidCallback.assertNoCallback();
@@ -10006,22 +10060,25 @@
assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
- // Add our UID to the allowlist and re-enable lockdown, expect network is not blocked.
- allowList.add(TEST_PACKAGE_NAME);
- mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+ // Add our UID to the allowlist, expect network is not blocked. Coverage in VpnTest.
+ excludedUids.add(uid);
+ if (mDeps.isAtLeastT()) {
+ // On T onwards, the corresponding SDK sandbox UID should also be excluded
+ excludedUids.add(toSdkSandboxUid(uid));
+ }
+ final List<Range<Integer>> primaryRangesExcludingUid =
+ intRangesPrimaryExcludingUids(excludedUids);
+ mCm.setRequireVpnForUids(true, primaryRangesExcludingUid);
+ waitForIdle();
+
callback.assertNoCallback();
defaultCallback.assertNoCallback();
vpnUidCallback.assertNoCallback();
vpnUidDefaultCallback.assertNoCallback();
vpnDefaultCallbackAsUid.assertNoCallback();
- excludedUids.add(uid);
- if (mDeps.isAtLeastT()) {
- // On T onwards, the corresponding SDK sandbox UID should also be excluded
- excludedUids.add(toSdkSandboxUid(uid));
- }
- final UidRangeParcel[] uidRangeParcelsAlsoExcludingUs = uidRangeParcelsExcludingUids(
- excludedUids.toArray(new Integer[0]));
+ final UidRangeParcel[] uidRangeParcelsAlsoExcludingUs =
+ intToUidRangeStableParcels(primaryRangesExcludingUid);
expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcelsAlsoExcludingUs);
assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetwork());
@@ -10044,15 +10101,15 @@
assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
- // Disable lockdown, remove our UID from the allowlist, and re-enable lockdown.
- // Everything should now be blocked.
- mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
+ // Disable lockdown
+ mCm.setRequireVpnForUids(false, primaryRangesExcludingUid);
waitForIdle();
expectNetworkRejectNonSecureVpn(inOrder, false, uidRangeParcelsAlsoExcludingUs);
- allowList.clear();
- mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+ // Remove our UID from the allowlist, and re-enable lockdown.
+ mCm.setRequireVpnForUids(true, primaryRanges);
waitForIdle();
expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
+ // Everything should now be blocked.
defaultCallback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> cb.getBlocked());
assertBlockedCallbackInAnyOrder(callback, true, mWiFiAgent, mCellAgent);
vpnUidCallback.assertNoCallback();
@@ -10065,7 +10122,7 @@
assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
// Disable lockdown. Everything is unblocked.
- mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
+ mCm.setRequireVpnForUids(false, primaryRanges);
defaultCallback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> !cb.getBlocked());
assertBlockedCallbackInAnyOrder(callback, false, mWiFiAgent, mCellAgent);
vpnUidCallback.assertNoCallback();
@@ -10077,36 +10134,8 @@
assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
- // Enable and disable an always-on VPN package without lockdown. Expect no changes.
- reset(mMockNetd);
- mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, false /* lockdown */, allowList);
- inOrder.verify(mMockNetd, never()).networkRejectNonSecureVpn(anyBoolean(), any());
- callback.assertNoCallback();
- defaultCallback.assertNoCallback();
- vpnUidCallback.assertNoCallback();
- vpnUidDefaultCallback.assertNoCallback();
- vpnDefaultCallbackAsUid.assertNoCallback();
- assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
- assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetwork());
- assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
- assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
- assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
-
- mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
- inOrder.verify(mMockNetd, never()).networkRejectNonSecureVpn(anyBoolean(), any());
- callback.assertNoCallback();
- defaultCallback.assertNoCallback();
- vpnUidCallback.assertNoCallback();
- vpnUidDefaultCallback.assertNoCallback();
- vpnDefaultCallbackAsUid.assertNoCallback();
- assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
- assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetwork());
- assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
- assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
- assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
-
// Enable lockdown and connect a VPN. The VPN is not blocked.
- mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+ mCm.setRequireVpnForUids(true, primaryRanges);
defaultCallback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> cb.getBlocked());
assertBlockedCallbackInAnyOrder(callback, true, mWiFiAgent, mCellAgent);
vpnUidCallback.assertNoCallback();
@@ -10192,7 +10221,7 @@
doAsUid(Process.SYSTEM_UID, () -> mCm.unregisterNetworkCallback(perUidCb));
}
- private VpnProfile setupLegacyLockdownVpn() {
+ private VpnProfile setupLockdownVpn(int profileType) {
final String profileName = "testVpnProfile";
final byte[] profileTag = profileName.getBytes(StandardCharsets.UTF_8);
doReturn(profileTag).when(mVpnProfileStore).get(Credentials.LOCKDOWN_VPN);
@@ -10201,7 +10230,9 @@
profile.name = "My VPN";
profile.server = "192.0.2.1";
profile.dnsServers = "8.8.8.8";
- profile.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK;
+ profile.ipsecIdentifier = "My ipsecIdentifier";
+ profile.ipsecSecret = "My PSK";
+ profile.type = profileType;
final byte[] encodedProfile = profile.encode();
doReturn(encodedProfile).when(mVpnProfileStore).get(Credentials.VPN + profileName);
@@ -10219,8 +10250,8 @@
mMockVpn.connect(true);
}
- @Test
- public void testLegacyLockdownVpn() throws Exception {
+ private void doTestLockdownVpn(VpnProfile profile, boolean expectSetVpnDefaultForUids)
+ throws Exception {
mServiceContext.setPermission(
Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
@@ -10235,107 +10266,64 @@
mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback,
new Handler(ConnectivityThread.getInstanceLooper()));
- // Pretend lockdown VPN was configured.
- final VpnProfile profile = setupLegacyLockdownVpn();
-
- // LockdownVpnTracker disables the Vpn teardown code and enables lockdown.
- // Check the VPN's state before it does so.
- assertTrue(mMockVpn.getEnableTeardown());
- assertFalse(mMockVpn.getLockdown());
-
- // VMSHandlerThread was used inside VpnManagerService and taken into LockDownVpnTracker.
- // VpnManagerService was decoupled from this test but this handlerThread is still required
- // in LockDownVpnTracker. Keep it until LockDownVpnTracker related verification is moved to
- // its own test.
- final HandlerThread VMSHandlerThread = new HandlerThread("TestVpnManagerService");
- VMSHandlerThread.start();
-
- // LockdownVpnTracker is created from VpnManagerService but VpnManagerService is decoupled
- // from ConnectivityServiceTest. Create it directly to simulate LockdownVpnTracker is
- // created.
- // TODO: move LockdownVpnTracker related tests to its own test.
- // Lockdown VPN disables teardown and enables lockdown.
- final LockdownVpnTracker lockdownVpnTracker = new LockdownVpnTracker(mServiceContext,
- VMSHandlerThread.getThreadHandler(), mMockVpn, profile);
- lockdownVpnTracker.init();
- assertFalse(mMockVpn.getEnableTeardown());
- assertTrue(mMockVpn.getLockdown());
+ // Init lockdown state to simulate LockdownVpnTracker behavior.
+ mCm.setLegacyLockdownVpnEnabled(true);
+ mMockVpn.setEnableTeardown(false);
+ final List<Range<Integer>> ranges =
+ intRangesPrimaryExcludingUids(Collections.EMPTY_LIST /* excludedeUids */);
+ mCm.setRequireVpnForUids(true /* requireVpn */, ranges);
// Bring up a network.
- // Expect nothing to happen because the network does not have an IPv4 default route: legacy
- // VPN only supports IPv4.
final LinkProperties cellLp = new LinkProperties();
cellLp.setInterfaceName("rmnet0");
- cellLp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
- cellLp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, "rmnet0"));
- mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
- mCellAgent.connect(false /* validated */);
- callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
- defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
- systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
- waitForIdle();
- assertNull(mMockVpn.getAgent());
-
- // Add an IPv4 address. Ideally the VPN should start, but it doesn't because nothing calls
- // LockdownVpnTracker#handleStateChangedLocked. This is a bug.
- // TODO: consider fixing this.
cellLp.addLinkAddress(new LinkAddress("192.0.2.2/25"));
cellLp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"), null, "rmnet0"));
- mCellAgent.sendLinkProperties(cellLp);
- callback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
- defaultCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
- systemDefaultCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
- waitForIdle();
- assertNull(mMockVpn.getAgent());
-
- // Disconnect, then try again with a network that supports IPv4 at connection time.
- // Expect lockdown VPN to come up.
- ExpectedBroadcast b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
- mCellAgent.disconnect();
- callback.expect(LOST, mCellAgent);
- defaultCallback.expect(LOST, mCellAgent);
- systemDefaultCallback.expect(LOST, mCellAgent);
- b1.expectBroadcast();
-
// When lockdown VPN is active, the NetworkInfo state in CONNECTIVITY_ACTION is overwritten
// with the state of the VPN network. So expect a CONNECTING broadcast.
- b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTING);
+ final ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTING);
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
mCellAgent.connect(false /* validated */);
callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
- b1.expectBroadcast();
- assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+ b.expectBroadcast();
+ // Simulate LockdownVpnTracker attempting to start the VPN since it received the
+ // systemDefault callback.
+ mMockVpn.startLegacyVpnPrivileged(profile, mCellAgent.getNetwork(), cellLp);
+ if (expectSetVpnDefaultForUids) {
+ // setVpnDefaultForUids() releases the original network request and creates a VPN
+ // request so LOST callback is received.
+ defaultCallback.expect(LOST, mCellAgent);
+ // Due to the VPN default request, getActiveNetworkInfo() gets the mNoServiceNetwork
+ // as the network satisfier.
+ assertNull(mCm.getActiveNetworkInfo());
+ } else {
+ assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED);
assertExtraInfoFromCmBlocked(mCellAgent);
- // TODO: it would be nice if we could simply rely on the production code here, and have
- // LockdownVpnTracker start the VPN, have the VPN code register its NetworkAgent with
- // ConnectivityService, etc. That would require duplicating a fair bit of code from the
- // Vpn tests around how to mock out LegacyVpnRunner. But even if we did that, this does not
- // work for at least two reasons:
- // 1. In this test, calling registerNetworkAgent does not actually result in an agent being
- // registered. This is because nothing calls onNetworkMonitorCreated, which is what
- // actually ends up causing handleRegisterNetworkAgent to be called. Code in this test
- // that wants to register an agent must use TestNetworkAgentWrapper.
- // 2. Even if we exposed Vpn#agentConnect to the test, and made MockVpn#agentConnect call
- // the TestNetworkAgentWrapper code, this would deadlock because the
- // TestNetworkAgentWrapper code cannot be called on the handler thread since it calls
- // waitForIdle().
- mMockVpn.expectStartLegacyVpnRunner();
- b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
- ExpectedBroadcast b2 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED);
+ final ExpectedBroadcast b2 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
+ final ExpectedBroadcast b3 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED);
establishLegacyLockdownVpn(mCellAgent.getNetwork());
callback.expectAvailableThenValidatedCallbacks(mMockVpn);
defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
systemDefaultCallback.assertNoCallback();
- NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
- b1.expectBroadcast();
+ final NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
b2.expectBroadcast();
- assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+ b3.expectBroadcast();
+ if (expectSetVpnDefaultForUids) {
+ // Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
+ // network satisfier which has TYPE_VPN.
+ assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+ } else {
+ // LegacyVpnRunner does not call setVpnDefaultsForUids(), which means
+ // getActiveNetworkInfo() can only return the info for the system-wide default instead.
+ // This should be fixed, but LegacyVpnRunner will be removed soon anyway.
+ assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10356,53 +10344,78 @@
wifiNc.addCapability(NET_CAPABILITY_NOT_METERED);
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp, wifiNc);
- b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
+ final ExpectedBroadcast b4 =
+ expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
// Wifi is CONNECTING because the VPN isn't up yet.
- b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTING);
- ExpectedBroadcast b3 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+ final ExpectedBroadcast b5 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTING);
mWiFiAgent.connect(false /* validated */);
- b1.expectBroadcast();
- b2.expectBroadcast();
- b3.expectBroadcast();
- mMockVpn.expectStopVpnRunnerPrivileged();
- mMockVpn.expectStartLegacyVpnRunner();
-
- // TODO: why is wifi not blocked? Is it because when this callback is sent, the VPN is still
- // connected, so the network is not considered blocked by the lockdown UID ranges? But the
- // fact that a VPN is connected should only result in the VPN itself being unblocked, not
- // any other network. Bug in isUidBlockedByVpn?
+ // Wifi is not blocked since VPN network is still connected.
callback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+ defaultCallback.assertNoCallback();
+ systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+ b4.expectBroadcast();
+ b5.expectBroadcast();
+
+ // Simulate LockdownVpnTracker restarting the VPN since it received the systemDefault
+ // callback with different network.
+ final ExpectedBroadcast b6 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+ mMockVpn.stopVpnRunnerPrivileged();
+ mMockVpn.startLegacyVpnPrivileged(profile, mWiFiAgent.getNetwork(), wifiLp);
+ // VPN network is disconnected (to restart)
callback.expect(LOST, mMockVpn);
defaultCallback.expect(LOST, mMockVpn);
+ // The network preference is cleared when VPN is disconnected so it receives callbacks for
+ // the system-wide default.
defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiAgent);
- systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+ if (expectSetVpnDefaultForUids) {
+ // setVpnDefaultForUids() releases the original network request and creates a VPN
+ // request so LOST callback is received.
+ defaultCallback.expect(LOST, mWiFiAgent);
+ }
+ systemDefaultCallback.assertNoCallback();
+ b6.expectBroadcast();
// While the VPN is reconnecting on the new network, everything is blocked.
- assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+ if (expectSetVpnDefaultForUids) {
+ // Due to the VPN default request, getActiveNetworkInfo() gets the mNoServiceNetwork
+ // as the network satisfier.
+ assertNull(mCm.getActiveNetworkInfo());
+ } else {
+ assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED);
assertExtraInfoFromCmBlocked(mWiFiAgent);
// The VPN comes up again on wifi.
- b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
- b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
+ final ExpectedBroadcast b7 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
+ final ExpectedBroadcast b8 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
establishLegacyLockdownVpn(mWiFiAgent.getNetwork());
callback.expectAvailableThenValidatedCallbacks(mMockVpn);
defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
systemDefaultCallback.assertNoCallback();
- b1.expectBroadcast();
- b2.expectBroadcast();
- assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+ b7.expectBroadcast();
+ b8.expectBroadcast();
+ if (expectSetVpnDefaultForUids) {
+ // Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
+ // network satisfier which has TYPE_VPN.
+ assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+ } else {
+ // LegacyVpnRunner does not call setVpnDefaultsForUids(), which means
+ // getActiveNetworkInfo() can only return the info for the system-wide default instead.
+ // This should be fixed, but LegacyVpnRunner will be removed soon anyway.
+ assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
assertExtraInfoFromCmPresent(mWiFiAgent);
- vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
- assertTrue(vpnNc.hasTransport(TRANSPORT_VPN));
- assertTrue(vpnNc.hasTransport(TRANSPORT_WIFI));
- assertFalse(vpnNc.hasTransport(TRANSPORT_CELLULAR));
- assertTrue(vpnNc.hasCapability(NET_CAPABILITY_NOT_METERED));
+ final NetworkCapabilities vpnNc2 = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
+ assertTrue(vpnNc2.hasTransport(TRANSPORT_VPN));
+ assertTrue(vpnNc2.hasTransport(TRANSPORT_WIFI));
+ assertFalse(vpnNc2.hasTransport(TRANSPORT_CELLULAR));
+ assertTrue(vpnNc2.hasCapability(NET_CAPABILITY_NOT_METERED));
// Disconnect cell. Nothing much happens since it's not the default network.
mCellAgent.disconnect();
@@ -10410,30 +10423,61 @@
defaultCallback.assertNoCallback();
systemDefaultCallback.assertNoCallback();
- assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+ if (expectSetVpnDefaultForUids) {
+ // Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
+ // network satisfier which has TYPE_VPN.
+ assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+ } else {
+ // LegacyVpnRunner does not call setVpnDefaultsForUids(), which means
+ // getActiveNetworkInfo() can only return the info for the system-wide default instead.
+ // This should be fixed, but LegacyVpnRunner will be removed soon anyway.
+ assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
assertExtraInfoFromCmPresent(mWiFiAgent);
- b1 = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED);
- b2 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+ final ExpectedBroadcast b9 =
+ expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED);
+ final ExpectedBroadcast b10 =
+ expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
mWiFiAgent.disconnect();
callback.expect(LOST, mWiFiAgent);
- systemDefaultCallback.expect(LOST, mWiFiAgent);
- b1.expectBroadcast();
callback.expectCaps(mMockVpn, c -> !c.hasTransport(TRANSPORT_WIFI));
- mMockVpn.expectStopVpnRunnerPrivileged();
+ defaultCallback.expectCaps(mMockVpn, c -> !c.hasTransport(TRANSPORT_WIFI));
+ systemDefaultCallback.expect(LOST, mWiFiAgent);
+ // TODO: There should only be one LOST callback. Since the WIFI network is underlying a VPN
+ // network, ConnectivityService#propagateUnderlyingNetworkCapabilities() causes a rematch to
+ // occur. Notably, this happens before setting the satisfiers of its network requests to
+ // null. Since the satisfiers are set to null in the rematch, an extra LOST callback is
+ // called.
+ systemDefaultCallback.expect(LOST, mWiFiAgent);
+ b9.expectBroadcast();
+ mMockVpn.stopVpnRunnerPrivileged();
callback.expect(LOST, mMockVpn);
- b2.expectBroadcast();
+ defaultCallback.expect(LOST, mMockVpn);
+ b10.expectBroadcast();
- VMSHandlerThread.quitSafely();
- VMSHandlerThread.join();
+ assertNoCallbacks(callback, defaultCallback, systemDefaultCallback);
+ }
+
+ @Test
+ public void testLockdownVpn_LegacyVpnRunner() throws Exception {
+ final VpnProfile profile = setupLockdownVpn(VpnProfile.TYPE_IPSEC_XAUTH_PSK);
+ doTestLockdownVpn(profile, false /* expectSetVpnDefaultForUids */);
+ }
+
+ @Test
+ public void testLockdownVpn_Ikev2VpnRunner() throws Exception {
+ final VpnProfile profile = setupLockdownVpn(VpnProfile.TYPE_IKEV2_IPSEC_PSK);
+ doTestLockdownVpn(profile, true /* expectSetVpnDefaultForUids */);
}
@Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testLockdownSetFirewallUidRule() throws Exception {
- final Set<Range<Integer>> lockdownRange = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
+ final List<Range<Integer>> lockdownRange =
+ intRangesPrimaryExcludingUids(Collections.EMPTY_LIST /* excludedeUids */);
// Enable Lockdown
mCm.setRequireVpnForUids(true /* requireVpn */, lockdownRange);
waitForIdle();
@@ -12798,7 +12842,8 @@
private NetworkAgentInfo fakeNai(NetworkCapabilities nc, NetworkInfo networkInfo) {
return new NetworkAgentInfo(null, new Network(NET_ID), networkInfo, new LinkProperties(),
- nc, new NetworkScore.Builder().setLegacyInt(0).build(),
+ nc, null /* localNetworkConfig */,
+ new NetworkScore.Builder().setLegacyInt(0).build(),
mServiceContext, null, new NetworkAgentConfig(), mService, null, null, 0,
INVALID_UID, TEST_LINGER_DELAY_MS, mQosCallbackTracker,
new ConnectivityService.Dependencies());
@@ -13190,7 +13235,7 @@
}
@Test
- public void testDumpDoesNotCrash() {
+ public void testDumpDoesNotCrash() throws Exception {
mServiceContext.setPermission(DUMP, PERMISSION_GRANTED);
// Filing a couple requests prior to testing the dump.
final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback();
@@ -13202,6 +13247,44 @@
mCm.registerNetworkCallback(genericRequest, genericNetworkCallback);
mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
+ // NetworkProvider
+ final NetworkProvider wifiProvider = new NetworkProvider(mServiceContext,
+ mCsHandlerThread.getLooper(), "Wifi provider");
+ mCm.registerNetworkProvider(wifiProvider);
+
+ // NetworkAgent
+ final LinkProperties wifiLp = new LinkProperties();
+ wifiLp.setInterfaceName(WIFI_IFNAME);
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+ mWiFiAgent.connect(true);
+
+ // NetworkOffer
+ final NetworkScore wifiScore = new NetworkScore.Builder().build();
+ final NetworkCapabilities wifiCaps = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build();
+ final TestableNetworkOfferCallback wifiCallback = new TestableNetworkOfferCallback(
+ TIMEOUT_MS /* timeout */, TEST_CALLBACK_TIMEOUT_MS /* noCallbackTimeout */);
+ wifiProvider.registerNetworkOffer(wifiScore, wifiCaps, r -> r.run(), wifiCallback);
+
+ // Profile preferences
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent();
+ workAgent.connect(true);
+ mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+ null /* executor */, null /* listener */);
+
+ // OEM preferences
+ @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+ OEM_NETWORK_PREFERENCE_OEM_PAID;
+ setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+ setOemNetworkPreference(networkPref, TEST_PACKAGE_NAME);
+
+ // Mobile data preferred UIDs
+ setAndUpdateMobileDataPreferredUids(Set.of(TEST_PACKAGE_UID));
+
verifyDump(new String[0]);
// Verify dump with arguments.
diff --git a/tests/unit/java/com/android/server/HandlerUtilsTest.kt b/tests/unit/java/com/android/server/HandlerUtilsTest.kt
new file mode 100644
index 0000000..62bb651
--- /dev/null
+++ b/tests/unit/java/com/android/server/HandlerUtilsTest.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.os.HandlerThread
+import com.android.server.connectivity.HandlerUtils
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+const val THREAD_BLOCK_TIMEOUT_MS = 1000L
+const val TEST_REPEAT_COUNT = 100
+@RunWith(DevSdkIgnoreRunner::class)
+class HandlerUtilsTest {
+ val handlerThread = HandlerThread("HandlerUtilsTestHandlerThread").also {
+ it.start()
+ }
+ val handler = handlerThread.threadHandler
+
+ @Test
+ fun testRunWithScissors() {
+ // Repeat the test a fair amount of times to ensure that it does not pass by chance.
+ repeat(TEST_REPEAT_COUNT) {
+ var result = false
+ HandlerUtils.runWithScissors(handler, {
+ assertEquals(Thread.currentThread(), handlerThread)
+ result = true
+ }, THREAD_BLOCK_TIMEOUT_MS)
+ // Assert that the result is modified on the handler thread, but can also be seen from
+ // the current thread. The assertion should pass if the runWithScissors provides
+ // the guarantee where the assignment happens-before the assertion.
+ assertTrue(result)
+ }
+ }
+
+ @After
+ fun tearDown() {
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+}
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 71bd330..771edb2 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -35,14 +35,17 @@
import static android.net.nsd.NsdManager.FAILURE_BAD_PARAMETERS;
import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
+
import static com.android.networkstack.apishim.api33.ConstantsShim.REGISTER_NSD_OFFLOAD_ENGINE;
import static com.android.server.NsdService.DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF;
import static com.android.server.NsdService.MdnsListener;
import static com.android.server.NsdService.NO_TRANSACTION;
import static com.android.server.NsdService.parseTypeAndSubtype;
import static com.android.testutils.ContextUtils.mockService;
+
import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -220,7 +223,7 @@
anyInt(), anyString(), anyString(), anyString(), anyInt());
doReturn(false).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class));
doReturn(mDiscoveryManager).when(mDeps)
- .makeMdnsDiscoveryManager(any(), any(), any());
+ .makeMdnsDiscoveryManager(any(), any(), any(), any());
doReturn(mMulticastLock).when(mWifiManager).createMulticastLock(any());
doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any(), any(), any());
doReturn(DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF).when(mDeps).getDeviceConfigInt(
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index 24aecdb..9a6fd38 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -169,10 +169,12 @@
lp.addDnsServer(InetAddress.getByName("4.4.4.4"));
// Send a validation event that is tracked on the alternate netId
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setTransportTypes(TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
- mDnsManager.updateTransportsForNetwork(TEST_NETID_ALTERNATE, TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID_ALTERNATE, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID_ALTERNATE, lp);
mDnsManager.flushVmDnsCache();
mDnsManager.updatePrivateDnsValidation(
@@ -205,7 +207,7 @@
InetAddress.parseNumericAddress("6.6.6.6"),
InetAddress.parseNumericAddress("2001:db8:66:66::1")
}));
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
fixedLp = new LinkProperties(lp);
@@ -242,7 +244,9 @@
// be tracked.
LinkProperties lp = new LinkProperties();
lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setTransportTypes(TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
mDnsManager.updatePrivateDnsValidation(
@@ -256,7 +260,7 @@
// Validation event has untracked netId
mDnsManager.updatePrivateDns(new Network(TEST_NETID),
mDnsManager.getPrivateDnsConfig());
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
mDnsManager.updatePrivateDnsValidation(
@@ -307,7 +311,7 @@
ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_OFF);
mDnsManager.updatePrivateDns(new Network(TEST_NETID),
mDnsManager.getPrivateDnsConfig());
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
mDnsManager.updatePrivateDnsValidation(
@@ -352,7 +356,9 @@
lp.setInterfaceName(TEST_IFACENAME);
lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
lp.addDnsServer(InetAddress.getByName("4.4.4.4"));
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setTransportTypes(TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
index e6c0c83..07883ff 100644
--- a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -372,9 +372,10 @@
caps.addCapability(0);
caps.addTransportType(transport);
NetworkAgentInfo nai = new NetworkAgentInfo(null, new Network(netId), info,
- new LinkProperties(), caps, new NetworkScore.Builder().setLegacyInt(50).build(),
- mCtx, null, new NetworkAgentConfig.Builder().build(), mConnService, mNetd,
- mDnsResolver, NetworkProvider.ID_NONE, Binder.getCallingUid(), TEST_LINGER_DELAY_MS,
+ new LinkProperties(), caps, null /* localNetworkConfiguration */,
+ new NetworkScore.Builder().setLegacyInt(50).build(), mCtx, null,
+ new NetworkAgentConfig.Builder().build(), mConnService, mNetd, mDnsResolver,
+ NetworkProvider.ID_NONE, Binder.getCallingUid(), TEST_LINGER_DELAY_MS,
mQosCallbackTracker, new ConnectivityService.Dependencies());
if (setEverValidated) {
// As tests in this class deal with testing lingering, most tests are interested
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 56346ad..48cfe77 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -579,6 +579,18 @@
}
@Test
+ public void testAlwaysOnWithoutLockdown() throws Exception {
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ assertTrue(vpn.setAlwaysOnPackage(
+ PKGS[1], false /* lockdown */, null /* lockdownAllowlist */));
+ verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any());
+
+ assertTrue(vpn.setAlwaysOnPackage(
+ null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
+ verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any());
+ }
+
+ @Test
public void testLockdownChangingPackage() throws Exception {
final Vpn vpn = createVpn(PRIMARY_USER.id);
final Range<Integer> user = PRIMARY_USER_RANGE;
@@ -724,6 +736,37 @@
}
@Test
+ public void testLockdownSystemUser() throws Exception {
+ final Vpn vpn = createVpn(SYSTEM_USER_ID);
+
+ // Uid 0 is always excluded and PKG_UIDS[1] is the uid of the VPN.
+ final List<Integer> excludedUids = new ArrayList<>(List.of(0, PKG_UIDS[1]));
+ final List<Range<Integer>> ranges = makeVpnUidRange(SYSTEM_USER_ID, excludedUids);
+
+ // Set always-on with lockdown.
+ assertTrue(vpn.setAlwaysOnPackage(
+ PKGS[1], true /* lockdown */, null /* lockdownAllowlist */));
+ verify(mConnectivityManager).setRequireVpnForUids(true, ranges);
+
+ // Disable always-on with lockdown.
+ assertTrue(vpn.setAlwaysOnPackage(
+ null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
+ verify(mConnectivityManager).setRequireVpnForUids(false, ranges);
+
+ // Set always-on with lockdown and allow the app PKGS[2].
+ excludedUids.add(PKG_UIDS[2]);
+ final List<Range<Integer>> ranges2 = makeVpnUidRange(SYSTEM_USER_ID, excludedUids);
+ assertTrue(vpn.setAlwaysOnPackage(
+ PKGS[1], true /* lockdown */, Collections.singletonList(PKGS[2])));
+ verify(mConnectivityManager).setRequireVpnForUids(true, ranges2);
+
+ // Disable always-on with lockdown.
+ assertTrue(vpn.setAlwaysOnPackage(
+ null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
+ verify(mConnectivityManager).setRequireVpnForUids(false, ranges2);
+ }
+
+ @Test
public void testLockdownRuleRepeatability() throws Exception {
final Vpn vpn = createVpn(PRIMARY_USER.id);
final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
@@ -788,6 +831,101 @@
}
@Test
+ public void testOnUserAddedAndRemoved_restrictedUser() throws Exception {
+ final InOrder order = inOrder(mMockNetworkAgent);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final Set<Range<Integer>> initialRange = rangeSet(PRIMARY_USER_RANGE);
+ // Note since mVpnProfile is a Ikev2VpnProfile, this starts an IkeV2VpnRunner.
+ startLegacyVpn(vpn, mVpnProfile);
+ // Set an initial Uid range and mock the network agent
+ vpn.mNetworkCapabilities.setUids(initialRange);
+ vpn.mNetworkAgent = mMockNetworkAgent;
+
+ // Add the restricted user
+ setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
+ vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
+ // Expect restricted user range to be added to the NetworkCapabilities.
+ final Set<Range<Integer>> expectRestrictedRange =
+ rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id));
+ assertEquals(expectRestrictedRange, vpn.mNetworkCapabilities.getUids());
+ order.verify(mMockNetworkAgent).doSendNetworkCapabilities(
+ argThat(nc -> expectRestrictedRange.equals(nc.getUids())));
+
+ // Remove the restricted user
+ vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
+ // Expect restricted user range to be removed from the NetworkCapabilities.
+ assertEquals(initialRange, vpn.mNetworkCapabilities.getUids());
+ order.verify(mMockNetworkAgent).doSendNetworkCapabilities(
+ argThat(nc -> initialRange.equals(nc.getUids())));
+ }
+
+ @Test
+ public void testOnUserAddedAndRemoved_restrictedUserLockdown() throws Exception {
+ final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
+ new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
+ final Range<Integer> restrictedUserRange = uidRangeForUser(RESTRICTED_PROFILE_A.id);
+ final UidRangeParcel[] restrictedUserRangeParcel = new UidRangeParcel[] {
+ new UidRangeParcel(restrictedUserRange.getLower(), restrictedUserRange.getUpper())};
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+
+ // Set lockdown calls setRequireVpnForUids
+ vpn.setLockdown(true);
+ verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(primaryUserRangeParcel));
+
+ // Add the restricted user
+ doReturn(true).when(mUserManager).canHaveRestrictedProfile();
+ setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
+ vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
+
+ // Expect restricted user range to be added.
+ verify(mConnectivityManager).setRequireVpnForUids(true,
+ toRanges(restrictedUserRangeParcel));
+
+ // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not
+ // return the restricted user but it is still returned in mUserManager.getUserInfo().
+ RESTRICTED_PROFILE_A.partial = true;
+ // Remove the restricted user
+ vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
+ verify(mConnectivityManager).setRequireVpnForUids(false,
+ toRanges(restrictedUserRangeParcel));
+ // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static.
+ RESTRICTED_PROFILE_A.partial = false;
+ }
+
+ @Test
+ public void testOnUserAddedAndRemoved_restrictedUserAlwaysOn() throws Exception {
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+
+ // setAlwaysOnPackage() calls setRequireVpnForUids()
+ assertTrue(vpn.setAlwaysOnPackage(
+ PKGS[0], true /* lockdown */, null /* lockdownAllowlist */));
+ final List<Integer> excludedUids = List.of(PKG_UIDS[0]);
+ final List<Range<Integer>> primaryRanges =
+ makeVpnUidRange(PRIMARY_USER.id, excludedUids);
+ verify(mConnectivityManager).setRequireVpnForUids(true, primaryRanges);
+
+ // Add the restricted user
+ doReturn(true).when(mUserManager).canHaveRestrictedProfile();
+ setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
+ vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
+
+ final List<Range<Integer>> restrictedRanges =
+ makeVpnUidRange(RESTRICTED_PROFILE_A.id, excludedUids);
+ // Expect restricted user range to be added.
+ verify(mConnectivityManager).setRequireVpnForUids(true, restrictedRanges);
+
+ // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not
+ // return the restricted user but it is still returned in mUserManager.getUserInfo().
+ RESTRICTED_PROFILE_A.partial = true;
+ // Remove the restricted user
+ vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
+ verify(mConnectivityManager).setRequireVpnForUids(false, restrictedRanges);
+
+ // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static.
+ RESTRICTED_PROFILE_A.partial = false;
+ }
+
+ @Test
public void testPrepare_throwSecurityExceptionWhenGivenPackageDoesNotBelongToTheCaller()
throws Exception {
mTestDeps.mIgnoreCallingUidChecks = false;
@@ -1002,12 +1140,12 @@
// List in keystore is not changed, but UID for the removed packages is no longer exempted.
assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+ assertEquals(makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids),
vpn.mNetworkCapabilities.getUids());
ArgumentCaptor<NetworkCapabilities> ncCaptor =
ArgumentCaptor.forClass(NetworkCapabilities.class);
verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
- assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+ assertEquals(makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids),
ncCaptor.getValue().getUids());
reset(mMockNetworkAgent);
@@ -1019,26 +1157,28 @@
// List in keystore is not changed and the uid list should be updated in the net cap.
assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+ assertEquals(makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids),
vpn.mNetworkCapabilities.getUids());
verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
- assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
+ assertEquals(makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids),
ncCaptor.getValue().getUids());
}
- private Set<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedList) {
+ private List<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedAppIdList) {
final SortedSet<Integer> list = new TreeSet<>();
final int userBase = userId * UserHandle.PER_USER_RANGE;
- for (int uid : excludedList) {
- final int applicationUid = UserHandle.getUid(userId, uid);
- list.add(applicationUid);
- list.add(Process.toSdkSandboxUid(applicationUid)); // Add Sdk Sandbox UID
+ for (int appId : excludedAppIdList) {
+ final int uid = UserHandle.getUid(userId, appId);
+ list.add(uid);
+ if (Process.isApplicationUid(uid)) {
+ list.add(Process.toSdkSandboxUid(uid)); // Add Sdk Sandbox UID
+ }
}
final int minUid = userBase;
final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
- final Set<Range<Integer>> ranges = new ArraySet<>();
+ final List<Range<Integer>> ranges = new ArrayList<>();
// Iterate the list to create the ranges between each uid.
int start = minUid;
@@ -1059,6 +1199,10 @@
return ranges;
}
+ private Set<Range<Integer>> makeVpnUidRangeSet(int userId, List<Integer> excludedAppIdList) {
+ return new ArraySet<>(makeVpnUidRange(userId, excludedAppIdList));
+ }
+
@Test
public void testSetAndGetAppExclusionListRestrictedUser() throws Exception {
final Vpn vpn = prepareVpnForVerifyAppExclusionList();
@@ -1839,6 +1983,22 @@
// a subsequent CL.
}
+ @Test
+ public void testStartLegacyVpnIpv6() throws Exception {
+ setMockedUsers(PRIMARY_USER);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(EGRESS_IFACE);
+ lp.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+ final RouteInfo defaultRoute = new RouteInfo(
+ new IpPrefix(Inet6Address.ANY, 0), null, EGRESS_IFACE);
+ lp.addRoute(defaultRoute);
+
+ // IllegalStateException thrown since legacy VPN only supports IPv4.
+ assertThrows(IllegalStateException.class,
+ () -> vpn.startLegacyVpn(mVpnProfile, EGRESS_NETWORK, lp));
+ }
+
private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
setMockedUsers(PRIMARY_USER);
@@ -2988,8 +3148,15 @@
profile.mppe = useMppe;
doReturn(new Network[] { new Network(101) }).when(mConnectivityManager).getAllNetworks();
- doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(any(), any(),
- any(), any(), any(), any(), anyInt());
+ doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(
+ any(), // INetworkAgent
+ any(), // NetworkInfo
+ any(), // LinkProperties
+ any(), // NetworkCapabilities
+ any(), // LocalNetworkConfig
+ any(), // NetworkScore
+ any(), // NetworkAgentConfig
+ anyInt()); // provider ID
final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
final TestDeps deps = (TestDeps) vpn.mDeps;
@@ -3011,8 +3178,15 @@
assertEquals("nomppe", mtpdArgs[argsPrefix.length]);
}
- verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
- any(), any(), any(), any(), anyInt());
+ verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(
+ any(), // INetworkAgent
+ any(), // NetworkInfo
+ any(), // LinkProperties
+ any(), // NetworkCapabilities
+ any(), // LocalNetworkConfig
+ any(), // NetworkScore
+ any(), // NetworkAgentConfig
+ anyInt()); // provider ID
}, () -> { // Cleanup
vpn.mVpnRunner.exitVpnRunner();
deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
@@ -3037,7 +3211,7 @@
.thenReturn(new Network[] { new Network(101) });
when(mConnectivityManager.registerNetworkAgent(any(), any(), any(), any(),
- any(), any(), anyInt())).thenAnswer(invocation -> {
+ any(), any(), any(), anyInt())).thenAnswer(invocation -> {
// The runner has registered an agent and is now ready.
legacyRunnerReady.open();
return new Network(102);
@@ -3063,7 +3237,7 @@
ArgumentCaptor<NetworkCapabilities> ncCaptor =
ArgumentCaptor.forClass(NetworkCapabilities.class);
verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
- lpCaptor.capture(), ncCaptor.capture(), any(), any(), anyInt());
+ lpCaptor.capture(), ncCaptor.capture(), any(), any(), any(), anyInt());
// In this test the expected address is always v4 so /32.
// Note that the interface needs to be specified because RouteInfo objects stored in
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index 8eace1c..a86f923 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -153,10 +153,10 @@
thread.start()
doReturn(TEST_HOSTNAME).`when`(mockDeps).generateHostname()
doReturn(mockInterfaceAdvertiser1).`when`(mockDeps).makeAdvertiser(eq(mockSocket1),
- any(), any(), any(), any(), eq(TEST_HOSTNAME), any()
+ any(), any(), any(), any(), eq(TEST_HOSTNAME), any(), any()
)
doReturn(mockInterfaceAdvertiser2).`when`(mockDeps).makeAdvertiser(eq(mockSocket2),
- any(), any(), any(), any(), eq(TEST_HOSTNAME), any()
+ any(), any(), any(), any(), eq(TEST_HOSTNAME), any(), any()
)
doReturn(true).`when`(mockInterfaceAdvertiser1).isProbing(anyInt())
doReturn(true).`when`(mockInterfaceAdvertiser2).isProbing(anyInt())
@@ -202,6 +202,7 @@
any(),
intAdvCbCaptor.capture(),
eq(TEST_HOSTNAME),
+ any(),
any()
)
@@ -259,10 +260,10 @@
val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
val intAdvCbCaptor2 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any()
+ eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockDeps).makeAdvertiser(eq(mockSocket2), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any()
+ eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(
anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
@@ -367,7 +368,7 @@
val intAdvCbCaptor = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any()
+ eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
argThat { it.matches(SERVICE_1) }, eq(null))
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
index c39ee1e..2797462 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
@@ -82,7 +82,8 @@
@Test
fun testAnnounce() {
- val replySender = MdnsReplySender( thread.looper, socket, buffer, sharedLog)
+ val replySender = MdnsReplySender(
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
@Suppress("UNCHECKED_CAST")
val cb = mock(MdnsPacketRepeater.PacketRepeaterCallback::class.java)
as MdnsPacketRepeater.PacketRepeaterCallback<BaseAnnouncementInfo>
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index e869b91..331a5b6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -106,7 +106,7 @@
doReturn(thread.getLooper()).when(socketClient).getLooper();
doReturn(true).when(socketClient).supportsRequestingSpecificNetworks();
discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient,
- sharedLog) {
+ sharedLog, MdnsFeatureFlags.newBuilder().build()) {
@Override
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
@NonNull SocketKey socketKey) {
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index c19747e..db41a6a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -77,6 +77,7 @@
private val announcer = mock(MdnsAnnouncer::class.java)
private val prober = mock(MdnsProber::class.java)
private val sharedlog = SharedLog("MdnsInterfaceAdvertiserTest")
+ private val flags = MdnsFeatureFlags.newBuilder().build()
@Suppress("UNCHECKED_CAST")
private val probeCbCaptor = ArgumentCaptor.forClass(PacketRepeaterCallback::class.java)
as ArgumentCaptor<PacketRepeaterCallback<ProbingInfo>>
@@ -99,15 +100,14 @@
cb,
deps,
TEST_HOSTNAME,
- sharedlog
+ sharedlog,
+ flags
)
}
@Before
fun setUp() {
- doReturn(repository).`when`(deps).makeRecordRepository(any(),
- eq(TEST_HOSTNAME)
- )
+ doReturn(repository).`when`(deps).makeRecordRepository(any(), eq(TEST_HOSTNAME), any())
doReturn(replySender).`when`(deps).makeReplySender(anyString(), any(), any(), any(), any())
doReturn(announcer).`when`(deps).makeMdnsAnnouncer(anyString(), any(), any(), any(), any())
doReturn(prober).`when`(deps).makeMdnsProber(anyString(), any(), any(), any(), any())
@@ -190,8 +190,8 @@
fun testReplyToQuery() {
addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- val mockReply = mock(MdnsRecordRepository.ReplyInfo::class.java)
- doReturn(mockReply).`when`(repository).getReply(any(), any())
+ val testReply = MdnsReplyInfo(emptyList(), emptyList(), 0, InetSocketAddress(0))
+ doReturn(testReply).`when`(repository).getReply(any(), any())
// Query obtained with:
// scapy.raw(scapy.DNS(
@@ -216,7 +216,7 @@
assertContentEquals(arrayOf("_testservice", "_tcp", "local"), it.questions[0].name)
}
- verify(replySender).queueReply(mockReply)
+ verify(replySender).queueReply(testReply)
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index 3701b0c..8917ed3 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -42,6 +42,7 @@
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -69,6 +70,7 @@
@Mock private SocketCreationCallback mSocketCreationCallback;
@Mock private SharedLog mSharedLog;
private MdnsMultinetworkSocketClient mSocketClient;
+ private HandlerThread mHandlerThread;
private Handler mHandler;
private SocketKey mSocketKey;
@@ -76,14 +78,23 @@
public void setUp() throws SocketException {
MockitoAnnotations.initMocks(this);
- final HandlerThread thread = new HandlerThread("MdnsMultinetworkSocketClientTest");
- thread.start();
- mHandler = new Handler(thread.getLooper());
+ mHandlerThread = new HandlerThread("MdnsMultinetworkSocketClientTest");
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
mSocketKey = new SocketKey(1000 /* interfaceIndex */);
- mSocketClient = new MdnsMultinetworkSocketClient(thread.getLooper(), mProvider, mSharedLog);
+ mSocketClient = new MdnsMultinetworkSocketClient(
+ mHandlerThread.getLooper(), mProvider, mSharedLog);
mHandler.post(() -> mSocketClient.setCallback(mCallback));
}
+ @After
+ public void tearDown() throws Exception {
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+ }
+ }
+
private SocketCallback expectSocketCallback() {
return expectSocketCallback(mListener, mNetwork);
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
index f284819..5b7c0ba 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
@@ -119,7 +119,8 @@
@Test
fun testProbe() {
- val replySender = MdnsReplySender(thread.looper, socket, buffer, sharedLog)
+ val replySender = MdnsReplySender(
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(
listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)))
@@ -143,7 +144,8 @@
@Test
fun testProbeMultipleRecords() {
- val replySender = MdnsReplySender(thread.looper, socket, buffer, sharedLog)
+ val replySender = MdnsReplySender(
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(listOf(
makeServiceRecord(TEST_SERVICE_NAME_1, 37890),
@@ -181,7 +183,8 @@
@Test
fun testStopProbing() {
- val replySender = MdnsReplySender(thread.looper, socket, buffer, sharedLog)
+ val replySender = MdnsReplySender(
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(
listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)),
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index c9b502e..f26f7e1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -78,6 +78,7 @@
override fun getInterfaceInetAddresses(iface: NetworkInterface) =
Collections.enumeration(TEST_ADDRESSES.map { it.address })
}
+ private val flags = MdnsFeatureFlags.newBuilder().build()
@Before
fun setUp() {
@@ -92,7 +93,7 @@
@Test
fun testAddServiceAndProbe() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
assertEquals(0, repository.servicesCount)
assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
null /* subtype */))
@@ -127,7 +128,7 @@
@Test
fun testAddAndConflicts() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
assertFailsWith(NameConflictException::class) {
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */)
@@ -139,7 +140,7 @@
@Test
fun testInvalidReuseOfServiceId() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
assertFailsWith(IllegalArgumentException::class) {
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* subtype */)
@@ -148,7 +149,7 @@
@Test
fun testHasActiveService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
@@ -165,7 +166,7 @@
@Test
fun testExitAnnouncements() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
@@ -195,7 +196,7 @@
@Test
fun testExitAnnouncements_WithSubtype() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
@@ -231,7 +232,7 @@
@Test
fun testExitingServiceReAdded() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
repository.exitService(TEST_SERVICE_ID_1)
@@ -246,7 +247,7 @@
@Test
fun testOnProbingSucceeded() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
TEST_SUBTYPE)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
@@ -371,7 +372,7 @@
@Test
fun testGetOffloadPacket() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
val serviceType = arrayOf("_testservice", "_tcp", "local")
@@ -433,7 +434,7 @@
@Test
fun testGetReplyCaseInsensitive() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
val questionsCaseInSensitive =
listOf(MdnsPointerRecord(arrayOf("_TESTSERVICE", "_TCP", "local"),
@@ -463,7 +464,7 @@
}
private fun doGetReplyTest(subtype: String?) {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, subtype)
val queriedName = if (subtype == null) arrayOf("_testservice", "_tcp", "local")
else arrayOf(subtype, "_sub", "_testservice", "_tcp", "local")
@@ -551,7 +552,7 @@
@Test
fun testGetConflictingServices() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
@@ -579,7 +580,7 @@
@Test
fun testGetConflictingServicesCaseInsensitive() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
@@ -607,7 +608,7 @@
@Test
fun testGetConflictingServices_IdenticalService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
@@ -636,7 +637,7 @@
@Test
fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
@@ -665,7 +666,7 @@
@Test
fun testGetServiceRepliedRequestsCount() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
// Verify that there is no packet replied.
assertEquals(MdnsConstants.NO_PACKET,
@@ -690,6 +691,68 @@
assertEquals(MdnsConstants.NO_PACKET,
repository.getServiceRepliedRequestsCount(TEST_SERVICE_ID_2))
}
+
+ @Test
+ fun testIncludeInetAddressRecordsInProbing() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+ MdnsFeatureFlags.newBuilder().setIncludeInetAddressRecordsInProbing(true).build())
+ repository.updateAddresses(TEST_ADDRESSES)
+ assertEquals(0, repository.servicesCount)
+ assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+ null /* subtype */))
+ assertEquals(1, repository.servicesCount)
+
+ val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
+ assertNotNull(probingInfo)
+ assertTrue(repository.isProbing(TEST_SERVICE_ID_1))
+
+ assertEquals(TEST_SERVICE_ID_1, probingInfo.serviceId)
+ val packet = probingInfo.getPacket(0)
+
+ assertEquals(MdnsConstants.FLAGS_QUERY, packet.flags)
+ assertEquals(0, packet.answers.size)
+ assertEquals(0, packet.additionalRecords.size)
+
+ assertEquals(2, packet.questions.size)
+ val expectedName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ assertContentEquals(listOf(
+ MdnsAnyRecord(expectedName, false /* unicast */),
+ MdnsAnyRecord(TEST_HOSTNAME, false /* unicast */),
+ ), packet.questions)
+
+ assertEquals(4, packet.authorityRecords.size)
+ assertContentEquals(listOf(
+ MdnsServiceRecord(
+ expectedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 120_000L /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 120_000L /* ttlMillis */,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 120_000L /* ttlMillis */,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 120_000L /* ttlMillis */,
+ TEST_ADDRESSES[2].address)
+ ), packet.authorityRecords)
+
+ assertContentEquals(intArrayOf(TEST_SERVICE_ID_1), repository.clearServices())
+ }
}
private fun MdnsRecordRepository.initWithService(
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index b43bcf7..2b3b834 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -19,6 +19,7 @@
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
+import com.android.server.connectivity.mdns.MdnsServiceCache.CacheKey
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import java.util.concurrent.CompletableFuture
@@ -43,13 +44,12 @@
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsServiceCacheTest {
private val socketKey = SocketKey(null /* network */, INTERFACE_INDEX)
+ private val cacheKey1 = CacheKey(SERVICE_TYPE_1, socketKey)
+ private val cacheKey2 = CacheKey(SERVICE_TYPE_2, socketKey)
private val thread = HandlerThread(MdnsServiceCacheTest::class.simpleName)
private val handler by lazy {
Handler(thread.looper)
}
- private val serviceCache by lazy {
- MdnsServiceCache(thread.looper)
- }
@Before
fun setUp() {
@@ -61,6 +61,11 @@
thread.quitSafely()
}
+ private fun makeFlags(isExpiredServicesRemovalEnabled: Boolean = false) =
+ MdnsFeatureFlags.Builder()
+ .setIsExpiredServicesRemovalEnabled(isExpiredServicesRemovalEnabled)
+ .build()
+
private fun <T> runningOnHandlerAndReturn(functor: (() -> T)): T {
val future = CompletableFuture<T>()
handler.post {
@@ -70,46 +75,50 @@
}
private fun addOrUpdateService(
- serviceType: String,
- socketKey: SocketKey,
+ serviceCache: MdnsServiceCache,
+ cacheKey: CacheKey,
service: MdnsResponse
- ): Unit = runningOnHandlerAndReturn {
- serviceCache.addOrUpdateService(serviceType, socketKey, service)
+ ): Unit = runningOnHandlerAndReturn { serviceCache.addOrUpdateService(cacheKey, service) }
+
+ private fun removeService(
+ serviceCache: MdnsServiceCache,
+ serviceName: String,
+ cacheKey: CacheKey
+ ): Unit = runningOnHandlerAndReturn { serviceCache.removeService(serviceName, cacheKey) }
+
+ private fun getService(
+ serviceCache: MdnsServiceCache,
+ serviceName: String,
+ cacheKey: CacheKey,
+ ): MdnsResponse? = runningOnHandlerAndReturn {
+ serviceCache.getCachedService(serviceName, cacheKey)
}
- private fun removeService(serviceName: String, serviceType: String, socketKey: SocketKey):
- Unit = runningOnHandlerAndReturn {
- serviceCache.removeService(serviceName, serviceType, socketKey) }
-
- private fun getService(serviceName: String, serviceType: String, socketKey: SocketKey):
- MdnsResponse? = runningOnHandlerAndReturn {
- serviceCache.getCachedService(serviceName, serviceType, socketKey) }
-
- private fun getServices(serviceType: String, socketKey: SocketKey): List<MdnsResponse> =
- runningOnHandlerAndReturn { serviceCache.getCachedServices(serviceType, socketKey) }
+ private fun getServices(
+ serviceCache: MdnsServiceCache,
+ cacheKey: CacheKey,
+ ): List<MdnsResponse> = runningOnHandlerAndReturn { serviceCache.getCachedServices(cacheKey) }
@Test
fun testAddAndRemoveService() {
- addOrUpdateService(
- SERVICE_TYPE_1, socketKey, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
- var response = getService(SERVICE_NAME_1, SERVICE_TYPE_1, socketKey)
+ val serviceCache = MdnsServiceCache(thread.looper, makeFlags())
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
+ var response = getService(serviceCache, SERVICE_NAME_1, cacheKey1)
assertNotNull(response)
assertEquals(SERVICE_NAME_1, response.serviceInstanceName)
- removeService(SERVICE_NAME_1, SERVICE_TYPE_1, socketKey)
- response = getService(SERVICE_NAME_1, SERVICE_TYPE_1, socketKey)
+ removeService(serviceCache, SERVICE_NAME_1, cacheKey1)
+ response = getService(serviceCache, SERVICE_NAME_1, cacheKey1)
assertNull(response)
}
@Test
fun testGetCachedServices_multipleServiceTypes() {
- addOrUpdateService(
- SERVICE_TYPE_1, socketKey, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
- addOrUpdateService(
- SERVICE_TYPE_1, socketKey, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
- addOrUpdateService(
- SERVICE_TYPE_2, socketKey, createResponse(SERVICE_NAME_2, SERVICE_TYPE_2))
+ val serviceCache = MdnsServiceCache(thread.looper, makeFlags())
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
+ addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_2, SERVICE_TYPE_2))
- val responses1 = getServices(SERVICE_TYPE_1, socketKey)
+ val responses1 = getServices(serviceCache, cacheKey1)
assertEquals(2, responses1.size)
assertTrue(responses1.stream().anyMatch { response ->
response.serviceInstanceName == SERVICE_NAME_1
@@ -117,19 +126,19 @@
assertTrue(responses1.any { response ->
response.serviceInstanceName == SERVICE_NAME_2
})
- val responses2 = getServices(SERVICE_TYPE_2, socketKey)
+ val responses2 = getServices(serviceCache, cacheKey2)
assertEquals(1, responses2.size)
assertTrue(responses2.any { response ->
response.serviceInstanceName == SERVICE_NAME_2
})
- removeService(SERVICE_NAME_2, SERVICE_TYPE_1, socketKey)
- val responses3 = getServices(SERVICE_TYPE_1, socketKey)
+ removeService(serviceCache, SERVICE_NAME_2, cacheKey1)
+ val responses3 = getServices(serviceCache, cacheKey1)
assertEquals(1, responses3.size)
assertTrue(responses3.any { response ->
response.serviceInstanceName == SERVICE_NAME_1
})
- val responses4 = getServices(SERVICE_TYPE_2, socketKey)
+ val responses4 = getServices(serviceCache, cacheKey2)
assertEquals(1, responses4.size)
assertTrue(responses4.any { response ->
response.serviceInstanceName == SERVICE_NAME_2
@@ -137,6 +146,6 @@
}
private fun createResponse(serviceInstanceName: String, serviceType: String) = MdnsResponse(
- 0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
+ 0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
socketKey.interfaceIndex, socketKey.network)
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index fde5abd..ce154dd 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -193,7 +193,8 @@
thread = new HandlerThread("MdnsServiceTypeClientTests");
thread.start();
handler = new Handler(thread.getLooper());
- serviceCache = new MdnsServiceCache(thread.getLooper());
+ serviceCache = new MdnsServiceCache(
+ thread.getLooper(), MdnsFeatureFlags.newBuilder().build());
doAnswer(inv -> {
latestDelayMs = 0;
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
index 6f8ba6c..58f20a9 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
@file:Suppress("DEPRECATION") // This file tests a bunch of deprecated methods : don't warn about it
package com.android.server
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
new file mode 100644
index 0000000..6220e76
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.LocalNetworkConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_DOWNSTREAM_NETWORK
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class CSKeepConnectedTest : CSTest() {
+ @Test
+ fun testKeepConnectedLocalAgent() {
+ deps.setBuildSdk(VERSION_V)
+ val nc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build()
+ val keepConnectedAgent = Agent(nc = nc, score = FromS(NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_DOWNSTREAM_NETWORK)
+ .build()),
+ lnc = LocalNetworkConfig.Builder().build())
+ val dontKeepConnectedAgent = Agent(nc = nc, lnc = LocalNetworkConfig.Builder().build())
+ doTestKeepConnected(keepConnectedAgent, dontKeepConnectedAgent)
+ }
+
+ @Test
+ fun testKeepConnectedForTest() {
+ val keepAgent = Agent(score = FromS(NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+ .build()))
+ val dontKeepAgent = Agent()
+ doTestKeepConnected(keepAgent, dontKeepAgent)
+ }
+
+ fun doTestKeepConnected(keepAgent: CSAgentWrapper, dontKeepAgent: CSAgentWrapper) {
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+ keepAgent.connect()
+ dontKeepAgent.connect()
+
+ cb.expectAvailableCallbacks(keepAgent.network, validated = false)
+ cb.expectAvailableCallbacks(dontKeepAgent.network, validated = false)
+
+ // After the nascent timer, the agent without keep connected gets lost.
+ cb.expect<Lost>(dontKeepAgent.network)
+ cb.assertNoCallback()
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
new file mode 100644
index 0000000..cfc3a3d
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.content.pm.PackageManager.FEATURE_LEANBACK
+import android.net.INetd
+import android.net.LocalNetworkConfig
+import android.net.NativeNetworkConfig
+import android.net.NativeNetworkType
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.VpnManager
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import kotlin.test.assertFailsWith
+
+private const val TIMEOUT_MS = 2_000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
+
+private fun keepConnectedScore() =
+ FromS(NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build())
+
+private fun defaultLnc() = LocalNetworkConfig.Builder().build()
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSLocalAgentCreationTests(
+ private val sdkLevel: Int,
+ private val isTv: Boolean,
+ private val addLocalNetCapToRequest: Boolean
+) : CSTest() {
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters
+ fun arguments() = listOf(
+ arrayOf(VERSION_V, false /* isTv */, true /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_V, false /* isTv */, false /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_V, true /* isTv */, true /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_V, true /* isTv */, false /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_U, false /* isTv */, true /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_U, false /* isTv */, false /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_U, true /* isTv */, true /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_U, true /* isTv */, false /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_T, false /* isTv */, false /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_T, true /* isTv */, false /* addLocalNetCapToRequest */),
+ )
+ }
+
+ private fun makeNativeNetworkConfigLocal(netId: Int, permission: Int) =
+ NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL_LOCAL, permission,
+ false /* secure */, VpnManager.TYPE_VPN_NONE, false /* excludeLocalRoutes */)
+
+ @Test
+ fun testLocalAgents() {
+ val netdInOrder = inOrder(netd)
+ deps.setBuildSdk(sdkLevel)
+ doReturn(isTv).`when`(packageManager).hasSystemFeature(FEATURE_LEANBACK)
+ val allNetworksCb = TestableNetworkCallback()
+ val request = NetworkRequest.Builder()
+ if (addLocalNetCapToRequest) {
+ request.addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ }
+ cm.registerNetworkCallback(request.build(), allNetworksCb)
+ val ncTemplate = NetworkCapabilities.Builder().run {
+ addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ }.build()
+ val localAgent = if (sdkLevel >= VERSION_V || sdkLevel == VERSION_U && isTv) {
+ Agent(nc = ncTemplate, score = keepConnectedScore(), lnc = defaultLnc())
+ } else {
+ assertFailsWith<IllegalArgumentException> { Agent(nc = ncTemplate, lnc = defaultLnc()) }
+ netdInOrder.verify(netd, never()).networkCreate(any())
+ return
+ }
+ localAgent.connect()
+ netdInOrder.verify(netd).networkCreate(
+ makeNativeNetworkConfigLocal(localAgent.network.netId, INetd.PERMISSION_NONE))
+ if (addLocalNetCapToRequest) {
+ assertEquals(localAgent.network, allNetworksCb.expect<Available>().network)
+ } else {
+ allNetworksCb.assertNoCallback(NO_CALLBACK_TIMEOUT_MS)
+ }
+ cm.unregisterNetworkCallback(allNetworksCb)
+ localAgent.disconnect()
+ netdInOrder.verify(netd, timeout(TIMEOUT_MS)).networkDestroy(localAgent.network.netId)
+ }
+
+ @Test
+ fun testBadAgents() {
+ assertFailsWith<IllegalArgumentException> {
+ Agent(nc = NetworkCapabilities.Builder()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(),
+ lnc = null)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ Agent(nc = NetworkCapabilities.Builder().build(),
+ lnc = LocalNetworkConfig.Builder().build())
+ }
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
new file mode 100644
index 0000000..bd3efa9
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.LocalNetworkConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.RouteInfo
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertFailsWith
+
+private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
+ addTransportType(transport)
+ caps.forEach {
+ addCapability(it)
+ }
+ // Useful capabilities for everybody
+ addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+}.build()
+
+private fun lp(iface: String) = LinkProperties().apply {
+ interfaceName = iface
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class CSLocalAgentTests : CSTest() {
+ @Test
+ fun testBadAgents() {
+ assertFailsWith<IllegalArgumentException> {
+ Agent(nc = NetworkCapabilities.Builder()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(),
+ lnc = null)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ Agent(nc = NetworkCapabilities.Builder().build(),
+ lnc = LocalNetworkConfig.Builder().build())
+ }
+ }
+
+ @Test
+ fun testUpdateLocalAgentConfig() {
+ deps.setBuildSdk(VERSION_V)
+
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(),
+ cb)
+
+ // Set up a local agent that should forward its traffic to the best DUN upstream.
+ val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+ lp = lp("local0"),
+ lnc = LocalNetworkConfig.Builder().build(),
+ )
+ localAgent.connect()
+
+ cb.expect<Available>(localAgent.network)
+ cb.expect<CapabilitiesChanged>(localAgent.network)
+ cb.expect<LinkPropertiesChanged>(localAgent.network)
+ cb.expect<BlockedStatus>(localAgent.network)
+
+ val newLnc = LocalNetworkConfig.Builder()
+ .setUpstreamSelector(NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build())
+ .build()
+ localAgent.sendLocalNetworkConfig(newLnc)
+
+ localAgent.disconnect()
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index a5a1aeb..094ded3 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.server
import android.content.Context
@@ -5,6 +21,7 @@
import android.net.INetworkMonitor
import android.net.INetworkMonitorCallbacks
import android.net.LinkProperties
+import android.net.LocalNetworkConfig
import android.net.Network
import android.net.NetworkAgent
import android.net.NetworkAgentConfig
@@ -19,6 +36,7 @@
import android.os.HandlerThread
import com.android.modules.utils.build.SdkLevel
import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.TestableNetworkCallback
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
@@ -31,6 +49,8 @@
import kotlin.test.assertEquals
import kotlin.test.fail
+const val SHORT_TIMEOUT_MS = 200L
+
private inline fun <reified T> ArgumentCaptor() = ArgumentCaptor.forClass(T::class.java)
private val agentCounter = AtomicInteger(1)
@@ -44,11 +64,13 @@
*/
class CSAgentWrapper(
val context: Context,
+ val deps: ConnectivityService.Dependencies,
csHandlerThread: HandlerThread,
networkStack: NetworkStackClientBase,
nac: NetworkAgentConfig,
val nc: NetworkCapabilities,
val lp: LinkProperties,
+ val lnc: LocalNetworkConfig?,
val score: FromS<NetworkScore>,
val provider: NetworkProvider?
) : TestableNetworkCallback.HasNetwork {
@@ -78,9 +100,9 @@
nmCbCaptor.capture())
// Create the actual agent. NetworkAgent is abstract, so make an anonymous subclass.
- if (SdkLevel.isAtLeastS()) {
+ if (deps.isAtLeastS()) {
agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
- nc, lp, score.value, nac, provider) {}
+ nc, lp, lnc, score.value, nac, provider) {}
} else {
agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
nc, lp, 50 /* score */, nac, provider) {}
@@ -92,7 +114,7 @@
}
private fun onValidationRequested() {
- if (SdkLevel.isAtLeastT()) {
+ if (deps.isAtLeastT()) {
verify(networkMonitor).notifyNetworkConnectedParcel(any())
} else {
verify(networkMonitor).notifyNetworkConnected(any(), any())
@@ -109,9 +131,10 @@
fun connect() {
val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
- val request = NetworkRequest.Builder().clearCapabilities()
- .addTransportType(nc.transportTypes[0])
- .build()
+ val request = NetworkRequest.Builder().apply {
+ clearCapabilities()
+ if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+ }.build()
val cb = TestableNetworkCallback()
mgr.registerNetworkCallback(request, cb)
agent.markConnected()
@@ -133,6 +156,17 @@
}
fun disconnect() {
+ val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val request = NetworkRequest.Builder().apply {
+ clearCapabilities()
+ if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+ }.build()
+ val cb = TestableNetworkCallback(timeoutMs = SHORT_TIMEOUT_MS)
+ mgr.registerNetworkCallback(request, cb)
+ cb.eventuallyExpect<Available> { it.network == agent.network }
agent.unregister()
+ cb.eventuallyExpect<Lost> { it.network == agent.network }
}
+
+ fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index b11878d..0ccbfc3 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.server
import android.content.BroadcastReceiver
@@ -10,19 +26,21 @@
import android.net.ConnectivityManager
import android.net.INetd
import android.net.InetAddresses
-import android.net.IpPrefix
-import android.net.LinkAddress
import android.net.LinkProperties
+import android.net.LocalNetworkConfig
import android.net.NetworkAgentConfig
import android.net.NetworkCapabilities
-import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
-import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
+import android.net.NetworkCapabilities.TRANSPORT_TEST
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkPolicyManager
import android.net.NetworkProvider
import android.net.NetworkScore
import android.net.PacProxyManager
-import android.net.RouteInfo
import android.net.networkstack.NetworkStackClientBase
import android.os.BatteryStatsManager
import android.os.Handler
@@ -60,6 +78,25 @@
open class FromS<Type>(val value: Type)
+internal const val VERSION_UNMOCKED = -1
+internal const val VERSION_R = 1
+internal const val VERSION_S = 2
+internal const val VERSION_T = 3
+internal const val VERSION_U = 4
+internal const val VERSION_V = 5
+internal const val VERSION_MAX = VERSION_V
+
+private fun NetworkCapabilities.getLegacyType() =
+ when (transportTypes.getOrElse(0) { TRANSPORT_WIFI }) {
+ TRANSPORT_BLUETOOTH -> ConnectivityManager.TYPE_BLUETOOTH
+ TRANSPORT_CELLULAR -> ConnectivityManager.TYPE_MOBILE
+ TRANSPORT_ETHERNET -> ConnectivityManager.TYPE_ETHERNET
+ TRANSPORT_TEST -> ConnectivityManager.TYPE_TEST
+ TRANSPORT_VPN -> ConnectivityManager.TYPE_VPN
+ TRANSPORT_WIFI -> ConnectivityManager.TYPE_WIFI
+ else -> ConnectivityManager.TYPE_NONE
+ }
+
/**
* Base class for tests testing ConnectivityService and its satellites.
*
@@ -108,9 +145,10 @@
val networkStack = mock<NetworkStackClientBase>()
val csHandlerThread = HandlerThread("CSTestHandler")
val sysResources = mock<Resources>().also { initMockedResources(it) }
- val packageManager = makeMockPackageManager()
+ val packageManager = makeMockPackageManager(instrumentationContext)
val connResources = makeMockConnResources(sysResources, packageManager)
+ val netd = mock<INetd>()
val bpfNetMaps = mock<BpfNetMaps>()
val clatCoordinator = mock<ClatCoordinator>()
val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
@@ -122,7 +160,7 @@
}
val deps = CSDeps()
- val service = makeConnectivityService(context, deps).also { it.systemReadyInternal() }
+ val service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
val cm = ConnectivityManager(context, service)
val csHandler = Handler(csHandlerThread.looper)
@@ -176,6 +214,24 @@
changeId in enabledChangeIds
override fun isChangeEnabled(changeId: Long, uid: Int) =
changeId in enabledChangeIds
+
+ // In AOSP, build version codes can't always distinguish between some versions (e.g. at the
+ // time of this writing U == V). Define custom ones.
+ private var sdkLevel = VERSION_UNMOCKED
+ private val isSdkUnmocked get() = sdkLevel == VERSION_UNMOCKED
+
+ fun setBuildSdk(sdkLevel: Int) {
+ require(sdkLevel <= VERSION_MAX) {
+ "setBuildSdk must not be called with Build.VERSION constants but " +
+ "CsTest.VERSION_* constants"
+ }
+ visibleOnHandlerThread(csHandler) { this.sdkLevel = sdkLevel }
+ }
+
+ override fun isAtLeastS() = if (isSdkUnmocked) super.isAtLeastS() else sdkLevel >= VERSION_S
+ override fun isAtLeastT() = if (isSdkUnmocked) super.isAtLeastT() else sdkLevel >= VERSION_T
+ override fun isAtLeastU() = if (isSdkUnmocked) super.isAtLeastU() else sdkLevel >= VERSION_U
+ override fun isAtLeastV() = if (isSdkUnmocked) super.isAtLeastV() else sdkLevel >= VERSION_V
}
inner class CSContext(base: Context) : BroadcastInterceptingContext(base) {
@@ -231,29 +287,17 @@
// Utility methods for subclasses to use
fun waitForIdle() = csHandlerThread.waitForIdle(HANDLER_TIMEOUT_MS)
- private fun emptyAgentConfig() = NetworkAgentConfig.Builder().build()
- private fun defaultNc() = NetworkCapabilities.Builder()
- // Add sensible defaults for agents that don't want to care
- .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
- .addCapability(NET_CAPABILITY_NOT_ROAMING)
- .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
- .build()
- private fun defaultScore() = FromS<NetworkScore>(NetworkScore.Builder().build())
- private fun defaultLp() = LinkProperties().apply {
- addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
- addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
- }
-
// Network agents. See CSAgentWrapper. This class contains utility methods to simplify
// creation.
fun Agent(
- nac: NetworkAgentConfig = emptyAgentConfig(),
nc: NetworkCapabilities = defaultNc(),
+ nac: NetworkAgentConfig = emptyAgentConfig(nc.getLegacyType()),
lp: LinkProperties = defaultLp(),
+ lnc: LocalNetworkConfig? = null,
score: FromS<NetworkScore> = defaultScore(),
provider: NetworkProvider? = null
- ) = CSAgentWrapper(context, csHandlerThread, networkStack, nac, nc, lp, score, provider)
-
+ ) = CSAgentWrapper(context, deps, csHandlerThread, networkStack,
+ nac, nc, lp, lnc, score, provider)
fun Agent(vararg transports: Int, lp: LinkProperties = defaultLp()): CSAgentWrapper {
val nc = NetworkCapabilities.Builder().apply {
transports.forEach {
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
index b8f2151..c1828b2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
@file:JvmName("CsTestHelpers")
package com.android.server
@@ -14,7 +30,15 @@
import android.content.res.Resources
import android.net.IDnsResolver
import android.net.INetd
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkScore
+import android.net.RouteInfo
import android.net.metrics.IpConnectivityLog
+import android.os.Binder
import android.os.Handler
import android.os.HandlerThread
import android.os.SystemClock
@@ -31,19 +55,38 @@
import com.android.server.connectivity.ConnectivityResources
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.argThat
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito
import org.mockito.Mockito.doAnswer
-import org.mockito.Mockito.doReturn
import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
import kotlin.test.fail
internal inline fun <reified T> mock() = Mockito.mock(T::class.java)
internal inline fun <reified T> any() = any(T::class.java)
+internal fun emptyAgentConfig(legacyType: Int) = NetworkAgentConfig.Builder()
+ .setLegacyType(legacyType)
+ .build()
+
+internal fun defaultNc() = NetworkCapabilities.Builder()
+ // Add sensible defaults for agents that don't want to care
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+
+internal fun defaultScore() = FromS(NetworkScore.Builder().build())
+
+internal fun defaultLp() = LinkProperties().apply {
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
internal fun makeMockContentResolver(context: Context) = MockContentResolver(context).apply {
addProvider(Settings.AUTHORITY, FakeSettingsProvider())
}
@@ -59,9 +102,22 @@
}
}
-internal fun makeMockPackageManager() = mock<PackageManager>().also { pm ->
+internal fun makeMockPackageManager(realContext: Context) = mock<PackageManager>().also { pm ->
val supported = listOf(FEATURE_WIFI, FEATURE_WIFI_DIRECT, FEATURE_BLUETOOTH, FEATURE_ETHERNET)
doReturn(true).`when`(pm).hasSystemFeature(argThat { supported.contains(it) })
+ val myPackageName = realContext.packageName
+ val myPackageInfo = realContext.packageManager.getPackageInfo(myPackageName,
+ PackageManager.GET_PERMISSIONS)
+ // Very high version code so that the checks for the module version will always
+ // say that it is recent enough. This is the most sensible default, but if some
+ // test needs to test with different version codes they can re-mock this with a
+ // different value.
+ myPackageInfo.longVersionCode = 9999999L
+ doReturn(arrayOf(myPackageName)).`when`(pm).getPackagesForUid(Binder.getCallingUid())
+ doReturn(myPackageInfo).`when`(pm).getPackageInfoAsUser(
+ eq(myPackageName), anyInt(), eq(UserHandle.getCallingUserId()))
+ doReturn(listOf(myPackageInfo)).`when`(pm)
+ .getInstalledPackagesAsUser(eq(PackageManager.GET_PERMISSIONS), anyInt())
}
internal fun makeMockConnResources(resources: Resources, pm: PackageManager) = mock<Context>().let {
@@ -129,12 +185,13 @@
private val TEST_LINGER_DELAY_MS = 400
private val TEST_NASCENT_DELAY_MS = 300
-internal fun makeConnectivityService(context: Context, deps: Dependencies) = ConnectivityService(
- context,
- mock<IDnsResolver>(),
- mock<IpConnectivityLog>(),
- mock<INetd>(),
- deps).also {
- it.mLingerDelayMs = TEST_LINGER_DELAY_MS
- it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
-}
+internal fun makeConnectivityService(context: Context, netd: INetd, deps: Dependencies) =
+ ConnectivityService(
+ context,
+ mock<IDnsResolver>(),
+ mock<IpConnectivityLog>(),
+ netd,
+ deps).also {
+ it.mLingerDelayMs = TEST_LINGER_DELAY_MS
+ it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
+ }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsEventLoggerTest.kt b/tests/unit/java/com/android/server/net/NetworkStatsEventLoggerTest.kt
new file mode 100644
index 0000000..9f2d4d3
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/NetworkStatsEventLoggerTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.util.IndentingPrintWriter
+import com.android.server.net.NetworkStatsEventLogger.MAX_POLL_REASON
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_DUMPSYS
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_FORCE_UPDATE
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_GLOBAL_ALERT
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_NETWORK_STATUS_CHANGED
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_OPEN_SESSION
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK
+import com.android.server.net.NetworkStatsEventLogger.PollEvent
+import com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf
+import com.android.testutils.DevSdkIgnoreRunner
+import java.io.StringWriter
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+const val TEST_PERSIST_FLAG = 0x101
+
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkStatsEventLoggerTest {
+ val logger = NetworkStatsEventLogger()
+ val stringWriter = TestStringWriter()
+ val pw = IndentingPrintWriter(stringWriter)
+
+ @Test
+ fun testDump_invalid() {
+ // Verify it won't crash.
+ logger.dump(pw)
+ // Clear output buffer.
+ stringWriter.getOutputAndClear()
+
+ // Verify log invalid event throws. And nothing output in the dump.
+ val invalidReasons = listOf(-1, MAX_POLL_REASON + 1)
+ invalidReasons.forEach {
+ assertFailsWith<IllegalArgumentException> {
+ logger.logPollEvent(TEST_PERSIST_FLAG, PollEvent(it))
+ }
+ logger.dumpRecentPollEvents(pw)
+ val output = stringWriter.getOutputAndClear()
+ assertStringNotContains(output, pollReasonNameOf(it))
+ }
+ }
+
+ @Test
+ fun testDump_valid() {
+ // Choose arbitrary set of reasons for testing.
+ val loggedReasons = listOf(
+ POLL_REASON_GLOBAL_ALERT,
+ POLL_REASON_FORCE_UPDATE,
+ POLL_REASON_DUMPSYS,
+ POLL_REASON_PERIODIC,
+ POLL_REASON_RAT_CHANGED
+ )
+ val nonLoggedReasons = listOf(
+ POLL_REASON_NETWORK_STATUS_CHANGED,
+ POLL_REASON_OPEN_SESSION,
+ POLL_REASON_REG_CALLBACK)
+
+ // Add some valid records.
+ loggedReasons.forEach {
+ logger.logPollEvent(TEST_PERSIST_FLAG, PollEvent(it))
+ }
+
+ // Collect dumps.
+ logger.dumpRecentPollEvents(pw)
+ val outputRecentEvents = stringWriter.getOutputAndClear()
+ logger.dumpPollCountsPerReason(pw)
+ val outputCountsPerReason = stringWriter.getOutputAndClear()
+
+ // Verify the output contains at least necessary information.
+ loggedReasons.forEach {
+ // Verify all events are shown in the recent event dump.
+ val eventString = PollEvent(it).toString()
+ assertStringContains(outputRecentEvents, TEST_PERSIST_FLAG.toString())
+ assertStringContains(eventString, pollReasonNameOf(it))
+ assertStringContains(outputRecentEvents, eventString)
+ // Verify counts are 1 for each reason.
+ assertCountForReason(outputCountsPerReason, it, 1)
+ }
+
+ // Verify the output remains untouched for other reasons.
+ nonLoggedReasons.forEach {
+ assertStringNotContains(outputRecentEvents, PollEvent(it).toString())
+ assertCountForReason(outputCountsPerReason, it, 0)
+ }
+ }
+
+ @Test
+ fun testDump_maxEventLogs() {
+ // Choose arbitrary reason.
+ val reasonToBeTested = POLL_REASON_PERIODIC
+ val repeatCount = NetworkStatsEventLogger.MAX_EVENTS_LOGS * 2
+
+ // Collect baseline.
+ logger.dumpRecentPollEvents(pw)
+ val lineCountBaseLine = getLineCount(stringWriter.getOutputAndClear())
+
+ repeat(repeatCount) {
+ logger.logPollEvent(TEST_PERSIST_FLAG, PollEvent(reasonToBeTested))
+ }
+
+ // Collect dump.
+ logger.dumpRecentPollEvents(pw)
+ val lineCountAfterTest = getLineCount(stringWriter.getOutputAndClear())
+
+ // Verify line count increment is limited.
+ assertEquals(
+ NetworkStatsEventLogger.MAX_EVENTS_LOGS,
+ lineCountAfterTest - lineCountBaseLine
+ )
+
+ // Verify count per reason increased for the testing reason.
+ logger.dumpPollCountsPerReason(pw)
+ val outputCountsPerReason = stringWriter.getOutputAndClear()
+ for (reason in 0..MAX_POLL_REASON) {
+ assertCountForReason(
+ outputCountsPerReason,
+ reason,
+ if (reason == reasonToBeTested) repeatCount else 0
+ )
+ }
+ }
+
+ private fun getLineCount(multilineString: String) = multilineString.lines().size
+
+ private fun assertStringContains(got: String, want: String) {
+ assertTrue(got.contains(want), "Wanted: $want, but got: $got")
+ }
+
+ private fun assertStringNotContains(got: String, unwant: String) {
+ assertFalse(got.contains(unwant), "Unwanted: $unwant, but got: $got")
+ }
+
+ /**
+ * Assert the reason and the expected count are at the same line.
+ */
+ private fun assertCountForReason(dump: String, reason: Int, expectedCount: Int) {
+ // Matches strings like "GLOBAL_ALERT: 50" but not too strict since the format might change.
+ val regex = Regex(pollReasonNameOf(reason) + "[^0-9]+" + expectedCount)
+ assertEquals(
+ 1,
+ regex.findAll(dump).count(),
+ "Unexpected output: $dump " + " for reason: " + pollReasonNameOf(reason)
+ )
+ }
+
+ class TestStringWriter : StringWriter() {
+ fun getOutputAndClear() = toString().also { buffer.setLength(0) }
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 292f77e..c477b2c 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -59,6 +59,7 @@
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -139,6 +140,14 @@
mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
}
+ @After
+ public void tearDown() throws Exception {
+ if (mObserverHandlerThread != null) {
+ mObserverHandlerThread.quitSafely();
+ mObserverHandlerThread.join();
+ }
+ }
+
@Test
public void testRegister_thresholdTooLow_setsDefaultThreshold() throws Exception {
final long thresholdTooLowBytes = 1L;
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 9453617..e8d5c66 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -64,6 +64,8 @@
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME;
@@ -525,6 +527,11 @@
IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
return mSkDestroyListener;
}
+
+ @Override
+ public boolean supportEventLogger(@NonNull Context cts) {
+ return true;
+ }
};
}
@@ -2674,4 +2681,14 @@
doReturn(null).when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
doTestDumpIfaceStatsMap("unknown");
}
+
+ // Basic test to ensure event logger dump is called.
+ // Note that tests to ensure detailed correctness is done in the dedicated tests.
+ // See NetworkStatsEventLoggerTest.
+ @Test
+ public void testDumpEventLogger() {
+ setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS);
+ final String dump = getDump();
+ assertDumpContains(dump, pollReasonNameOf(POLL_REASON_RAT_CHANGED));
+ }
}
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index 17a74f6..3eaebfa 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -1,7 +1,5 @@
{
- // TODO (b/297729075): graduate this test to presubmit once it meets the SLO requirements.
- // See go/test-mapping-slo-guide
- "postsubmit": [
+ "presubmit": [
{
"name": "CtsThreadNetworkTestCases"
}
diff --git a/thread/flags/thread_base.aconfig b/thread/flags/thread_base.aconfig
new file mode 100644
index 0000000..bf1f288
--- /dev/null
+++ b/thread/flags/thread_base.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.net.thread.flags"
+
+flag {
+ name: "thread_enabled"
+ namespace: "thread_network"
+ description: "Controls whether the Android Thread feature is enabled"
+ bug: "301473012"
+}
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl b/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl
new file mode 100644
index 0000000..8bf12a4
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 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.thread;
+
+parcelable ActiveOperationalDataset;
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
new file mode 100644
index 0000000..c9b047a
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
@@ -0,0 +1,1165 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import static android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkState;
+import static com.android.net.module.util.HexDump.dumpHexString;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.annotation.SystemApi;
+import android.net.IpPrefix;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.ByteArrayOutputStream;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * Data interface for managing a Thread Active Operational Dataset.
+ *
+ * <p>An example usage of creating an Active Operational Dataset with random parameters:
+ *
+ * <pre>{@code
+ * ActiveOperationalDataset activeDataset = ActiveOperationalDataset.createRandomDataset();
+ * }</pre>
+ *
+ * <p>or random Dataset with customized Network Name:
+ *
+ * <pre>{@code
+ * ActiveOperationalDataset activeDataset =
+ * new ActiveOperationalDataset.Builder(ActiveOperationalDataset.createRandomDataset())
+ * .setNetworkName("MyThreadNet").build();
+ * }</pre>
+ *
+ * <p>If the Active Operational Dataset is already known as <a
+ * href="https://www.threadgroup.org">Thread TLVs</a>, you can simply use:
+ *
+ * <pre>{@code
+ * ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlvs);
+ * }</pre>
+ *
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public final class ActiveOperationalDataset implements Parcelable {
+ /** The maximum length of the Active Operational Dataset TLV array in bytes. */
+ public static final int LENGTH_MAX_DATASET_TLVS = 254;
+ /** The length of Extended PAN ID in bytes. */
+ public static final int LENGTH_EXTENDED_PAN_ID = 8;
+ /** The minimum length of Network Name as UTF-8 bytes. */
+ public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1;
+ /** The maximum length of Network Name as UTF-8 bytes. */
+ public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16;
+ /** The length of Network Key in bytes. */
+ public static final int LENGTH_NETWORK_KEY = 16;
+ /** The length of Mesh-Local Prefix in bits. */
+ public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64;
+ /** The length of PSKc in bytes. */
+ public static final int LENGTH_PSKC = 16;
+ /** The 2.4 GHz channel page. */
+ public static final int CHANNEL_PAGE_24_GHZ = 0;
+ /** The minimum 2.4GHz channel. */
+ public static final int CHANNEL_MIN_24_GHZ = 11;
+ /** The maximum 2.4GHz channel. */
+ public static final int CHANNEL_MAX_24_GHZ = 26;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_CHANNEL = 0;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_PAN_ID = 1;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_EXTENDED_PAN_ID = 2;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_NETWORK_NAME = 3;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_PSKC = 4;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_NETWORK_KEY = 5;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_MESH_LOCAL_PREFIX = 7;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_SECURITY_POLICY = 12;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_ACTIVE_TIMESTAMP = 14;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_CHANNEL_MASK = 53;
+
+ private static final byte MESH_LOCAL_PREFIX_FIRST_BYTE = (byte) 0xfd;
+ private static final int LENGTH_CHANNEL = 3;
+ private static final int LENGTH_PAN_ID = 2;
+
+ @NonNull
+ public static final Creator<ActiveOperationalDataset> CREATOR =
+ new Creator<>() {
+ @Override
+ public ActiveOperationalDataset createFromParcel(Parcel in) {
+ return ActiveOperationalDataset.fromThreadTlvs(in.createByteArray());
+ }
+
+ @Override
+ public ActiveOperationalDataset[] newArray(int size) {
+ return new ActiveOperationalDataset[size];
+ }
+ };
+
+ private final OperationalDatasetTimestamp mActiveTimestamp;
+ private final String mNetworkName;
+ private final byte[] mExtendedPanId;
+ private final int mPanId;
+ private final int mChannel;
+ private final int mChannelPage;
+ private final SparseArray<byte[]> mChannelMask;
+ private final byte[] mPskc;
+ private final byte[] mNetworkKey;
+ private final IpPrefix mMeshLocalPrefix;
+ private final SecurityPolicy mSecurityPolicy;
+ private final SparseArray<byte[]> mUnknownTlvs;
+
+ private ActiveOperationalDataset(Builder builder) {
+ this(
+ requireNonNull(builder.mActiveTimestamp),
+ requireNonNull(builder.mNetworkName),
+ requireNonNull(builder.mExtendedPanId),
+ requireNonNull(builder.mPanId),
+ requireNonNull(builder.mChannelPage),
+ requireNonNull(builder.mChannel),
+ requireNonNull(builder.mChannelMask),
+ requireNonNull(builder.mPskc),
+ requireNonNull(builder.mNetworkKey),
+ requireNonNull(builder.mMeshLocalPrefix),
+ requireNonNull(builder.mSecurityPolicy),
+ requireNonNull(builder.mUnknownTlvs));
+ }
+
+ private ActiveOperationalDataset(
+ OperationalDatasetTimestamp activeTimestamp,
+ String networkName,
+ byte[] extendedPanId,
+ int panId,
+ int channelPage,
+ int channel,
+ SparseArray<byte[]> channelMask,
+ byte[] pskc,
+ byte[] networkKey,
+ IpPrefix meshLocalPrefix,
+ SecurityPolicy securityPolicy,
+ SparseArray<byte[]> unknownTlvs) {
+ this.mActiveTimestamp = activeTimestamp;
+ this.mNetworkName = networkName;
+ this.mExtendedPanId = extendedPanId.clone();
+ this.mPanId = panId;
+ this.mChannel = channel;
+ this.mChannelPage = channelPage;
+ this.mChannelMask = deepCloneSparseArray(channelMask);
+ this.mPskc = pskc.clone();
+ this.mNetworkKey = networkKey.clone();
+ this.mMeshLocalPrefix = meshLocalPrefix;
+ this.mSecurityPolicy = securityPolicy;
+ this.mUnknownTlvs = deepCloneSparseArray(unknownTlvs);
+ }
+
+ /**
+ * Creates a new {@link ActiveOperationalDataset} object from a series of Thread TLVs.
+ *
+ * <p>{@code tlvs} can be obtained from the value of a Thread Active Operational Dataset TLV
+ * (see the <a href="https://www.threadgroup.org/support#specifications">Thread
+ * specification</a> for the definition) or the return value of {@link #toThreadTlvs}.
+ *
+ * @param tlvs a series of Thread TLVs which contain the Active Operational Dataset
+ * @return the decoded Active Operational Dataset
+ * @throws IllegalArgumentException if {@code tlvs} is malformed or the length is larger than
+ * {@link LENGTH_MAX_DATASET_TLVS}
+ */
+ @NonNull
+ public static ActiveOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) {
+ requireNonNull(tlvs, "tlvs cannot be null");
+ if (tlvs.length > LENGTH_MAX_DATASET_TLVS) {
+ throw new IllegalArgumentException(
+ String.format(
+ "tlvs length exceeds max length %d (actual is %d)",
+ LENGTH_MAX_DATASET_TLVS, tlvs.length));
+ }
+
+ Builder builder = new Builder();
+ int i = 0;
+ while (i < tlvs.length) {
+ int type = tlvs[i++] & 0xff;
+ if (i >= tlvs.length) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Found TLV type %d at end of operational dataset with length %d",
+ type, tlvs.length));
+ }
+
+ int length = tlvs[i++] & 0xff;
+ if (i + length > tlvs.length) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Found TLV type %d with length %d which exceeds the remaining data"
+ + " in the operational dataset with length %d",
+ type, length, tlvs.length));
+ }
+
+ initWithTlv(builder, type, Arrays.copyOfRange(tlvs, i, i + length));
+ i += length;
+ }
+ try {
+ return builder.build();
+ } catch (IllegalStateException e) {
+ throw new IllegalArgumentException(
+ "Failed to build the ActiveOperationalDataset object", e);
+ }
+ }
+
+ private static void initWithTlv(Builder builder, int type, byte[] value) {
+ // The max length of the dataset is 254 bytes, so the max length of a single TLV value is
+ // 252 (254 - 1 - 1)
+ if (value.length > LENGTH_MAX_DATASET_TLVS - 2) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Length of TLV %d exceeds %d (actualLength = %d)",
+ (type & 0xff), LENGTH_MAX_DATASET_TLVS - 2, value.length));
+ }
+
+ switch (type) {
+ case TYPE_CHANNEL:
+ checkArgument(
+ value.length == LENGTH_CHANNEL,
+ "Invalid channel (length = %d, expectedLength = %d)",
+ value.length,
+ LENGTH_CHANNEL);
+ builder.setChannel((value[0] & 0xff), ((value[1] & 0xff) << 8) | (value[2] & 0xff));
+ break;
+ case TYPE_PAN_ID:
+ checkArgument(
+ value.length == LENGTH_PAN_ID,
+ "Invalid PAN ID (length = %d, expectedLength = %d)",
+ value.length,
+ LENGTH_PAN_ID);
+ builder.setPanId(((value[0] & 0xff) << 8) | (value[1] & 0xff));
+ break;
+ case TYPE_EXTENDED_PAN_ID:
+ builder.setExtendedPanId(value);
+ break;
+ case TYPE_NETWORK_NAME:
+ builder.setNetworkName(new String(value, UTF_8));
+ break;
+ case TYPE_PSKC:
+ builder.setPskc(value);
+ break;
+ case TYPE_NETWORK_KEY:
+ builder.setNetworkKey(value);
+ break;
+ case TYPE_MESH_LOCAL_PREFIX:
+ builder.setMeshLocalPrefix(value);
+ break;
+ case TYPE_SECURITY_POLICY:
+ builder.setSecurityPolicy(SecurityPolicy.fromTlvValue(value));
+ break;
+ case TYPE_ACTIVE_TIMESTAMP:
+ builder.setActiveTimestamp(OperationalDatasetTimestamp.fromTlvValue(value));
+ break;
+ case TYPE_CHANNEL_MASK:
+ builder.setChannelMask(decodeChannelMask(value));
+ break;
+ default:
+ builder.addUnknownTlv(type & 0xff, value);
+ break;
+ }
+ }
+
+ private static SparseArray<byte[]> decodeChannelMask(byte[] tlvValue) {
+ SparseArray<byte[]> channelMask = new SparseArray<>();
+ int i = 0;
+ while (i < tlvValue.length) {
+ int channelPage = tlvValue[i++] & 0xff;
+ if (i >= tlvValue.length) {
+ throw new IllegalArgumentException(
+ "Invalid channel mask - channel mask length is missing");
+ }
+
+ int maskLength = tlvValue[i++] & 0xff;
+ if (i + maskLength > tlvValue.length) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Invalid channel mask - channel mask is incomplete "
+ + "(offset = %d, length = %d, totalLength = %d)",
+ i, maskLength, tlvValue.length));
+ }
+
+ channelMask.put(channelPage, Arrays.copyOfRange(tlvValue, i, i + maskLength));
+ i += maskLength;
+ }
+ return channelMask;
+ }
+
+ private static void encodeChannelMask(
+ SparseArray<byte[]> channelMask, ByteArrayOutputStream outputStream) {
+ ByteArrayOutputStream entryStream = new ByteArrayOutputStream();
+
+ for (int i = 0; i < channelMask.size(); i++) {
+ int key = channelMask.keyAt(i);
+ byte[] value = channelMask.get(key);
+ entryStream.write(key);
+ entryStream.write(value.length);
+ entryStream.write(value, 0, value.length);
+ }
+
+ byte[] entries = entryStream.toByteArray();
+
+ outputStream.write(TYPE_CHANNEL_MASK);
+ outputStream.write(entries.length);
+ outputStream.write(entries, 0, entries.length);
+ }
+
+ /**
+ * Creates a new {@link ActiveOperationalDataset} object with randomized or default parameters.
+ *
+ * <p>The randomized (or default) value for each parameter:
+ *
+ * <ul>
+ * <li>{@code Active Timestamp} defaults to {@code new OperationalDatasetTimestamp(1, 0,
+ * false)}
+ * <li>{@code Network Name} defaults to "THREAD-PAN-<PAN ID decimal>", for example
+ * "THREAD-PAN-12345"
+ * <li>{@code Extended PAN ID} filled with randomly generated bytes
+ * <li>{@code PAN ID} randomly generated integer in range of [0, 0xfffe]
+ * <li>{@code Channel Page} defaults to {@link #CHANNEL_PAGE_24_GHZ}
+ * <li>{@code Channel} randomly selected channel in range of [{@link #CHANNEL_MIN_24_GHZ},
+ * {@link #CHANNEL_MAX_24_GHZ}]
+ * <li>{@code Channel Mask} all bits from {@link #CHANNEL_MIN_24_GHZ} to {@link
+ * #CHANNEL_MAX_24_GHZ} are set to {@code true}
+ * <li>{@code PSKc} filled with bytes generated by secure random generator
+ * <li>{@code Network Key} filled with bytes generated by secure random generator
+ * <li>{@code Mesh-local Prefix} filled with randomly generated bytes except that the first
+ * byte is always set to {@code 0xfd}
+ * <li>{@code Security Policy} defaults to {@code new SecurityPolicy(
+ * DEFAULT_ROTATION_TIME_HOURS, new byte[]{(byte)0xff, (byte)0xf8})}. This is the default
+ * values required by the Thread 1.2 specification
+ * </ul>
+ *
+ * <p>This method is the recommended way to create a randomized operational dataset for a new
+ * Thread network. It may be desired to change one or more of the generated value(s). For
+ * example, to use a more meaningful Network Name. To do that, create a new {@link Builder}
+ * object from this dataset with {@link Builder#Builder(ActiveOperationalDataset)} and override
+ * the value with the setters of {@link Builder}.
+ *
+ * <p>Note that it's highly discouraged to change the randomly generated Extended PAN ID,
+ * Network Key or PSKc, as it will compromise the security of a Thread network.
+ */
+ @NonNull
+ public static ActiveOperationalDataset createRandomDataset() {
+ return createRandomDataset(new Random(Instant.now().toEpochMilli()), new SecureRandom());
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public static ActiveOperationalDataset createRandomDataset(
+ Random random, SecureRandom secureRandom) {
+ int panId = random.nextInt(/* bound= */ 0xffff);
+ byte[] meshLocalPrefix = newRandomBytes(random, LENGTH_MESH_LOCAL_PREFIX_BITS / 8);
+ meshLocalPrefix[0] = MESH_LOCAL_PREFIX_FIRST_BYTE;
+
+ SparseArray<byte[]> channelMask = new SparseArray<>(1);
+ channelMask.put(CHANNEL_PAGE_24_GHZ, new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
+
+ return new Builder()
+ .setActiveTimestamp(
+ new OperationalDatasetTimestamp(
+ /* seconds= */ 1,
+ /* ticks= */ 0,
+ /* isAuthoritativeSource= */ false))
+ .setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
+ .setPanId(panId)
+ .setNetworkName("THREAD-PAN-" + panId)
+ .setChannel(
+ CHANNEL_PAGE_24_GHZ,
+ random.nextInt(CHANNEL_MAX_24_GHZ - CHANNEL_MIN_24_GHZ + 1)
+ + CHANNEL_MIN_24_GHZ)
+ .setChannelMask(channelMask)
+ .setPskc(newRandomBytes(secureRandom, LENGTH_PSKC))
+ .setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY))
+ .setMeshLocalPrefix(meshLocalPrefix)
+ .setSecurityPolicy(
+ new SecurityPolicy(
+ DEFAULT_ROTATION_TIME_HOURS, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .build();
+ }
+
+ private static byte[] newRandomBytes(Random random, int length) {
+ byte[] result = new byte[length];
+ random.nextBytes(result);
+ return result;
+ }
+
+ private static boolean areByteSparseArraysEqual(
+ @NonNull SparseArray<byte[]> first, @NonNull SparseArray<byte[]> second) {
+ if (first == second) {
+ return true;
+ } else if (first == null || second == null) {
+ return false;
+ } else if (first.size() != second.size()) {
+ return false;
+ } else {
+ for (int i = 0; i < first.size(); i++) {
+ int firstKey = first.keyAt(i);
+ int secondKey = second.keyAt(i);
+ if (firstKey != secondKey) {
+ return false;
+ }
+
+ byte[] firstValue = first.valueAt(i);
+ byte[] secondValue = second.valueAt(i);
+ if (!Arrays.equals(firstValue, secondValue)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ /** An easy-to-use wrapper of {@link Arrays#deepHashCode}. */
+ private static int deepHashCode(Object... values) {
+ return Arrays.deepHashCode(values);
+ }
+
+ /**
+ * Converts this {@link ActiveOperationalDataset} object to a series of Thread TLVs.
+ *
+ * <p>See the <a href="https://www.threadgroup.org/support#specifications">Thread
+ * specification</a> for the definition of the Thread TLV format.
+ *
+ * @return a series of Thread TLVs which contain this Active Operational Dataset
+ */
+ @NonNull
+ public byte[] toThreadTlvs() {
+ ByteArrayOutputStream dataset = new ByteArrayOutputStream();
+
+ dataset.write(TYPE_ACTIVE_TIMESTAMP);
+ byte[] activeTimestampBytes = mActiveTimestamp.toTlvValue();
+ dataset.write(activeTimestampBytes.length);
+ dataset.write(activeTimestampBytes, 0, activeTimestampBytes.length);
+
+ dataset.write(TYPE_NETWORK_NAME);
+ byte[] networkNameBytes = mNetworkName.getBytes(UTF_8);
+ dataset.write(networkNameBytes.length);
+ dataset.write(networkNameBytes, 0, networkNameBytes.length);
+
+ dataset.write(TYPE_EXTENDED_PAN_ID);
+ dataset.write(mExtendedPanId.length);
+ dataset.write(mExtendedPanId, 0, mExtendedPanId.length);
+
+ dataset.write(TYPE_PAN_ID);
+ dataset.write(LENGTH_PAN_ID);
+ dataset.write(mPanId >> 8);
+ dataset.write(mPanId);
+
+ dataset.write(TYPE_CHANNEL);
+ dataset.write(LENGTH_CHANNEL);
+ dataset.write(mChannelPage);
+ dataset.write(mChannel >> 8);
+ dataset.write(mChannel);
+
+ encodeChannelMask(mChannelMask, dataset);
+
+ dataset.write(TYPE_PSKC);
+ dataset.write(mPskc.length);
+ dataset.write(mPskc, 0, mPskc.length);
+
+ dataset.write(TYPE_NETWORK_KEY);
+ dataset.write(mNetworkKey.length);
+ dataset.write(mNetworkKey, 0, mNetworkKey.length);
+
+ dataset.write(TYPE_MESH_LOCAL_PREFIX);
+ dataset.write(mMeshLocalPrefix.getPrefixLength() / 8);
+ dataset.write(mMeshLocalPrefix.getRawAddress(), 0, mMeshLocalPrefix.getPrefixLength() / 8);
+
+ dataset.write(TYPE_SECURITY_POLICY);
+ byte[] securityPolicyBytes = mSecurityPolicy.toTlvValue();
+ dataset.write(securityPolicyBytes.length);
+ dataset.write(securityPolicyBytes, 0, securityPolicyBytes.length);
+
+ for (int i = 0; i < mUnknownTlvs.size(); i++) {
+ byte[] value = mUnknownTlvs.valueAt(i);
+ dataset.write(mUnknownTlvs.keyAt(i));
+ dataset.write(value.length);
+ dataset.write(value, 0, value.length);
+ }
+
+ return dataset.toByteArray();
+ }
+
+ /** Returns the Active Timestamp. */
+ @NonNull
+ public OperationalDatasetTimestamp getActiveTimestamp() {
+ return mActiveTimestamp;
+ }
+
+ /** Returns the Network Name. */
+ @NonNull
+ @Size(min = LENGTH_MIN_NETWORK_NAME_BYTES, max = LENGTH_MAX_NETWORK_NAME_BYTES)
+ public String getNetworkName() {
+ return mNetworkName;
+ }
+
+ /** Returns the Extended PAN ID. */
+ @NonNull
+ @Size(LENGTH_EXTENDED_PAN_ID)
+ public byte[] getExtendedPanId() {
+ return mExtendedPanId.clone();
+ }
+
+ /** Returns the PAN ID. */
+ @IntRange(from = 0, to = 0xfffe)
+ public int getPanId() {
+ return mPanId;
+ }
+
+ /** Returns the Channel. */
+ @IntRange(from = 0, to = 65535)
+ public int getChannel() {
+ return mChannel;
+ }
+
+ /** Returns the Channel Page. */
+ @IntRange(from = 0, to = 255)
+ public int getChannelPage() {
+ return mChannelPage;
+ }
+
+ /**
+ * Returns the Channel masks. For the returned {@link SparseArray}, the key is the Channel Page
+ * and the value is the Channel Mask.
+ */
+ @NonNull
+ @Size(min = 1)
+ public SparseArray<byte[]> getChannelMask() {
+ return deepCloneSparseArray(mChannelMask);
+ }
+
+ private static SparseArray<byte[]> deepCloneSparseArray(SparseArray<byte[]> src) {
+ SparseArray<byte[]> dst = new SparseArray<>(src.size());
+ for (int i = 0; i < src.size(); i++) {
+ dst.put(src.keyAt(i), src.valueAt(i).clone());
+ }
+ return dst;
+ }
+
+ /** Returns the PSKc. */
+ @NonNull
+ @Size(LENGTH_PSKC)
+ public byte[] getPskc() {
+ return mPskc.clone();
+ }
+
+ /** Returns the Network Key. */
+ @NonNull
+ @Size(LENGTH_NETWORK_KEY)
+ public byte[] getNetworkKey() {
+ return mNetworkKey.clone();
+ }
+
+ /**
+ * Returns the Mesh-local Prefix. The length of the returned prefix is always {@link
+ * #LENGTH_MESH_LOCAL_PREFIX_BITS}.
+ */
+ @NonNull
+ public IpPrefix getMeshLocalPrefix() {
+ return mMeshLocalPrefix;
+ }
+
+ /** Returns the Security Policy. */
+ @NonNull
+ public SecurityPolicy getSecurityPolicy() {
+ return mSecurityPolicy;
+ }
+
+ /**
+ * Returns Thread TLVs which are not recognized by this device. The returned {@link SparseArray}
+ * associates TLV values to their keys.
+ *
+ * @hide
+ */
+ @NonNull
+ public SparseArray<byte[]> getUnknownTlvs() {
+ return deepCloneSparseArray(mUnknownTlvs);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeByteArray(toThreadTlvs());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ } else if (!(other instanceof ActiveOperationalDataset)) {
+ return false;
+ } else {
+ ActiveOperationalDataset otherDataset = (ActiveOperationalDataset) other;
+ return mActiveTimestamp.equals(otherDataset.mActiveTimestamp)
+ && mNetworkName.equals(otherDataset.mNetworkName)
+ && Arrays.equals(mExtendedPanId, otherDataset.mExtendedPanId)
+ && mPanId == otherDataset.mPanId
+ && mChannelPage == otherDataset.mChannelPage
+ && mChannel == otherDataset.mChannel
+ && areByteSparseArraysEqual(mChannelMask, otherDataset.mChannelMask)
+ && Arrays.equals(mPskc, otherDataset.mPskc)
+ && Arrays.equals(mNetworkKey, otherDataset.mNetworkKey)
+ && mMeshLocalPrefix.equals(otherDataset.mMeshLocalPrefix)
+ && mSecurityPolicy.equals(otherDataset.mSecurityPolicy)
+ && areByteSparseArraysEqual(mUnknownTlvs, otherDataset.mUnknownTlvs);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return deepHashCode(
+ mActiveTimestamp,
+ mNetworkName,
+ mExtendedPanId,
+ mPanId,
+ mChannel,
+ mChannelPage,
+ mChannelMask,
+ mPskc,
+ mNetworkKey,
+ mMeshLocalPrefix,
+ mSecurityPolicy);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{networkName=")
+ .append(getNetworkName())
+ .append(", extendedPanId=")
+ .append(dumpHexString(getExtendedPanId()))
+ .append(", panId=")
+ .append(getPanId())
+ .append(", channel=")
+ .append(getChannel())
+ .append(", activeTimestamp=")
+ .append(getActiveTimestamp())
+ .append("}");
+ return sb.toString();
+ }
+
+ /** The builder for creating {@link ActiveOperationalDataset} objects. */
+ public static final class Builder {
+ private OperationalDatasetTimestamp mActiveTimestamp;
+ private String mNetworkName;
+ private byte[] mExtendedPanId;
+ private Integer mPanId;
+ private Integer mChannel;
+ private Integer mChannelPage;
+ private SparseArray<byte[]> mChannelMask;
+ private byte[] mPskc;
+ private byte[] mNetworkKey;
+ private IpPrefix mMeshLocalPrefix;
+ private SecurityPolicy mSecurityPolicy;
+ private SparseArray<byte[]> mUnknownTlvs;
+
+ /**
+ * Creates a {@link Builder} object with values from an {@link ActiveOperationalDataset}
+ * object.
+ */
+ public Builder(@NonNull ActiveOperationalDataset activeOpDataset) {
+ requireNonNull(activeOpDataset, "activeOpDataset cannot be null");
+
+ this.mActiveTimestamp = activeOpDataset.mActiveTimestamp;
+ this.mNetworkName = activeOpDataset.mNetworkName;
+ this.mExtendedPanId = activeOpDataset.mExtendedPanId.clone();
+ this.mPanId = activeOpDataset.mPanId;
+ this.mChannel = activeOpDataset.mChannel;
+ this.mChannelPage = activeOpDataset.mChannelPage;
+ this.mChannelMask = deepCloneSparseArray(activeOpDataset.mChannelMask);
+ this.mPskc = activeOpDataset.mPskc.clone();
+ this.mNetworkKey = activeOpDataset.mNetworkKey.clone();
+ this.mMeshLocalPrefix = activeOpDataset.mMeshLocalPrefix;
+ this.mSecurityPolicy = activeOpDataset.mSecurityPolicy;
+ this.mUnknownTlvs = deepCloneSparseArray(activeOpDataset.mUnknownTlvs);
+ }
+
+ /**
+ * Creates an empty {@link Builder} object.
+ *
+ * <p>An empty builder cannot build a new {@link ActiveOperationalDataset} object. The
+ * Active Operational Dataset parameters must be set with setters of this builder.
+ */
+ public Builder() {
+ mChannelMask = new SparseArray<>();
+ mUnknownTlvs = new SparseArray<>();
+ }
+
+ /**
+ * Sets the Active Timestamp.
+ *
+ * @param activeTimestamp Active Timestamp of the Operational Dataset
+ */
+ @NonNull
+ public Builder setActiveTimestamp(@NonNull OperationalDatasetTimestamp activeTimestamp) {
+ requireNonNull(activeTimestamp, "activeTimestamp cannot be null");
+ this.mActiveTimestamp = activeTimestamp;
+ return this;
+ }
+
+ /**
+ * Sets the Network Name.
+ *
+ * @param networkName the name of the Thread network
+ * @throws IllegalArgumentException if length of the UTF-8 representation of {@code
+ * networkName} isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link
+ * #LENGTH_MAX_NETWORK_NAME_BYTES}].
+ */
+ @NonNull
+ public Builder setNetworkName(
+ @NonNull
+ @Size(
+ min = LENGTH_MIN_NETWORK_NAME_BYTES,
+ max = LENGTH_MAX_NETWORK_NAME_BYTES)
+ String networkName) {
+ requireNonNull(networkName, "networkName cannot be null");
+
+ int nameLength = networkName.getBytes(UTF_8).length;
+ checkArgument(
+ nameLength >= LENGTH_MIN_NETWORK_NAME_BYTES
+ && nameLength <= LENGTH_MAX_NETWORK_NAME_BYTES,
+ "Invalid network name (length = %d, expectedLengthRange = [%d, %d])",
+ nameLength,
+ LENGTH_MIN_NETWORK_NAME_BYTES,
+ LENGTH_MAX_NETWORK_NAME_BYTES);
+ this.mNetworkName = networkName;
+ return this;
+ }
+
+ /**
+ * Sets the Extended PAN ID.
+ *
+ * <p>Use with caution. A randomly generated Extended PAN ID should be used for real Thread
+ * networks. It's discouraged to call this method to override the default value created by
+ * {@link ActiveOperationalDataset#createRandomDataset} in production.
+ *
+ * @throws IllegalArgumentException if length of {@code extendedPanId} is not {@link
+ * #LENGTH_EXTENDED_PAN_ID}.
+ */
+ @NonNull
+ public Builder setExtendedPanId(
+ @NonNull @Size(LENGTH_EXTENDED_PAN_ID) byte[] extendedPanId) {
+ requireNonNull(extendedPanId, "extendedPanId cannot be null");
+ checkArgument(
+ extendedPanId.length == LENGTH_EXTENDED_PAN_ID,
+ "Invalid extended PAN ID (length = %d, expectedLength = %d)",
+ extendedPanId.length,
+ LENGTH_EXTENDED_PAN_ID);
+ this.mExtendedPanId = extendedPanId.clone();
+ return this;
+ }
+
+ /**
+ * Sets the PAN ID.
+ *
+ * @throws IllegalArgumentException if {@code panId} is not in range of 0x0-0xfffe
+ */
+ @NonNull
+ public Builder setPanId(@IntRange(from = 0, to = 0xfffe) int panId) {
+ checkArgument(
+ panId >= 0 && panId <= 0xfffe,
+ "PAN ID exceeds allowed range (panid = %d, allowedRange = [0x0, 0xffff])",
+ panId);
+ this.mPanId = panId;
+ return this;
+ }
+
+ /**
+ * Sets the Channel Page and Channel.
+ *
+ * <p>Channel Pages other than {@link #CHANNEL_PAGE_24_GHZ} are undefined and may lead to
+ * unexpected behavior if it's applied to Thread devices.
+ *
+ * @throws IllegalArgumentException if invalid channel is specified for the {@code
+ * channelPage}
+ */
+ @NonNull
+ public Builder setChannel(
+ @IntRange(from = 0, to = 255) int page,
+ @IntRange(from = 0, to = 65535) int channel) {
+ checkArgument(
+ page >= 0 && page <= 255,
+ "Invalid channel page (page = %d, allowedRange = [0, 255])",
+ page);
+ if (page == CHANNEL_PAGE_24_GHZ) {
+ checkArgument(
+ channel >= CHANNEL_MIN_24_GHZ && channel <= CHANNEL_MAX_24_GHZ,
+ "Invalid channel %d in page %d (allowedChannelRange = [%d, %d])",
+ channel,
+ page,
+ CHANNEL_MIN_24_GHZ,
+ CHANNEL_MAX_24_GHZ);
+ } else {
+ checkArgument(
+ channel >= 0 && channel <= 65535,
+ "Invalid channel %d in page %d "
+ + "(channel = %d, allowedChannelRange = [0, 65535])",
+ channel,
+ page,
+ channel);
+ }
+
+ this.mChannelPage = page;
+ this.mChannel = channel;
+ return this;
+ }
+
+ /**
+ * Sets the Channel Mask.
+ *
+ * @throws IllegalArgumentException if {@code channelMask} is empty
+ */
+ @NonNull
+ public Builder setChannelMask(@NonNull @Size(min = 1) SparseArray<byte[]> channelMask) {
+ requireNonNull(channelMask, "channelMask cannot be null");
+ checkArgument(channelMask.size() > 0, "channelMask is empty");
+ this.mChannelMask = deepCloneSparseArray(channelMask);
+ return this;
+ }
+
+ /**
+ * Sets the PSKc.
+ *
+ * <p>Use with caution. A randomly generated PSKc should be used for real Thread networks.
+ * It's discouraged to call this method to override the default value created by {@link
+ * ActiveOperationalDataset#createRandomDataset} in production.
+ *
+ * @param pskc the key stretched version of the Commissioning Credential for the network
+ * @throws IllegalArgumentException if length of {@code pskc} is not {@link #LENGTH_PSKC}
+ */
+ @NonNull
+ public Builder setPskc(@NonNull @Size(LENGTH_PSKC) byte[] pskc) {
+ requireNonNull(pskc, "pskc cannot be null");
+ checkArgument(
+ pskc.length == LENGTH_PSKC,
+ "Invalid PSKc length (length = %d, expectedLength = %d)",
+ pskc.length,
+ LENGTH_PSKC);
+ this.mPskc = pskc.clone();
+ return this;
+ }
+
+ /**
+ * Sets the Network Key.
+ *
+ * <p>Use with caution, randomly generated Network Key should be used for real Thread
+ * networks. It's discouraged to call this method to override the default value created by
+ * {@link ActiveOperationalDataset#createRandomDataset} in production.
+ *
+ * @param networkKey a 128-bit security key-derivation key for the Thread Network
+ * @throws IllegalArgumentException if length of {@code networkKey} is not {@link
+ * #LENGTH_NETWORK_KEY}
+ */
+ @NonNull
+ public Builder setNetworkKey(@NonNull @Size(LENGTH_NETWORK_KEY) byte[] networkKey) {
+ requireNonNull(networkKey, "networkKey cannot be null");
+ checkArgument(
+ networkKey.length == LENGTH_NETWORK_KEY,
+ "Invalid network key length (length = %d, expectedLength = %d)",
+ networkKey.length,
+ LENGTH_NETWORK_KEY);
+ this.mNetworkKey = networkKey.clone();
+ return this;
+ }
+
+ /**
+ * Sets the Mesh-Local Prefix.
+ *
+ * @param meshLocalPrefix the prefix used for realm-local traffic within the mesh
+ * @throws IllegalArgumentException if prefix length of {@code meshLocalPrefix} isn't {@link
+ * #LENGTH_MESH_LOCAL_PREFIX_BITS} or {@code meshLocalPrefix} doesn't start with {@code
+ * 0xfd}
+ */
+ @NonNull
+ public Builder setMeshLocalPrefix(@NonNull IpPrefix meshLocalPrefix) {
+ requireNonNull(meshLocalPrefix, "meshLocalPrefix cannot be null");
+ checkArgument(
+ meshLocalPrefix.getPrefixLength() == LENGTH_MESH_LOCAL_PREFIX_BITS,
+ "Invalid mesh-local prefix length (length = %d, expectedLength = %d)",
+ meshLocalPrefix.getPrefixLength(),
+ LENGTH_MESH_LOCAL_PREFIX_BITS);
+ checkArgument(
+ meshLocalPrefix.getRawAddress()[0] == MESH_LOCAL_PREFIX_FIRST_BYTE,
+ "Mesh-local prefix must start with 0xfd: " + meshLocalPrefix);
+ this.mMeshLocalPrefix = meshLocalPrefix;
+ return this;
+ }
+
+ @NonNull
+ private Builder setMeshLocalPrefix(byte[] meshLocalPrefix) {
+ final int prefixLength = meshLocalPrefix.length * 8;
+ checkArgument(
+ prefixLength == LENGTH_MESH_LOCAL_PREFIX_BITS,
+ "Invalid mesh-local prefix length (length = %d, expectedLength = %d)",
+ prefixLength,
+ LENGTH_MESH_LOCAL_PREFIX_BITS);
+ byte[] ip6RawAddress = new byte[16];
+ System.arraycopy(meshLocalPrefix, 0, ip6RawAddress, 0, meshLocalPrefix.length);
+ try {
+ return setMeshLocalPrefix(
+ new IpPrefix(Inet6Address.getByAddress(ip6RawAddress), prefixLength));
+ } catch (UnknownHostException e) {
+ // Can't happen because numeric address is provided
+ throw new AssertionError(e);
+ }
+ }
+
+ /** Sets the Security Policy. */
+ @NonNull
+ public Builder setSecurityPolicy(@NonNull SecurityPolicy securityPolicy) {
+ requireNonNull(securityPolicy, "securityPolicy cannot be null");
+ this.mSecurityPolicy = securityPolicy;
+ return this;
+ }
+
+ /**
+ * Sets additional unknown TLVs.
+ *
+ * @hide
+ */
+ @NonNull
+ public Builder setUnknownTlvs(@NonNull SparseArray<byte[]> unknownTlvs) {
+ requireNonNull(unknownTlvs, "unknownTlvs cannot be null");
+ mUnknownTlvs = deepCloneSparseArray(unknownTlvs);
+ return this;
+ }
+
+ /** Adds one more unknown TLV. @hide */
+ @VisibleForTesting
+ @NonNull
+ public Builder addUnknownTlv(int type, byte[] value) {
+ mUnknownTlvs.put(type, value);
+ return this;
+ }
+
+ /**
+ * Creates a new {@link ActiveOperationalDataset} object.
+ *
+ * @throws IllegalStateException if any of the fields isn't set or the total length exceeds
+ * {@link #LENGTH_MAX_DATASET_TLVS} bytes
+ */
+ @NonNull
+ public ActiveOperationalDataset build() {
+ checkState(mActiveTimestamp != null, "Active Timestamp is missing");
+ checkState(mNetworkName != null, "Network Name is missing");
+ checkState(mExtendedPanId != null, "Extended PAN ID is missing");
+ checkState(mPanId != null, "PAN ID is missing");
+ checkState(mChannel != null, "Channel is missing");
+ checkState(mChannelPage != null, "Channel Page is missing");
+ checkState(mChannelMask.size() != 0, "Channel Mask is missing");
+ checkState(mPskc != null, "PSKc is missing");
+ checkState(mNetworkKey != null, "Network Key is missing");
+ checkState(mMeshLocalPrefix != null, "Mesh Local Prefix is missing");
+ checkState(mSecurityPolicy != null, "Security Policy is missing");
+
+ int length = getTotalDatasetLength();
+ if (length > LENGTH_MAX_DATASET_TLVS) {
+ throw new IllegalStateException(
+ String.format(
+ "Total dataset length exceeds max length %d (actual is %d)",
+ LENGTH_MAX_DATASET_TLVS, length));
+ }
+
+ return new ActiveOperationalDataset(this);
+ }
+
+ private int getTotalDatasetLength() {
+ int length =
+ 2 * 9 // 9 fields with 1 byte of type and 1 byte of length
+ + OperationalDatasetTimestamp.LENGTH_TIMESTAMP
+ + mNetworkName.getBytes(UTF_8).length
+ + LENGTH_EXTENDED_PAN_ID
+ + LENGTH_PAN_ID
+ + LENGTH_CHANNEL
+ + LENGTH_PSKC
+ + LENGTH_NETWORK_KEY
+ + LENGTH_MESH_LOCAL_PREFIX_BITS / 8
+ + mSecurityPolicy.toTlvValue().length;
+
+ for (int i = 0; i < mChannelMask.size(); i++) {
+ length += 2 + mChannelMask.valueAt(i).length;
+ }
+
+ // For the type and length bytes of the Channel Mask TLV because the masks are encoded
+ // as TLVs in TLV.
+ length += 2;
+
+ for (int i = 0; i < mUnknownTlvs.size(); i++) {
+ length += 2 + mUnknownTlvs.valueAt(i).length;
+ }
+
+ return length;
+ }
+ }
+
+ /**
+ * The Security Policy of Thread Operational Dataset which provides an administrator with a way
+ * to enable or disable certain security related behaviors.
+ */
+ public static final class SecurityPolicy {
+ /** The default Rotation Time in hours. */
+ public static final int DEFAULT_ROTATION_TIME_HOURS = 672;
+ /** The minimum length of Security Policy flags in bytes. */
+ public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1;
+ /** The length of Rotation Time TLV value in bytes. */
+ private static final int LENGTH_SECURITY_POLICY_ROTATION_TIME = 2;
+
+ private final int mRotationTimeHours;
+ private final byte[] mFlags;
+
+ /**
+ * Creates a new {@link SecurityPolicy} object.
+ *
+ * @param rotationTimeHours the value for Thread key rotation in hours. Must be in range of
+ * 0x1-0xffff.
+ * @param flags security policy flags with length of either 1 byte for Thread 1.1 or 2 bytes
+ * for Thread 1.2 or higher.
+ * @throws IllegalArgumentException if {@code rotationTimeHours} is not in range of
+ * 0x1-0xffff or length of {@code flags} is smaller than {@link
+ * #LENGTH_MIN_SECURITY_POLICY_FLAGS}.
+ */
+ public SecurityPolicy(
+ @IntRange(from = 0x1, to = 0xffff) int rotationTimeHours,
+ @NonNull @Size(min = LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[] flags) {
+ requireNonNull(flags, "flags cannot be null");
+ checkArgument(
+ rotationTimeHours >= 1 && rotationTimeHours <= 0xffff,
+ "Rotation time exceeds allowed range (rotationTimeHours = %d, allowedRange ="
+ + " [0x1, 0xffff])",
+ rotationTimeHours);
+ checkArgument(
+ flags.length >= LENGTH_MIN_SECURITY_POLICY_FLAGS,
+ "Invalid security policy flags length (length = %d, minimumLength = %d)",
+ flags.length,
+ LENGTH_MIN_SECURITY_POLICY_FLAGS);
+ this.mRotationTimeHours = rotationTimeHours;
+ this.mFlags = flags.clone();
+ }
+
+ /**
+ * Creates a new {@link SecurityPolicy} object from the Security Policy TLV value.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ @NonNull
+ public static SecurityPolicy fromTlvValue(byte[] encodedSecurityPolicy) {
+ checkArgument(
+ encodedSecurityPolicy.length
+ >= LENGTH_SECURITY_POLICY_ROTATION_TIME
+ + LENGTH_MIN_SECURITY_POLICY_FLAGS,
+ "Invalid Security Policy TLV length (length = %d, minimumLength = %d)",
+ encodedSecurityPolicy.length,
+ LENGTH_SECURITY_POLICY_ROTATION_TIME + LENGTH_MIN_SECURITY_POLICY_FLAGS);
+
+ return new SecurityPolicy(
+ ((encodedSecurityPolicy[0] & 0xff) << 8) | (encodedSecurityPolicy[1] & 0xff),
+ Arrays.copyOfRange(
+ encodedSecurityPolicy,
+ LENGTH_SECURITY_POLICY_ROTATION_TIME,
+ encodedSecurityPolicy.length));
+ }
+
+ /**
+ * Converts this {@link SecurityPolicy} object to Security Policy TLV value.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ @NonNull
+ public byte[] toTlvValue() {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ result.write(mRotationTimeHours >> 8);
+ result.write(mRotationTimeHours);
+ result.write(mFlags, 0, mFlags.length);
+ return result.toByteArray();
+ }
+
+ /** Returns the Security Policy Rotation Time in hours. */
+ @IntRange(from = 0x1, to = 0xffff)
+ public int getRotationTimeHours() {
+ return mRotationTimeHours;
+ }
+
+ /** Returns 1 byte flags for Thread 1.1 or 2 bytes flags for Thread 1.2. */
+ @NonNull
+ @Size(min = LENGTH_MIN_SECURITY_POLICY_FLAGS)
+ public byte[] getFlags() {
+ return mFlags.clone();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof SecurityPolicy)) {
+ return false;
+ } else {
+ SecurityPolicy otherSecurityPolicy = (SecurityPolicy) other;
+ return mRotationTimeHours == otherSecurityPolicy.mRotationTimeHours
+ && Arrays.equals(mFlags, otherSecurityPolicy.mFlags);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return deepHashCode(mRotationTimeHours, mFlags);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{rotation=")
+ .append(mRotationTimeHours)
+ .append(", flags=")
+ .append(dumpHexString(mFlags))
+ .append("}");
+ return sb.toString();
+ }
+ }
+}
diff --git a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
new file mode 100644
index 0000000..bda9373
--- /dev/null
+++ b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+
+import java.nio.ByteBuffer;
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * The timestamp of Thread Operational Dataset.
+ *
+ * @see ActiveOperationalDataset
+ * @see PendingOperationalDataset
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public final class OperationalDatasetTimestamp {
+ /** @hide */
+ public static final int LENGTH_TIMESTAMP = Long.BYTES;
+
+ private static final long TICKS_UPPER_BOUND = 0x8000;
+
+ private final Instant mInstant;
+ private final boolean mIsAuthoritativeSource;
+
+ /**
+ * Creates a new {@link OperationalDatasetTimestamp} object from an {@link Instant}.
+ *
+ * <p>The {@code seconds} is set to {@code instant.getEpochSecond()}, {@code ticks} is set to
+ * {@link instant#getNano()} based on frequency of 32768 Hz, and {@code isAuthoritativeSource}
+ * is set to {@code true}.
+ *
+ * @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
+ * 0xffffffffffffL}
+ */
+ @NonNull
+ public static OperationalDatasetTimestamp fromInstant(@NonNull Instant instant) {
+ return new OperationalDatasetTimestamp(instant, /* isAuthoritativeSource= */ true);
+ }
+
+ /** Converts this {@link OperationalDatasetTimestamp} object to an {@link Instant}. */
+ @NonNull
+ public Instant toInstant() {
+ return mInstant;
+ }
+
+ /**
+ * Creates a new {@link OperationalDatasetTimestamp} object from the OperationalDatasetTimestamp
+ * TLV value.
+ *
+ * @hide
+ */
+ @NonNull
+ public static OperationalDatasetTimestamp fromTlvValue(@NonNull byte[] encodedTimestamp) {
+ requireNonNull(encodedTimestamp, "encodedTimestamp cannot be null");
+ checkArgument(
+ encodedTimestamp.length == LENGTH_TIMESTAMP,
+ "Invalid Thread OperationalDatasetTimestamp length (length = %d,"
+ + " expectedLength=%d)",
+ encodedTimestamp.length,
+ LENGTH_TIMESTAMP);
+ long longTimestamp = ByteBuffer.wrap(encodedTimestamp).getLong();
+ return new OperationalDatasetTimestamp(
+ (longTimestamp >> 16) & 0x0000ffffffffffffL,
+ (int) ((longTimestamp >> 1) & 0x7fffL),
+ (longTimestamp & 0x01) != 0);
+ }
+
+ /**
+ * Converts this {@link OperationalDatasetTimestamp} object to Thread TLV value.
+ *
+ * @hide
+ */
+ @NonNull
+ public byte[] toTlvValue() {
+ byte[] tlv = new byte[LENGTH_TIMESTAMP];
+ ByteBuffer buffer = ByteBuffer.wrap(tlv);
+ long encodedValue =
+ (mInstant.getEpochSecond() << 16)
+ | ((mInstant.getNano() * TICKS_UPPER_BOUND / 1000000000L) << 1)
+ | (mIsAuthoritativeSource ? 1 : 0);
+ buffer.putLong(encodedValue);
+ return tlv;
+ }
+
+ /**
+ * Creates a new {@link OperationalDatasetTimestamp} object.
+ *
+ * @param seconds the value encodes a Unix Time value. Must be in the range of
+ * 0x0-0xffffffffffffL
+ * @param ticks the value encodes the fractional Unix Time value in 32.768 kHz resolution. Must
+ * be in the range of 0x0-0x7fff
+ * @param isAuthoritativeSource the flag indicates the time was obtained from an authoritative
+ * source: either NTP (Network Time Protocol), GPS (Global Positioning System), cell
+ * network, or other method
+ * @throws IllegalArgumentException if the {@code seconds} is not in range of
+ * 0x0-0xffffffffffffL or {@code ticks} is not in range of 0x0-0x7fff
+ */
+ public OperationalDatasetTimestamp(
+ @IntRange(from = 0x0, to = 0xffffffffffffL) long seconds,
+ @IntRange(from = 0x0, to = 0x7fff) int ticks,
+ boolean isAuthoritativeSource) {
+ this(makeInstant(seconds, ticks), isAuthoritativeSource);
+ }
+
+ private static Instant makeInstant(long seconds, int ticks) {
+ checkArgument(
+ seconds >= 0 && seconds <= 0xffffffffffffL,
+ "seconds exceeds allowed range (seconds = %d,"
+ + " allowedRange = [0x0, 0xffffffffffffL])",
+ seconds);
+ checkArgument(
+ ticks >= 0 && ticks <= 0x7fff,
+ "ticks exceeds allowed ranged (ticks = %d, allowedRange" + " = [0x0, 0x7fff])",
+ ticks);
+ long nanos = Math.round((double) ticks * 1000000000L / TICKS_UPPER_BOUND);
+ return Instant.ofEpochSecond(seconds, nanos);
+ }
+
+ /**
+ * Creates new {@link OperationalDatasetTimestamp} object.
+ *
+ * @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
+ * 0xffffffffffffL}
+ */
+ private OperationalDatasetTimestamp(@NonNull Instant instant, boolean isAuthoritativeSource) {
+ requireNonNull(instant, "instant cannot be null");
+ long seconds = instant.getEpochSecond();
+ checkArgument(
+ seconds >= 0 && seconds <= 0xffffffffffffL,
+ "instant seconds exceeds allowed range (seconds = %d, allowedRange = [0x0,"
+ + " 0xffffffffffffL])",
+ seconds);
+ mInstant = instant;
+ mIsAuthoritativeSource = isAuthoritativeSource;
+ }
+
+ /**
+ * Returns the rounded ticks converted from the nano seconds.
+ *
+ * <p>Note that rhe return value can be as large as {@code TICKS_UPPER_BOUND}.
+ */
+ private static int getRoundedTicks(long nanos) {
+ return (int) Math.round((double) nanos * TICKS_UPPER_BOUND / 1000000000L);
+ }
+
+ /** Returns the seconds portion of the timestamp. */
+ public @IntRange(from = 0x0, to = 0xffffffffffffL) long getSeconds() {
+ return mInstant.getEpochSecond() + getRoundedTicks(mInstant.getNano()) / TICKS_UPPER_BOUND;
+ }
+
+ /** Returns the ticks portion of the timestamp. */
+ public @IntRange(from = 0x0, to = 0x7fff) int getTicks() {
+ // the rounded ticks can be 0x8000 if mInstant.getNano() >= 999984742
+ return (int) (getRoundedTicks(mInstant.getNano()) % TICKS_UPPER_BOUND);
+ }
+
+ /** Returns {@code true} if the timestamp comes from an authoritative source. */
+ public boolean isAuthoritativeSource() {
+ return mIsAuthoritativeSource;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{seconds=")
+ .append(getSeconds())
+ .append(", ticks=")
+ .append(getTicks())
+ .append(", isAuthoritativeSource=")
+ .append(isAuthoritativeSource())
+ .append(", instant=")
+ .append(toInstant())
+ .append("}");
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof OperationalDatasetTimestamp)) {
+ return false;
+ } else {
+ OperationalDatasetTimestamp otherTimestamp = (OperationalDatasetTimestamp) other;
+ return mInstant.equals(otherTimestamp.mInstant)
+ && mIsAuthoritativeSource == otherTimestamp.mIsAuthoritativeSource;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mInstant, mIsAuthoritativeSource);
+ }
+}
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl b/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl
new file mode 100644
index 0000000..e5bc05e
--- /dev/null
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 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.thread;
+
+parcelable PendingOperationalDataset;
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.java b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
new file mode 100644
index 0000000..4762d7f
--- /dev/null
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * Data interface for managing a Thread Pending Operational Dataset.
+ *
+ * <p>The Pending Operational Dataset represents an Operational Dataset which will become Active in
+ * a given delay. This is typically used to deploy new network parameters (e.g. Network Key or
+ * Channel) to all devices in the network.
+ *
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public final class PendingOperationalDataset implements Parcelable {
+ // Value defined in Thread spec 8.10.1.16
+ private static final int TYPE_PENDING_TIMESTAMP = 51;
+
+ // Values defined in Thread spec 8.10.1.17
+ private static final int TYPE_DELAY_TIMER = 52;
+ private static final int LENGTH_DELAY_TIMER_BYTES = 4;
+
+ @NonNull
+ public static final Creator<PendingOperationalDataset> CREATOR =
+ new Creator<>() {
+ @Override
+ public PendingOperationalDataset createFromParcel(Parcel in) {
+ return PendingOperationalDataset.fromThreadTlvs(in.createByteArray());
+ }
+
+ @Override
+ public PendingOperationalDataset[] newArray(int size) {
+ return new PendingOperationalDataset[size];
+ }
+ };
+
+ @NonNull private final ActiveOperationalDataset mActiveOpDataset;
+ @NonNull private final OperationalDatasetTimestamp mPendingTimestamp;
+ @NonNull private final Duration mDelayTimer;
+
+ /** Creates a new {@link PendingOperationalDataset} object. */
+ public PendingOperationalDataset(
+ @NonNull ActiveOperationalDataset activeOpDataset,
+ @NonNull OperationalDatasetTimestamp pendingTimestamp,
+ @NonNull Duration delayTimer) {
+ requireNonNull(activeOpDataset, "activeOpDataset cannot be null");
+ requireNonNull(pendingTimestamp, "pendingTimestamp cannot be null");
+ requireNonNull(delayTimer, "delayTimer cannot be null");
+ this.mActiveOpDataset = activeOpDataset;
+ this.mPendingTimestamp = pendingTimestamp;
+ this.mDelayTimer = delayTimer;
+ }
+
+ /**
+ * Creates a new {@link PendingOperationalDataset} object from a series of Thread TLVs.
+ *
+ * <p>{@code tlvs} can be obtained from the value of a Thread Pending Operational Dataset TLV
+ * (see the <a href="https://www.threadgroup.org/support#specifications">Thread
+ * specification</a> for the definition) or the return value of {@link #toThreadTlvs}.
+ *
+ * @throws IllegalArgumentException if {@code tlvs} is malformed or contains an invalid Thread
+ * TLV
+ */
+ @NonNull
+ public static PendingOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) {
+ requireNonNull(tlvs, "tlvs cannot be null");
+
+ SparseArray<byte[]> newUnknownTlvs = new SparseArray<>();
+ OperationalDatasetTimestamp pendingTimestamp = null;
+ Duration delayTimer = null;
+ ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(tlvs);
+ SparseArray<byte[]> unknownTlvs = activeDataset.getUnknownTlvs();
+ for (int i = 0; i < unknownTlvs.size(); i++) {
+ int key = unknownTlvs.keyAt(i);
+ byte[] value = unknownTlvs.valueAt(i);
+ switch (key) {
+ case TYPE_PENDING_TIMESTAMP:
+ pendingTimestamp = OperationalDatasetTimestamp.fromTlvValue(value);
+ break;
+ case TYPE_DELAY_TIMER:
+ checkArgument(
+ value.length == LENGTH_DELAY_TIMER_BYTES,
+ "Invalid delay timer (length = %d, expectedLength = %d)",
+ value.length,
+ LENGTH_DELAY_TIMER_BYTES);
+ int millis = ByteBuffer.wrap(value).getInt();
+ delayTimer = Duration.ofMillis(Integer.toUnsignedLong(millis));
+ break;
+ default:
+ newUnknownTlvs.put(key, value);
+ break;
+ }
+ }
+
+ if (pendingTimestamp == null) {
+ throw new IllegalArgumentException("Pending Timestamp is missing");
+ }
+ if (delayTimer == null) {
+ throw new IllegalArgumentException("Delay Timer is missing");
+ }
+
+ activeDataset =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setUnknownTlvs(newUnknownTlvs)
+ .build();
+ return new PendingOperationalDataset(activeDataset, pendingTimestamp, delayTimer);
+ }
+
+ /** Returns the Active Operational Dataset. */
+ @NonNull
+ public ActiveOperationalDataset getActiveOperationalDataset() {
+ return mActiveOpDataset;
+ }
+
+ /** Returns the Pending Timestamp. */
+ @NonNull
+ public OperationalDatasetTimestamp getPendingTimestamp() {
+ return mPendingTimestamp;
+ }
+
+ /** Returns the Delay Timer. */
+ @NonNull
+ public Duration getDelayTimer() {
+ return mDelayTimer;
+ }
+
+ /**
+ * Converts this {@link PendingOperationalDataset} object to a series of Thread TLVs.
+ *
+ * <p>See the <a href="https://www.threadgroup.org/support#specifications">Thread
+ * specification</a> for the definition of the Thread TLV format.
+ */
+ @NonNull
+ public byte[] toThreadTlvs() {
+ ByteArrayOutputStream dataset = new ByteArrayOutputStream();
+
+ byte[] activeDatasetBytes = mActiveOpDataset.toThreadTlvs();
+ dataset.write(activeDatasetBytes, 0, activeDatasetBytes.length);
+
+ dataset.write(TYPE_PENDING_TIMESTAMP);
+ byte[] pendingTimestampBytes = mPendingTimestamp.toTlvValue();
+ dataset.write(pendingTimestampBytes.length);
+ dataset.write(pendingTimestampBytes, 0, pendingTimestampBytes.length);
+
+ dataset.write(TYPE_DELAY_TIMER);
+ byte[] delayTimerBytes = new byte[LENGTH_DELAY_TIMER_BYTES];
+ ByteBuffer.wrap(delayTimerBytes).putInt((int) mDelayTimer.toMillis());
+ dataset.write(delayTimerBytes.length);
+ dataset.write(delayTimerBytes, 0, delayTimerBytes.length);
+
+ return dataset.toByteArray();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof PendingOperationalDataset)) {
+ return false;
+ } else {
+ PendingOperationalDataset otherDataset = (PendingOperationalDataset) other;
+ return mActiveOpDataset.equals(otherDataset.mActiveOpDataset)
+ && mPendingTimestamp.equals(otherDataset.mPendingTimestamp)
+ && mDelayTimer.equals(otherDataset.mDelayTimer);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mActiveOpDataset, mPendingTimestamp, mDelayTimer);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{activeDataset=")
+ .append(getActiveOperationalDataset())
+ .append(", pendingTimestamp=")
+ .append(getPendingTimestamp())
+ .append(", delayTimer=")
+ .append(getDelayTimer())
+ .append("}");
+ return sb.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeByteArray(toThreadTlvs());
+ }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index fe189c2..7575757 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -18,6 +18,7 @@
import static java.util.Objects.requireNonNull;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SystemApi;
@@ -30,9 +31,10 @@
* Provides the primary API for controlling all aspects of a Thread network.
*
* @hide
- */
+*/
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
-public class ThreadNetworkController {
+public final class ThreadNetworkController {
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
new file mode 100644
index 0000000..e6ab988
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+/**
+ * Container for flag constants defined in the "thread_network" namespace.
+ *
+ * @hide
+ */
+// TODO: replace this class with auto-generated "com.android.net.thread.flags.Flags" once the
+// flagging infra is fully supported for mainline modules.
+public final class ThreadNetworkFlags {
+ /** @hide */
+ public static final String FLAG_THREAD_ENABLED = "com.android.net.thread.flags.thread_enabled";
+
+ private ThreadNetworkFlags() {}
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index 2a253a1..c3bdbd7 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -18,6 +18,7 @@
import static java.util.Objects.requireNonNull;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.annotation.SystemService;
@@ -34,9 +35,10 @@
*
* @hide
*/
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
@SystemService(ThreadNetworkManager.SERVICE_NAME)
-public class ThreadNetworkManager {
+public final class ThreadNetworkManager {
/**
* This value tracks {@link Context#THREAD_NETWORK_SERVICE}.
*
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 96056c6..ce770e0 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -37,8 +37,9 @@
"androidx.test.ext.junit",
"compatibility-device-util-axt",
"ctstestrunner-axt",
+ "guava-android-testlib",
"net-tests-utils",
- "truth-prebuilt",
+ "truth",
],
libs: [
"android.test.base",
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index 5ba605f..ffc181c 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -47,5 +47,7 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.net.thread.cts" />
+ <!-- Ignores tests introduced by guava-android-testlib -->
+ <option name="exclude-annotation" value="org.junit.Ignore"/>
</test>
</configuration>
diff --git a/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
new file mode 100644
index 0000000..39df21b
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
@@ -0,0 +1,737 @@
+/*
+ * Copyright (C) 2023 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.thread.cts;
+
+import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ActiveOperationalDataset.Builder;
+import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.primitives.Bytes;
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+
+/** CTS tests for {@link ActiveOperationalDataset}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ActiveOperationalDatasetTest {
+ private static final int TYPE_ACTIVE_TIMESTAMP = 14;
+ private static final int TYPE_CHANNEL = 0;
+ private static final int TYPE_CHANNEL_MASK = 53;
+ private static final int TYPE_EXTENDED_PAN_ID = 2;
+ private static final int TYPE_MESH_LOCAL_PREFIX = 7;
+ private static final int TYPE_NETWORK_KEY = 5;
+ private static final int TYPE_NETWORK_NAME = 3;
+ private static final int TYPE_PAN_ID = 1;
+ private static final int TYPE_PSKC = 4;
+ private static final int TYPE_SECURITY_POLICY = 12;
+
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ private static final byte[] VALID_DATASET =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+
+ private static byte[] removeTlv(byte[] dataset, int type) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream(dataset.length);
+ int i = 0;
+ while (i < dataset.length) {
+ int ty = dataset[i++] & 0xff;
+ byte length = dataset[i++];
+ if (ty != type) {
+ byte[] value = Arrays.copyOfRange(dataset, i, i + length);
+ os.write(ty);
+ os.write(length);
+ os.writeBytes(value);
+ }
+ i += length;
+ }
+ return os.toByteArray();
+ }
+
+ private static byte[] addTlv(byte[] dataset, String tlvHex) {
+ return Bytes.concat(dataset, base16().decode(tlvHex));
+ }
+
+ private static byte[] replaceTlv(byte[] dataset, int type, String newTlvHex) {
+ return addTlv(removeTlv(dataset, type), newTlvHex);
+ }
+
+ @Test
+ public void parcelable_parcelingIsLossLess() {
+ ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET);
+
+ assertParcelingIsLossless(dataset);
+ }
+
+ @Test
+ public void fromThreadTlvs_tooLongTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = new byte[255];
+ invalidTlv[0] = (byte) 0xff;
+
+ // This is invalid because the TLV has max total length of 254 bytes and the value length
+ // can't exceeds 252 ( = 254 - 1 - 1)
+ invalidTlv[1] = (byte) 253;
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidNetworkKeyTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_NETWORK_KEY, "05080000000000000000");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noNetworkKeyTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_NETWORK_KEY);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidActiveTimestampTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_ACTIVE_TIMESTAMP, "0E0700000000010000");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noActiveTimestampTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_ACTIVE_TIMESTAMP);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidNetworkNameTlv_emptyName_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_NETWORK_NAME, "0300");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidNetworkNameTlv_tooLongName_throwsIllegalArgument() {
+ byte[] invalidTlv =
+ replaceTlv(
+ VALID_DATASET, TYPE_NETWORK_NAME, "03114142434445464748494A4B4C4D4E4F5051");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noNetworkNameTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_NETWORK_NAME);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidChannelTlv_channelMissing_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000100");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_undefinedChannelPage_success() {
+ byte[] datasetTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "0003010020");
+
+ ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlv);
+
+ assertThat(dataset.getChannelPage()).isEqualTo(0x01);
+ assertThat(dataset.getChannel()).isEqualTo(0x20);
+ }
+
+ @Test
+ public void fromThreadTlvs_invalid2P4GhzChannel_throwsIllegalArgument() {
+ byte[] invalidTlv1 = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000300000A");
+ byte[] invalidTlv2 = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "000300001B");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv1));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv2));
+ }
+
+ @Test
+ public void fromThreadTlvs_valid2P4GhzChannelTlv_success() {
+ byte[] validTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL, "0003000010");
+
+ ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validTlv);
+
+ assertThat(dataset.getChannel()).isEqualTo(16);
+ }
+
+ @Test
+ public void fromThreadTlvs_noChannelTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_CHANNEL);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_prematureEndOfChannelMaskEntry_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "350100");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_inconsistentChannelMaskLength_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "3506000500010000");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_unsupportedChannelMaskLength_success() {
+ ActiveOperationalDataset dataset =
+ ActiveOperationalDataset.fromThreadTlvs(
+ replaceTlv(VALID_DATASET, TYPE_CHANNEL_MASK, "350700050001000000"));
+
+ SparseArray<byte[]> channelMask = dataset.getChannelMask();
+ assertThat(channelMask.size()).isEqualTo(1);
+ assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ))
+ .isEqualTo(new byte[] {0x00, 0x01, 0x00, 0x00, 0x00});
+ }
+
+ @Test
+ public void fromThreadTlvs_noChannelMaskTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_CHANNEL_MASK);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidPanIdTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_PAN_ID, "010101");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noPanIdTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_PAN_ID);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidExtendedPanIdTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_EXTENDED_PAN_ID, "020700010203040506");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noExtendedPanIdTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_EXTENDED_PAN_ID);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidPskcTlv_throwsIllegalArgument() {
+ byte[] invalidTlv =
+ replaceTlv(VALID_DATASET, TYPE_PSKC, "0411000102030405060708090A0B0C0D0E0F10");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noPskcTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_PSKC);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidMeshLocalPrefixTlv_throwsIllegalArgument() {
+ byte[] invalidTlv =
+ replaceTlv(VALID_DATASET, TYPE_MESH_LOCAL_PREFIX, "0709FD0001020304050607");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noMeshLocalPrefixTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_MESH_LOCAL_PREFIX);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_tooShortSecurityPolicyTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET, TYPE_SECURITY_POLICY, "0C0101");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noSecurityPolicyTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET, TYPE_SECURITY_POLICY);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_lengthAndDataMissing_throwsIllegalArgument() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {(byte) 0x00}));
+ }
+
+ @Test
+ public void fromThreadTlvs_prematureEndOfData_throwsIllegalArgument() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {0x00, 0x03, 0x00, 0x00}));
+ }
+
+ @Test
+ public void fromThreadTlvs_validFullDataset_success() {
+ // A valid Thread active operational dataset:
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ byte[] validDatasetTlv =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+
+ ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validDatasetTlv);
+
+ assertThat(dataset.getNetworkKey())
+ .isEqualTo(base16().decode("F26B3153760F519A63BAFDDFFC80D2AF"));
+ assertThat(dataset.getPanId()).isEqualTo(0xd9a0);
+ assertThat(dataset.getExtendedPanId()).isEqualTo(base16().decode("ACC214689BC40BDF"));
+ assertThat(dataset.getChannel()).isEqualTo(19);
+ assertThat(dataset.getNetworkName()).isEqualTo("OpenThread-d9a0");
+ assertThat(dataset.getPskc())
+ .isEqualTo(base16().decode("A245479C836D551B9CA557F7B9D351B4"));
+ assertThat(dataset.getActiveTimestamp())
+ .isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
+ SparseArray<byte[]> channelMask = dataset.getChannelMask();
+ assertThat(channelMask.size()).isEqualTo(1);
+ assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ))
+ .isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
+ assertThat(dataset.getMeshLocalPrefix())
+ .isEqualTo(new IpPrefix("fd64:db12:25f4:7e0b::/64"));
+ assertThat(dataset.getSecurityPolicy())
+ .isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}));
+ }
+
+ @Test
+ public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
+ final byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET, "AA01FFBB020102");
+
+ ActiveOperationalDataset dataset =
+ ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
+
+ byte[] newDatasetTlvs = dataset.toThreadTlvs();
+ String newDatasetTlvsHex = base16().encode(newDatasetTlvs);
+ assertThat(newDatasetTlvs.length).isEqualTo(datasetWithUnknownTlvs.length);
+ assertThat(newDatasetTlvsHex).contains("AA01FF");
+ assertThat(newDatasetTlvsHex).contains("BB020102");
+ }
+
+ @Test
+ public void toThreadTlvs_conversionIsLossLess() {
+ ActiveOperationalDataset dataset1 = ActiveOperationalDataset.createRandomDataset();
+
+ ActiveOperationalDataset dataset2 =
+ ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+ assertThat(dataset2).isEqualTo(dataset1);
+ }
+
+ @Test
+ public void builder_buildWithdefaultValues_throwsIllegalState() {
+ assertThrows(IllegalStateException.class, () -> new Builder().build());
+ }
+
+ @Test
+ public void builder_setValidNetworkKey_success() {
+ final byte[] networkKey =
+ new byte[] {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
+ 0x0d, 0x0e, 0x0f
+ };
+
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setNetworkKey(networkKey)
+ .build();
+
+ assertThat(dataset.getNetworkKey()).isEqualTo(networkKey);
+ }
+
+ @Test
+ public void builder_setInvalidNetworkKey_throwsIllegalArgument() {
+ byte[] invalidNetworkKey = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(
+ IllegalArgumentException.class, () -> builder.setNetworkKey(invalidNetworkKey));
+ }
+
+ @Test
+ public void builder_setValidExtendedPanId_success() {
+ byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
+
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setExtendedPanId(extendedPanId)
+ .build();
+
+ assertThat(dataset.getExtendedPanId()).isEqualTo(extendedPanId);
+ }
+
+ @Test
+ public void builder_setInvalidExtendedPanId_throwsIllegalArgument() {
+ byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setExtendedPanId(extendedPanId));
+ }
+
+ @Test
+ public void builder_setValidPanId_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setPanId(0xfffe)
+ .build();
+
+ assertThat(dataset.getPanId()).isEqualTo(0xfffe);
+ }
+
+ @Test
+ public void builder_setInvalidPanId_throwsIllegalArgument() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setPanId(0xffff));
+ }
+
+ @Test
+ public void builder_setInvalidChannel_throwsIllegalArgument() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 0));
+ assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 27));
+ }
+
+ @Test
+ public void builder_setValid2P4GhzChannel_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setChannel(CHANNEL_PAGE_24_GHZ, 16)
+ .build();
+
+ assertThat(dataset.getChannel()).isEqualTo(16);
+ assertThat(dataset.getChannelPage()).isEqualTo(CHANNEL_PAGE_24_GHZ);
+ }
+
+ @Test
+ public void builder_setValidNetworkName_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setNetworkName("ot-network")
+ .build();
+
+ assertThat(dataset.getNetworkName()).isEqualTo("ot-network");
+ }
+
+ @Test
+ public void builder_setEmptyNetworkName_throwsIllegalArgument() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName(""));
+ }
+
+ @Test
+ public void builder_setTooLongNetworkName_throwsIllegalArgument() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(
+ IllegalArgumentException.class, () -> builder.setNetworkName("openthread-network"));
+ }
+
+ @Test
+ public void builder_setTooLongUtf8NetworkName_throwsIllegalArgument() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ // UTF-8 encoded length of "我的线程网络" is 18 bytes which exceeds the max length
+ assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName("我的线程网络"));
+ }
+
+ @Test
+ public void builder_setValidUtf8NetworkName_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setNetworkName("我的网络")
+ .build();
+
+ assertThat(dataset.getNetworkName()).isEqualTo("我的网络");
+ }
+
+ @Test
+ public void builder_setValidPskc_success() {
+ byte[] pskc = base16().decode("A245479C836D551B9CA557F7B9D351B4");
+
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset()).setPskc(pskc).build();
+
+ assertThat(dataset.getPskc()).isEqualTo(pskc);
+ }
+
+ @Test
+ public void builder_setTooLongPskc_throwsIllegalArgument() {
+ byte[] tooLongPskc = base16().decode("A245479C836D551B9CA557F7B9D351B400");
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setPskc(tooLongPskc));
+ }
+
+ @Test
+ public void builder_setValidChannelMask_success() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ SparseArray<byte[]> channelMask = new SparseArray<byte[]>(1);
+ channelMask.put(0, new byte[] {0x00, 0x00, 0x01, 0x00});
+
+ ActiveOperationalDataset dataset = builder.setChannelMask(channelMask).build();
+
+ SparseArray<byte[]> resultChannelMask = dataset.getChannelMask();
+ assertThat(resultChannelMask.size()).isEqualTo(1);
+ assertThat(resultChannelMask.get(0)).isEqualTo(new byte[] {0x00, 0x00, 0x01, 0x00});
+ }
+
+ @Test
+ public void builder_setEmptyChannelMask_throwsIllegalArgument() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setChannelMask(new SparseArray<byte[]>()));
+ }
+
+ @Test
+ public void builder_setValidActiveTimestamp_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setActiveTimestamp(
+ new OperationalDatasetTimestamp(
+ /* seconds= */ 1,
+ /* ticks= */ 0,
+ /* isAuthoritativeSource= */ true))
+ .build();
+
+ assertThat(dataset.getActiveTimestamp().getSeconds()).isEqualTo(1);
+ assertThat(dataset.getActiveTimestamp().getTicks()).isEqualTo(0);
+ assertThat(dataset.getActiveTimestamp().isAuthoritativeSource()).isTrue();
+ }
+
+ @Test
+ public void builder_wrongMeshLocalPrefixLength_throwsIllegalArguments() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ // The Mesh-Local Prefix length must be 64 bits
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/32")));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/96")));
+
+ // The Mesh-Local Prefix must start with 0xfd
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64")));
+ }
+
+ @Test
+ public void builder_meshLocalPrefixNotStartWith0xfd_throwsIllegalArguments() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64")));
+ }
+
+ @Test
+ public void builder_setValidMeshLocalPrefix_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setMeshLocalPrefix(new IpPrefix("fd00::/64"))
+ .build();
+
+ assertThat(dataset.getMeshLocalPrefix()).isEqualTo(new IpPrefix("fd00::/64"));
+ }
+
+ @Test
+ public void builder_setValid1P2SecurityPolicy_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setSecurityPolicy(
+ new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .build();
+
+ assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
+ assertThat(dataset.getSecurityPolicy().getFlags())
+ .isEqualTo(new byte[] {(byte) 0xff, (byte) 0xf8});
+ }
+
+ @Test
+ public void builder_setValid1P1SecurityPolicy_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(ActiveOperationalDataset.createRandomDataset())
+ .setSecurityPolicy(new SecurityPolicy(672, new byte[] {(byte) 0xff}))
+ .build();
+
+ assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
+ assertThat(dataset.getSecurityPolicy().getFlags()).isEqualTo(new byte[] {(byte) 0xff});
+ }
+
+ @Test
+ public void securityPolicy_invalidRotationTime_throwsIllegalArguments() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new SecurityPolicy(0, new byte[] {(byte) 0xff, (byte) 0xf8}));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new SecurityPolicy(0x1ffff, new byte[] {(byte) 0xff, (byte) 0xf8}));
+ }
+
+ @Test
+ public void securityPolicy_emptyFlags_throwsIllegalArguments() {
+ assertThrows(IllegalArgumentException.class, () -> new SecurityPolicy(672, new byte[] {}));
+ }
+
+ @Test
+ public void securityPolicy_tooLongFlags_success() {
+ SecurityPolicy securityPolicy =
+ new SecurityPolicy(672, new byte[] {0, 1, 2, 3, 4, 5, 6, 7});
+
+ assertThat(securityPolicy.getFlags()).isEqualTo(new byte[] {0, 1, 2, 3, 4, 5, 6, 7});
+ }
+
+ @Test
+ public void securityPolicy_equals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}),
+ new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .addEqualityGroup(
+ new SecurityPolicy(1, new byte[] {(byte) 0xff}),
+ new SecurityPolicy(1, new byte[] {(byte) 0xff}))
+ .addEqualityGroup(
+ new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}),
+ new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .testEquals();
+ }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
new file mode 100644
index 0000000..9be3d56
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 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.thread.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.thread.OperationalDatasetTimestamp;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+
+/** Tests for {@link OperationalDatasetTimestamp}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class OperationalDatasetTimestampTest {
+ @Test
+ public void fromInstant_tooLargeInstant_throwsIllegalArgument() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ OperationalDatasetTimestamp.fromInstant(
+ Instant.ofEpochSecond(0xffffffffffffL + 1L)));
+ }
+
+ @Test
+ public void fromInstant_ticksIsRounded() {
+ Instant instant = Instant.ofEpochSecond(100L);
+
+ // 32767.5 / 32768 * 1000000000 = 999984741.2109375 and given the `ticks` is rounded, so
+ // the `ticks` should be 32767 for 999984741 and 0 (carried over to seconds) for 999984742.
+ OperationalDatasetTimestamp timestampTicks32767 =
+ OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984741));
+ OperationalDatasetTimestamp timestampTicks0 =
+ OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984742));
+
+ assertThat(timestampTicks32767.getSeconds()).isEqualTo(100L);
+ assertThat(timestampTicks0.getSeconds()).isEqualTo(101L);
+ assertThat(timestampTicks32767.getTicks()).isEqualTo(32767);
+ assertThat(timestampTicks0.getTicks()).isEqualTo(0);
+ assertThat(timestampTicks32767.isAuthoritativeSource()).isTrue();
+ assertThat(timestampTicks0.isAuthoritativeSource()).isTrue();
+ }
+
+ @Test
+ public void toInstant_nanosIsRounded() {
+ // 32767 / 32768 * 1000000000 = 999969482.421875
+ assertThat(new OperationalDatasetTimestamp(100L, 32767, false).toInstant().getNano())
+ .isEqualTo(999969482);
+
+ // 32766 / 32768 * 1000000000 = 999938964.84375
+ assertThat(new OperationalDatasetTimestamp(100L, 32766, false).toInstant().getNano())
+ .isEqualTo(999938965);
+ }
+
+ @Test
+ public void toInstant_onlyAuthoritativeSourceDiscarded() {
+ OperationalDatasetTimestamp timestamp1 =
+ new OperationalDatasetTimestamp(100L, 0x7fff, false);
+
+ OperationalDatasetTimestamp timestamp2 =
+ OperationalDatasetTimestamp.fromInstant(timestamp1.toInstant());
+
+ assertThat(timestamp2.getSeconds()).isEqualTo(100L);
+ assertThat(timestamp2.getTicks()).isEqualTo(0x7fff);
+ assertThat(timestamp2.isAuthoritativeSource()).isTrue();
+ }
+
+ @Test
+ public void constructor_tooLargeSeconds_throwsIllegalArguments() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new OperationalDatasetTimestamp(
+ /* seconds= */ 0x0001112233445566L,
+ /* ticks= */ 0,
+ /* isAuthoritativeSource= */ true));
+ }
+
+ @Test
+ public void constructor_tooLargeTicks_throwsIllegalArguments() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new OperationalDatasetTimestamp(
+ /* seconds= */ 0x01L,
+ /* ticks= */ 0x8000,
+ /* isAuthoritativeSource= */ true));
+ }
+
+ @Test
+ public void equalityTests() {
+ new EqualsTester()
+ .addEqualityGroup(
+ new OperationalDatasetTimestamp(100, 100, false),
+ new OperationalDatasetTimestamp(100, 100, false))
+ .addEqualityGroup(
+ new OperationalDatasetTimestamp(0, 0, false),
+ new OperationalDatasetTimestamp(0, 0, false))
+ .addEqualityGroup(
+ new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true),
+ new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true))
+ .testEquals();
+ }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
new file mode 100644
index 0000000..7a49957
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2023 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.thread.cts;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.PendingOperationalDataset;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.primitives.Bytes;
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Duration;
+
+/** Tests for {@link PendingOperationalDataset}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class PendingOperationalDatasetTest {
+ private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
+ ActiveOperationalDataset.createRandomDataset();
+
+ @Test
+ public void parcelable_parcelingIsLossLess() {
+ PendingOperationalDataset dataset =
+ new PendingOperationalDataset(
+ DEFAULT_ACTIVE_DATASET,
+ new OperationalDatasetTimestamp(31536000, 200, false),
+ Duration.ofHours(100));
+
+ assertParcelingIsLossless(dataset);
+ }
+
+ @Test
+ public void equalityTests() {
+ ActiveOperationalDataset activeDataset1 = ActiveOperationalDataset.createRandomDataset();
+ ActiveOperationalDataset activeDataset2 = ActiveOperationalDataset.createRandomDataset();
+
+ new EqualsTester()
+ .addEqualityGroup(
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(31536000, 100, false),
+ Duration.ofMillis(0)),
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(31536000, 100, false),
+ Duration.ofMillis(0)))
+ .addEqualityGroup(
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(31536000, 100, false),
+ Duration.ofMillis(0)),
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(31536000, 100, false),
+ Duration.ofMillis(0)))
+ .addEqualityGroup(
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(15768000, 0, false),
+ Duration.ofMillis(0)),
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(15768000, 0, false),
+ Duration.ofMillis(0)))
+ .addEqualityGroup(
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(15768000, 0, false),
+ Duration.ofMillis(100)),
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(15768000, 0, false),
+ Duration.ofMillis(100)))
+ .testEquals();
+ }
+
+ @Test
+ public void constructor_correctValuesAreSet() {
+ PendingOperationalDataset dataset =
+ new PendingOperationalDataset(
+ DEFAULT_ACTIVE_DATASET,
+ new OperationalDatasetTimestamp(31536000, 200, false),
+ Duration.ofHours(100));
+
+ assertThat(dataset.getActiveOperationalDataset()).isEqualTo(DEFAULT_ACTIVE_DATASET);
+ assertThat(dataset.getPendingTimestamp())
+ .isEqualTo(new OperationalDatasetTimestamp(31536000, 200, false));
+ assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofHours(100));
+ }
+
+ @Test
+ public void fromThreadTlvs_openthreadTlvs_success() {
+ // An example Pending Operational Dataset which is generated with OpenThread CLI:
+ // Pending Timestamp: 2
+ // Active Timestamp: 1
+ // Channel: 26
+ // Channel Mask: 0x07fff800
+ // Delay: 46354
+ // Ext PAN ID: a74182f4d3f4de41
+ // Mesh Local Prefix: fd46:c1b9:e159:5574::/64
+ // Network Key: ed916e454d96fd00184f10a6f5c9e1d3
+ // Network Name: OpenThread-bff8
+ // PAN ID: 0xbff8
+ // PSKc: 264f78414adc683191863d968f72d1b7
+ // Security Policy: 672 onrc
+ final byte[] OPENTHREAD_PENDING_DATASET_TLVS =
+ base16().lowerCase()
+ .decode(
+ "0e0800000000000100003308000000000002000034040000b51200030000"
+ + "1a35060004001fffe00208a74182f4d3f4de410708fd46c1b9"
+ + "e15955740510ed916e454d96fd00184f10a6f5c9e1d3030f4f"
+ + "70656e5468726561642d626666380102bff80410264f78414a"
+ + "dc683191863d968f72d1b70c0402a0f7f8");
+
+ PendingOperationalDataset pendingDataset =
+ PendingOperationalDataset.fromThreadTlvs(OPENTHREAD_PENDING_DATASET_TLVS);
+
+ ActiveOperationalDataset activeDataset = pendingDataset.getActiveOperationalDataset();
+ assertThat(pendingDataset.getPendingTimestamp().getSeconds()).isEqualTo(2L);
+ assertThat(activeDataset.getActiveTimestamp().getSeconds()).isEqualTo(1L);
+ assertThat(activeDataset.getChannel()).isEqualTo(26);
+ assertThat(activeDataset.getChannelMask().get(0))
+ .isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
+ assertThat(pendingDataset.getDelayTimer().toMillis()).isEqualTo(46354);
+ assertThat(activeDataset.getExtendedPanId())
+ .isEqualTo(base16().lowerCase().decode("a74182f4d3f4de41"));
+ assertThat(activeDataset.getMeshLocalPrefix())
+ .isEqualTo(new IpPrefix("fd46:c1b9:e159:5574::/64"));
+ assertThat(activeDataset.getNetworkKey())
+ .isEqualTo(base16().lowerCase().decode("ed916e454d96fd00184f10a6f5c9e1d3"));
+ assertThat(activeDataset.getNetworkName()).isEqualTo("OpenThread-bff8");
+ assertThat(activeDataset.getPanId()).isEqualTo(0xbff8);
+ assertThat(activeDataset.getPskc())
+ .isEqualTo(base16().lowerCase().decode("264f78414adc683191863d968f72d1b7"));
+ assertThat(activeDataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
+ assertThat(activeDataset.getSecurityPolicy().getFlags())
+ .isEqualTo(new byte[] {(byte) 0xf7, (byte) 0xf8});
+ }
+
+ @Test
+ public void fromThreadTlvs_completePendingDatasetTlvs_success() {
+ // Type Length Value
+ // 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
+ // 0x34 0x04 0x0000012C (Delay Timer TLV)
+ final byte[] pendingTimestampAndDelayTimerTlvs =
+ base16().decode("3308000000000001000034040000012C");
+ final byte[] pendingDatasetTlvs =
+ Bytes.concat(
+ pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
+
+ PendingOperationalDataset dataset =
+ PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs);
+
+ assertThat(dataset.getActiveOperationalDataset()).isEqualTo(DEFAULT_ACTIVE_DATASET);
+ assertThat(dataset.getPendingTimestamp())
+ .isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
+ assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofMillis(300));
+ }
+
+ @Test
+ public void fromThreadTlvs_PendingTimestampTlvIsMissing_throwsIllegalArgument() {
+ // Type Length Value
+ // 0x34 0x04 0x00000064 (Delay Timer TLV)
+ final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("34040000012C");
+ final byte[] pendingDatasetTlvs =
+ Bytes.concat(
+ pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs));
+ }
+
+ @Test
+ public void fromThreadTlvs_delayTimerTlvIsMissing_throwsIllegalArgument() {
+ // Type Length Value
+ // 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
+ final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("33080000000000010000");
+ final byte[] pendingDatasetTlvs =
+ Bytes.concat(
+ pendingTimestampAndDelayTimerTlvs, DEFAULT_ACTIVE_DATASET.toThreadTlvs());
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs));
+ }
+
+ @Test
+ public void fromThreadTlvs_activeDatasetTlvs_throwsIllegalArgument() {
+ final byte[] activeDatasetTlvs = DEFAULT_ACTIVE_DATASET.toThreadTlvs();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PendingOperationalDataset.fromThreadTlvs(activeDatasetTlvs));
+ }
+
+ @Test
+ public void fromThreadTlvs_malformedTlvs_throwsIllegalArgument() {
+ final byte[] invalidTlvs = new byte[] {0x00};
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PendingOperationalDataset.fromThreadTlvs(invalidTlvs));
+ }
+
+ @Test
+ public void toThreadTlvs_conversionIsLossLess() {
+ PendingOperationalDataset dataset1 =
+ new PendingOperationalDataset(
+ DEFAULT_ACTIVE_DATASET,
+ new OperationalDatasetTimestamp(31536000, 200, false),
+ Duration.ofHours(100));
+
+ PendingOperationalDataset dataset2 =
+ PendingOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+ assertThat(dataset2).isEqualTo(dataset1);
+ }
+}
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
new file mode 100644
index 0000000..3a087c7
--- /dev/null
+++ b/thread/tests/unit/Android.bp
@@ -0,0 +1,50 @@
+//
+// Copyright (C) 2023 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "ThreadNetworkUnitTests",
+ min_sdk_version: "33",
+ sdk_version: "module_current",
+ manifest: "AndroidManifest.xml",
+ test_config: "AndroidTest.xml",
+ srcs: [
+ "src/**/*.java",
+ ],
+ test_suites: [
+ "general-tests",
+ ],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "compatibility-device-util-axt",
+ "ctstestrunner-axt",
+ "framework-connectivity-pre-jarjar",
+ "framework-connectivity-t-pre-jarjar",
+ "guava-android-testlib",
+ "net-tests-utils",
+ "truth",
+ ],
+ libs: [
+ "android.test.base",
+ "android.test.runner",
+ ],
+ // Test coverage system runs on different devices. Need to
+ // compile for all architectures.
+ compile_multilib: "both",
+}
diff --git a/thread/tests/unit/AndroidManifest.xml b/thread/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..ace7c52
--- /dev/null
+++ b/thread/tests/unit/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 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.
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.net.thread.unittests">
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.net.thread.unittests"
+ android:label="Unit tests for android.net.thread" />
+</manifest>
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..663ff74
--- /dev/null
+++ b/thread/tests/unit/AndroidTest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 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.
+ -->
+
+<configuration description="Config for Thread network unit test cases">
+ <option name="test-tag" value="ThreadNetworkUnitTests" />
+ <option name="test-suite-tag" value="apct" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
+ <option name="check-min-sdk" value="true" />
+ <option name="cleanup-apks" value="true" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.net.thread.unittests" />
+ <!-- Ignores tests introduced by guava-android-testlib -->
+ <option name="exclude-annotation" value="org.junit.Ignore"/>
+ </test>
+</configuration>
diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
new file mode 100644
index 0000000..78eb3d0
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset.Builder;
+import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+/** Unit tests for {@link ActiveOperationalDataset}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActiveOperationalDatasetTest {
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ private static final byte[] VALID_DATASET =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+
+ @Mock private Random mockRandom;
+ @Mock private SecureRandom mockSecureRandom;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ private static byte[] addTlv(byte[] dataset, String tlvHex) {
+ return Bytes.concat(dataset, base16().decode(tlvHex));
+ }
+
+ @Test
+ public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
+ byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET, "AA01FFBB020102");
+
+ ActiveOperationalDataset dataset1 =
+ ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
+ ActiveOperationalDataset dataset2 =
+ ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+ SparseArray<byte[]> unknownTlvs = dataset2.getUnknownTlvs();
+ assertThat(unknownTlvs.size()).isEqualTo(2);
+ assertThat(unknownTlvs.get(0xAA)).isEqualTo(new byte[] {(byte) 0xFF});
+ assertThat(unknownTlvs.get(0xBB)).isEqualTo(new byte[] {0x01, 0x02});
+ assertThat(dataset2).isEqualTo(dataset1);
+ }
+
+ @Test
+ public void createRandomDataset_fieldsAreRandomized() {
+ // Always return the max bounded value
+ doAnswer(invocation -> (int) invocation.getArgument(0) - 1)
+ .when(mockRandom)
+ .nextInt(anyInt());
+ doAnswer(
+ invocation -> {
+ byte[] output = invocation.getArgument(0);
+ for (int i = 0; i < output.length; ++i) {
+ output[i] = (byte) (i + 10);
+ }
+ return null;
+ })
+ .when(mockRandom)
+ .nextBytes(any(byte[].class));
+ doAnswer(
+ invocation -> {
+ byte[] output = invocation.getArgument(0);
+ for (int i = 0; i < output.length; ++i) {
+ output[i] = (byte) (i + 30);
+ }
+ return null;
+ })
+ .when(mockSecureRandom)
+ .nextBytes(any(byte[].class));
+
+ ActiveOperationalDataset dataset =
+ ActiveOperationalDataset.createRandomDataset(mockRandom, mockSecureRandom);
+
+ assertThat(dataset.getActiveTimestamp())
+ .isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
+ assertThat(dataset.getExtendedPanId())
+ .isEqualTo(new byte[] {10, 11, 12, 13, 14, 15, 16, 17});
+ assertThat(dataset.getMeshLocalPrefix())
+ .isEqualTo(new IpPrefix("fd0b:0c0d:0e0f:1011::/64"));
+ verify(mockRandom, times(2)).nextBytes(any(byte[].class));
+ assertThat(dataset.getPanId()).isEqualTo(0xfffe); // PAN ID <= 0xfffe
+ verify(mockRandom, times(1)).nextInt(eq(0xffff));
+ assertThat(dataset.getChannel()).isEqualTo(26);
+ verify(mockRandom, times(1)).nextInt(eq(16));
+ assertThat(dataset.getChannelPage()).isEqualTo(0);
+ assertThat(dataset.getChannelMask().size()).isEqualTo(1);
+ assertThat(dataset.getPskc())
+ .isEqualTo(
+ new byte[] {
+ 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45
+ });
+ assertThat(dataset.getNetworkKey())
+ .isEqualTo(
+ new byte[] {
+ 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45
+ });
+ verify(mockSecureRandom, times(2)).nextBytes(any(byte[].class));
+ assertThat(dataset.getSecurityPolicy())
+ .isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}));
+ }
+
+ @Test
+ public void builder_buildWithTooLongTlvs_throwsIllegalState() {
+ Builder builder = new Builder(ActiveOperationalDataset.createRandomDataset());
+ for (int i = 0; i < 10; i++) {
+ builder.addUnknownTlv(i, new byte[20]);
+ }
+
+ assertThrows(IllegalStateException.class, () -> new Builder().build());
+ }
+
+ @Test
+ public void builder_setUnknownTlvs_success() {
+ ActiveOperationalDataset dataset1 = ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET);
+ SparseArray<byte[]> unknownTlvs = new SparseArray<>(2);
+ unknownTlvs.put(0x33, new byte[] {1, 2, 3});
+ unknownTlvs.put(0x44, new byte[] {1, 2, 3, 4});
+
+ ActiveOperationalDataset dataset2 =
+ new ActiveOperationalDataset.Builder(dataset1).setUnknownTlvs(unknownTlvs).build();
+
+ assertThat(dataset1.getUnknownTlvs().size()).isEqualTo(0);
+ assertThat(dataset2.getUnknownTlvs().size()).isEqualTo(2);
+ assertThat(dataset2.getUnknownTlvs().get(0x33)).isEqualTo(new byte[] {1, 2, 3});
+ assertThat(dataset2.getUnknownTlvs().get(0x44)).isEqualTo(new byte[] {1, 2, 3, 4});
+ }
+
+ @Test
+ public void securityPolicy_fromTooShortTlvValue_throwsIllegalArgument() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> SecurityPolicy.fromTlvValue(new byte[] {0x01}));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> SecurityPolicy.fromTlvValue(new byte[] {0x01, 0x02}));
+ }
+
+ @Test
+ public void securityPolicy_toTlvValue_conversionIsLossLess() {
+ SecurityPolicy policy1 = new SecurityPolicy(200, new byte[] {(byte) 0xFF, (byte) 0xF8});
+
+ SecurityPolicy policy2 = SecurityPolicy.fromTlvValue(policy1.toTlvValue());
+
+ assertThat(policy2).isEqualTo(policy1);
+ }
+}
diff --git a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
new file mode 100644
index 0000000..32063fc
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link OperationalDatasetTimestamp}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class OperationalDatasetTimestampTest {
+ @Test
+ public void fromTlvValue_invalidTimestamp_throwsIllegalArguments() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> OperationalDatasetTimestamp.fromTlvValue(new byte[7]));
+ }
+
+ @Test
+ public void fromTlvValue_goodValue_success() {
+ OperationalDatasetTimestamp timestamp =
+ OperationalDatasetTimestamp.fromTlvValue(base16().decode("FFEEDDCCBBAA9989"));
+
+ assertThat(timestamp.getSeconds()).isEqualTo(0xFFEEDDCCBBAAL);
+ // 0x9989 is 0x4CC4 << 1 + 1
+ assertThat(timestamp.getTicks()).isEqualTo(0x4CC4);
+ assertThat(timestamp.isAuthoritativeSource()).isTrue();
+ }
+
+ @Test
+ public void toTlvValue_conversionIsLossLess() {
+ OperationalDatasetTimestamp timestamp1 = new OperationalDatasetTimestamp(100L, 10, true);
+
+ OperationalDatasetTimestamp timestamp2 =
+ OperationalDatasetTimestamp.fromTlvValue(timestamp1.toTlvValue());
+
+ assertThat(timestamp2).isEqualTo(timestamp1);
+ }
+}