Snap for 12338107 from fe4b4da630119823c375338fe869515f0b2dde95 to mainline-cellbroadcast-release Change-Id: I929a05d13f9aa107a3c726cdb3491f0c275101c6
diff --git a/src/android/net/ip/ConnectivityPacketTracker.java b/src/android/net/ip/ConnectivityPacketTracker.java index c071a1b..ce4f6ae 100644 --- a/src/android/net/ip/ConnectivityPacketTracker.java +++ b/src/android/net/ip/ConnectivityPacketTracker.java
@@ -111,7 +111,6 @@ } } - public static final int MAX_CAPTURE_TIME_MS = 300000; private static final String TAG = ConnectivityPacketTracker.class.getSimpleName(); private static final boolean DBG = false; private static final String MARK_START = "--- START ---"; @@ -148,11 +147,6 @@ * @param isCapture {@code true} to enable capture, {@code false} to disable capture */ public void setCapture(boolean isCapture) { - // prevent configure the duplicated events - if (mCapturing == isCapture) { - return; - } - mCapturing = isCapture; if (!isCapture) { mPacketCache.evictAll(); @@ -185,12 +179,8 @@ mDisplayName = null; } - public boolean isCapturing() { - return mCapturing; - } - @VisibleForTesting(visibility = PRIVATE) - public int getTotalCapturePacketCount() { + public int getCapturePacketTypeCount() { return mPacketCache.size(); }
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java index 8d60804..d0dda68 100644 --- a/src/android/net/ip/IpClient.java +++ b/src/android/net/ip/IpClient.java
@@ -16,6 +16,10 @@ package android.net.ip; +import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM; +import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_CONFIRM; +import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC; +import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED; import static android.net.RouteInfo.RTN_UNICAST; import static android.net.RouteInfo.RTN_UNREACHABLE; import static android.net.dhcp.DhcpResultsParcelableUtil.toStableParcelable; @@ -26,7 +30,6 @@ import static android.net.ip.IIpClientCallbacks.DTIM_MULTIPLIER_RESET; import static android.net.ip.IpClient.IpClientCommands.CMD_ADDRESSES_CLEARED; import static android.net.ip.IpClient.IpClientCommands.CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF; -import static android.net.ip.IpClient.IpClientCommands.CMD_APF_STOP_CAPTURE; import static android.net.ip.IpClient.IpClientCommands.CMD_COMPLETE_PRECONNECTION; import static android.net.ip.IpClient.IpClientCommands.CMD_CONFIRM; import static android.net.ip.IpClient.IpClientCommands.CMD_JUMP_RUNNING_TO_STOPPING; @@ -46,6 +49,9 @@ import static android.net.ip.IpClient.IpClientCommands.EVENT_DHCPACTION_TIMEOUT; import static android.net.ip.IpClient.IpClientCommands.EVENT_IPV6_AUTOCONF_TIMEOUT; import static android.net.ip.IpClient.IpClientCommands.EVENT_NETLINK_LINKPROPERTIES_CHANGED; +import static android.net.ip.IpClient.IpClientCommands.EVENT_NUD_FAILURE_QUERY_FAILURE; +import static android.net.ip.IpClient.IpClientCommands.EVENT_NUD_FAILURE_QUERY_SUCCESS; +import static android.net.ip.IpClient.IpClientCommands.EVENT_NUD_FAILURE_QUERY_TIMEOUT; import static android.net.ip.IpClient.IpClientCommands.EVENT_PRE_DHCP_ACTION_COMPLETE; import static android.net.ip.IpClient.IpClientCommands.EVENT_PROVISIONING_TIMEOUT; import static android.net.ip.IpClient.IpClientCommands.EVENT_READ_PACKET_FILTER_COMPLETE; @@ -82,6 +88,7 @@ import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION; +import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION; import static com.android.networkstack.util.NetworkStackUtils.createInet6AddressFromEui64; import static com.android.networkstack.util.NetworkStackUtils.macAddressToEui64; import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission; @@ -115,6 +122,8 @@ import android.net.dhcp.DhcpClient; import android.net.dhcp.DhcpPacket; import android.net.dhcp6.Dhcp6Client; +import android.net.ipmemorystore.OnNetworkEventCountRetrievedListener; +import android.net.ipmemorystore.Status; import android.net.metrics.IpConnectivityLog; import android.net.metrics.IpManagerEvent; import android.net.networkstack.aidl.dhcp.DhcpOption; @@ -598,8 +607,9 @@ static final int CMD_UPDATE_APF_CAPABILITIES = 19; static final int EVENT_IPV6_AUTOCONF_TIMEOUT = 20; static final int CMD_UPDATE_APF_DATA_SNAPSHOT = 21; - // Triggered by ApfShellCommand - static final int CMD_APF_STOP_CAPTURE = 22; + static final int EVENT_NUD_FAILURE_QUERY_TIMEOUT = 22; + static final int EVENT_NUD_FAILURE_QUERY_SUCCESS = 23; + static final int EVENT_NUD_FAILURE_QUERY_FAILURE = 24; // Internal commands to use instead of trying to call transitionTo() inside // a given State's enter() method. Calling transitionTo() from enter/exit // encounters a Log.wtf() that can cause trouble on eng builds. @@ -686,6 +696,24 @@ static final String CONFIG_IPV6_AUTOCONF_TIMEOUT = "ipclient_ipv6_autoconf_timeout"; private static final int DEFAULT_IPV6_AUTOCONF_TIMEOUT_MS = 5000; + private static final int IPMEMORYSTORE_TIMEOUT_MS = 1000; + @VisibleForTesting + static final long SIX_HOURS_IN_MS = 6 * 3600 * 1000L; + @VisibleForTesting + public static final long ONE_DAY_IN_MS = 4 * SIX_HOURS_IN_MS; + @VisibleForTesting + public static final long ONE_WEEK_IN_MS = 7 * ONE_DAY_IN_MS; + @VisibleForTesting + static final String CONFIG_NUD_FAILURE_COUNT_DAILY_THRESHOLD = + "nud_failure_count_daily_threshold"; + @VisibleForTesting + static final int DEFAULT_NUD_FAILURE_COUNT_DAILY_THRESHOLD = 10; + @VisibleForTesting + static final String CONFIG_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD = + "nud_failure_count_weekly_threshold"; + @VisibleForTesting + static final int DEFAULT_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD = 20; + private static final boolean NO_CALLBACKS = false; private static final boolean SEND_CALLBACKS = true; @@ -725,12 +753,24 @@ "KT GiGA WiFi", "marente" )); + // The NUD failure event types to query. Although only NETWORK_EVENT_NUD_FAILURE_ORGANIC event + // is stored in the database currently, this array is still maintained to include other event + // types for testing and future expansion. + @VisibleForTesting + public static final int[] NETWORK_EVENT_NUD_FAILURE_TYPES = new int[] { + NETWORK_EVENT_NUD_FAILURE_ROAM, + NETWORK_EVENT_NUD_FAILURE_CONFIRM, + NETWORK_EVENT_NUD_FAILURE_ORGANIC, + NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED + }; + private final State mStoppedState = new StoppedState(); private final State mStoppingState = new StoppingState(); private final State mClearingIpAddressesState = new ClearingIpAddressesState(); private final State mStartedState = new StartedState(); private final State mRunningState = new RunningState(); private final State mPreconnectingState = new PreconnectingState(); + private final State mNudFailureQueryState = new NudFailureQueryState(); private final String mTag; private final Context mContext; @@ -767,6 +807,9 @@ // Polling interval to update APF data snapshot private final long mApfCounterPollingIntervalMs; + private final int mNudFailureCountDailyThreshold; + private final int mNudFailureCountWeeklyThreshold; + // Experiment flag read from device config. private final boolean mDhcp6PrefixDelegationEnabled; private final boolean mUseNewApfFilter; @@ -776,6 +819,7 @@ private final boolean mApfShouldHandleArpOffload; private final boolean mApfShouldHandleNdOffload; private final boolean mApfShouldHandleMdnsOffload; + private final boolean mIgnoreNudFailureEnabled; private InterfaceParams mInterfaceParams; @@ -791,7 +835,6 @@ private String mTcpBufferSizes; private ProxyInfo mHttpProxy; private AndroidPacketFilter mApfFilter; - private ConnectivityPacketTracker mPacketTracker; private String mL2Key; // The L2 key for this network, for writing into the memory store private String mCluster; // The cluster for this network, for writing into the memory store private int mCreatorUid; // Uid of app creating the wifi configuration @@ -804,6 +847,15 @@ private int mMaxDtimMultiplier = DTIM_MULTIPLIER_RESET; private ApfCapabilities mCurrentApfCapabilities; private WakeupMessage mIpv6AutoconfTimeoutAlarm = null; + private boolean mIgnoreNudFailure; + // An array of NUD failure event count associated with the query database since the timestamps + // in the past, and is always initialized to null in StoppedState. Currently supported array + // elements are as follows: + // element 0: failures in the past week + // element 1: failures in the past day + // element 2: failures in the past 6h + @Nullable + private int[] mNudFailureEventCounts = null; /** * Reading the snapshot is an asynchronous operation initiated by invoking @@ -1032,6 +1084,14 @@ mApfShouldHandleMdnsOffload = false; mPopulateLinkAddressLifetime = mDependencies.isFeatureEnabled(context, IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION); + mIgnoreNudFailureEnabled = mDependencies.isFeatureEnabled(mContext, + IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION); + mNudFailureCountDailyThreshold = mDependencies.getDeviceConfigPropertyInt( + CONFIG_NUD_FAILURE_COUNT_DAILY_THRESHOLD, + DEFAULT_NUD_FAILURE_COUNT_DAILY_THRESHOLD); + mNudFailureCountWeeklyThreshold = mDependencies.getDeviceConfigPropertyInt( + CONFIG_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD, + DEFAULT_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD); IpClientLinkObserver.Configuration config = new IpClientLinkObserver.Configuration( mMinRdnssLifetimeSec, mPopulateLinkAddressLifetime); @@ -1212,6 +1272,7 @@ addState(mStoppedState); addState(mStartedState); addState(mPreconnectingState, mStartedState); + addState(mNudFailureQueryState, mStartedState); addState(mClearingIpAddressesState, mStartedState); addState(mRunningState, mStartedState); addState(mStoppingState); @@ -1531,8 +1592,8 @@ } final CompletableFuture<String> result = new CompletableFuture<>(); - final Handler handler = getHandler(); - handler.post(() -> { + + getHandler().post(() -> { try { if (mApfFilter == null) { // IpClient has either stopped or the interface does not support APF. @@ -1570,42 +1631,6 @@ Objects.requireNonNull(snapshot, "No data snapshot recorded."); result.complete(snapshot); break; - case "start-capture": - if (mPacketTracker.isCapturing()) { - result.complete("PacketTracker is already capturing"); - } else { - // remove scheduled stop event if it already in the queue - if (handler.hasMessages(IpClientCommands.CMD_APF_STOP_CAPTURE)) { - handler.removeMessages(IpClientCommands.CMD_APF_STOP_CAPTURE); - } - - mPacketTracker.setCapture(true); - // capture up to 300 sec and stop capturing - sendMessageDelayed( - IpClientCommands.CMD_APF_STOP_CAPTURE, - ConnectivityPacketTracker.MAX_CAPTURE_TIME_MS - ); - result.complete("success"); - } - break; - case "matched-packet-counts": - Objects.requireNonNull(optarg, "No packet pattern provided"); - int pktCnt = mPacketTracker.getMatchedPacketCount(optarg); - result.complete(String.valueOf(pktCnt)); - break; - case "stop-capture": - if (!mPacketTracker.isCapturing()) { - result.complete("PacketTracker already stop capturing"); - } else { - // remove scheduled stop event if it already in the queue - if (handler.hasMessages(IpClientCommands.CMD_APF_STOP_CAPTURE)) { - handler.removeMessages(IpClientCommands.CMD_APF_STOP_CAPTURE); - } - - sendMessage(IpClientCommands.CMD_APF_STOP_CAPTURE); - result.complete("success"); - } - break; default: throw new IllegalArgumentException("Invalid apf command: " + cmd); } @@ -2516,6 +2541,39 @@ return true; } + // In order to avoid overflowing the database (the maximum is 10MB) in case of a NUD failure + // happens frequently (e.g, every 30s in a broken network), we stop writing the NUD failure + // event to database if the event count in past 6h has exceeded the daily threshold. + private boolean shouldStopWritingNudFailureEventToDatabase() { + // NUD failure query has not completed yet. + if (mNudFailureEventCounts == null) return true; + return mNudFailureEventCounts[2] >= mNudFailureCountDailyThreshold; + } + + private void maybeStoreNudFailureToDatabase(final NudEventType type) { + if (!mIgnoreNudFailureEnabled) return; + final int event = IpReachabilityMonitor.nudEventTypeToNetworkEvent(type); + // So far only NUD failure events due to organic kernel check are stored, which can be + // expanded to other causes later if necessary. + if (event != NETWORK_EVENT_NUD_FAILURE_ORGANIC) return; + if (shouldStopWritingNudFailureEventToDatabase()) return; + + final long now = System.currentTimeMillis(); + final long expiry = now + ONE_WEEK_IN_MS; + mIpMemoryStore.storeNetworkEvent(mCluster, now, expiry, event, + status -> { + if (!status.isSuccess()) { + Log.e(TAG, "Failed to store NUD failure event"); + } + }); + if (DBG) { + Log.d(TAG, "store network event " + type + + " at " + now + + " expire at " + expiry + + " with cluster " + mCluster); + } + } + private boolean startIpReachabilityMonitor() { try { mIpReachabilityMonitor = mDependencies.getIpReachabilityMonitor( @@ -2526,6 +2584,8 @@ new IpReachabilityMonitor.Callback() { @Override public void notifyLost(String logMsg, NudEventType type) { + maybeStoreNudFailureToDatabase(type); + if (mIgnoreNudFailure) return; final int version = mCallback.getInterfaceVersion(); if (version >= VERSION_ADDED_REACHABILITY_FAILURE) { final int reason = nudEventTypeToInt(type); @@ -2740,6 +2800,7 @@ mGratuitousNaTargetAddresses.clear(); mMulticastNsSourceAddresses.clear(); mDelegatedPrefixes.clear(); + mNudFailureEventCounts = null; resetLinkProperties(); if (mStartTimeMillis > 0) { @@ -2764,7 +2825,9 @@ case CMD_START: mConfiguration = (android.net.shared.ProvisioningConfiguration) msg.obj; - transitionTo(mClearingIpAddressesState); + transitionTo(mIgnoreNudFailureEnabled + ? mNudFailureQueryState + : mClearingIpAddressesState); break; case EVENT_NETLINK_LINKPROPERTIES_CHANGED: @@ -3191,7 +3254,82 @@ return mConfiguration.mIPv4ProvisioningMode != PROV_IPV4_DISABLED; } + private boolean shouldIgnoreNudFailure(@NonNull final int[] eventCounts) { + if (!mIgnoreNudFailureEnabled) return false; + if (eventCounts.length == 0) return false; + + final int countInPastOneWeek = eventCounts[0]; + final int countInPastOneDay = eventCounts[1]; + return countInPastOneDay >= mNudFailureCountDailyThreshold + || countInPastOneWeek >= mNudFailureCountWeeklyThreshold; + } + + class NudFailureQueryState extends State { + // This listener runs in a different thread (the Executor used in the IpMemoryStoreService) + // and it needs to be volatile to allow access by other threads than the IpClient state + // machine handler, which should be fine since it only accesses the mListener and calls + // sendMessage. + private volatile OnNetworkEventCountRetrievedListener mListener = + new OnNetworkEventCountRetrievedListener() { + @Override + public void onNetworkEventCountRetrieved(Status status, int[] counts) { + if (mListener != this) return; + if (counts.length == 0) { + if (!status.isSuccess()) { + Log.e(TAG, "Error retrieving NUD failure event count: " + status); + } + sendMessage(EVENT_NUD_FAILURE_QUERY_FAILURE); + return; + } + sendMessage(EVENT_NUD_FAILURE_QUERY_SUCCESS, counts); + }}; + + @Override + public void enter() { + super.enter(); + // Set a timeout for retrieving NUD failure event counts. + sendMessageDelayed(EVENT_NUD_FAILURE_QUERY_TIMEOUT, IPMEMORYSTORE_TIMEOUT_MS); + final long now = System.currentTimeMillis(); + final long[] sinceTimes = new long[3]; + sinceTimes[0] = now - ONE_WEEK_IN_MS; + sinceTimes[1] = now - ONE_DAY_IN_MS; + sinceTimes[2] = now - SIX_HOURS_IN_MS; + mIpMemoryStore.retrieveNetworkEventCount(mCluster, sinceTimes, + NETWORK_EVENT_NUD_FAILURE_TYPES, mListener); + } + + @Override + public boolean processMessage(Message message) { + switch (message.what) { + case EVENT_NUD_FAILURE_QUERY_FAILURE: + case EVENT_NUD_FAILURE_QUERY_TIMEOUT: + // TODO: log query result with metrics. + transitionTo(mClearingIpAddressesState); + return HANDLED; + + case EVENT_NUD_FAILURE_QUERY_SUCCESS: + mNudFailureEventCounts = (int[]) message.obj; + mIgnoreNudFailure = shouldIgnoreNudFailure(mNudFailureEventCounts); + transitionTo(mClearingIpAddressesState); + return HANDLED; + + default: + deferMessage(message); // e.g. LP updated during this state. + return HANDLED; + } + } + + @Override + public void exit() { + super.exit(); + removeMessages(EVENT_NUD_FAILURE_QUERY_FAILURE); + removeMessages(EVENT_NUD_FAILURE_QUERY_TIMEOUT); + removeMessages(EVENT_NUD_FAILURE_QUERY_SUCCESS); + } + } + class RunningState extends State { + private ConnectivityPacketTracker mPacketTracker; private boolean mDhcpActionInFlight; @Override @@ -3676,9 +3814,7 @@ mCallback.startReadPacketFilter("polling"); sendMessageDelayed(CMD_UPDATE_APF_DATA_SNAPSHOT, mApfCounterPollingIntervalMs); break; - case CMD_APF_STOP_CAPTURE: - mPacketTracker.setCapture(false); - break; + default: return NOT_HANDLED; }
diff --git a/src/com/android/networkstack/ipmemorystore/IpMemoryStoreDatabase.java b/src/com/android/networkstack/ipmemorystore/IpMemoryStoreDatabase.java index 00026a0..b00c03d 100644 --- a/src/com/android/networkstack/ipmemorystore/IpMemoryStoreDatabase.java +++ b/src/com/android/networkstack/ipmemorystore/IpMemoryStoreDatabase.java
@@ -39,6 +39,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; @@ -146,6 +147,42 @@ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLENAME; } + /** + * Contract class for the network events table. + */ + public static final class NetworkEventsContract { + private NetworkEventsContract() {} + + public static final String TABLENAME = "NetworkEvents"; + + public static final String COLNAME_CLUSTER = "cluster"; + public static final String COLTYPE_CLUSTER = "TEXT NOT NULL"; + + public static final String COLNAME_TIMESTAMP = "timestamp"; + public static final String COLTYPE_TIMESTAMP = "BIGINT"; + + public static final String COLNAME_EVENTTYPE = "eventType"; + public static final String COLTYPE_EVENTTYPE = "INTEGER"; + + public static final String COLNAME_EXPIRY = "expiry"; + // Milliseconds since the Epoch, in true Java style + public static final String COLTYPE_EXPIRY = "BIGINT"; + + public static final String CREATE_TABLE = "CREATE TABLE IF NOT EXISTS " + + TABLENAME + " (" + + COLNAME_CLUSTER + " " + COLTYPE_CLUSTER + ", " + + COLNAME_TIMESTAMP + " " + COLTYPE_TIMESTAMP + ", " + + COLNAME_EVENTTYPE + " " + COLTYPE_EVENTTYPE + ", " + + COLNAME_EXPIRY + " " + COLTYPE_EXPIRY + ")"; + public static final String INDEX_NAME = "idx_" + COLNAME_CLUSTER + "_" + COLNAME_TIMESTAMP + + "_" + COLNAME_EVENTTYPE; + public static final String CREATE_INDEX = "CREATE INDEX IF NOT EXISTS " + INDEX_NAME + + " ON " + TABLENAME + + " (" + COLNAME_CLUSTER + ", " + COLNAME_TIMESTAMP + ", " + COLNAME_EVENTTYPE + + ")"; + public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLENAME; + } + // To save memory when the DB is not used, close it after 30s of inactivity. This is // determined manually based on what feels right. private static final long IDLE_CONNECTION_TIMEOUT_MS = 30_000; @@ -153,12 +190,11 @@ /** The SQLite DB helper */ public static class DbHelper extends SQLiteOpenHelper { // Update this whenever changing the schema. - // DO NOT CHANGE without solid testing for downgrades, and checking onDowngrade - // below: b/171340630 @VisibleForTesting - static final int SCHEMA_VERSION = 4; - private static final String DATABASE_FILENAME = "IpMemoryStore.db"; + static final int SCHEMA_VERSION = 5; + private static final String DATABASE_FILENAME = "IpMemoryStoreV2.db"; private static final String TRIGGER_NAME = "delete_cascade_to_private"; + private static final String LEGACY_DATABASE_FILENAME = "IpMemoryStore.db"; public DbHelper(@NonNull final Context context) { super(context, DATABASE_FILENAME, null, SCHEMA_VERSION); @@ -176,6 +212,8 @@ public void onCreate(@NonNull final SQLiteDatabase db) { db.execSQL(NetworkAttributesContract.CREATE_TABLE); db.execSQL(PrivateDataContract.CREATE_TABLE); + db.execSQL(NetworkEventsContract.CREATE_TABLE); + db.execSQL(NetworkEventsContract.CREATE_INDEX); createTrigger(db); } @@ -201,11 +239,20 @@ if (oldVersion < 4) { createTrigger(db); } + + if (oldVersion < 5) { + // upgrade from version 4 to version 5, the NetworkEventsTable doesn't exist + // on previous version and onCreate won't be called during upgrade, therefore, + // create the table manually. + db.execSQL(NetworkEventsContract.CREATE_TABLE); + db.execSQL(NetworkEventsContract.CREATE_INDEX); + } } catch (SQLiteException e) { Log.e(TAG, "Could not upgrade to the new version", e); // create database with new version db.execSQL(NetworkAttributesContract.DROP_TABLE); db.execSQL(PrivateDataContract.DROP_TABLE); + db.execSQL(NetworkEventsContract.DROP_TABLE); onCreate(db); } } @@ -217,6 +264,7 @@ // Downgrades always nuke all data and recreate an empty table. db.execSQL(NetworkAttributesContract.DROP_TABLE); db.execSQL(PrivateDataContract.DROP_TABLE); + db.execSQL(NetworkEventsContract.DROP_TABLE); onCreate(db); } @@ -230,6 +278,33 @@ + "; END;"; db.execSQL(createTrigger); } + + /** + * Renames the database file to prevent crashes during downgrades. + * <p> + * Previous versions (before 5) has a bug(b/171340630) that would cause a crash when + * onDowngrade is triggered. We cannot just bump the schema version without + * renaming the database filename, because only bumping the schema version still causes + * crash when downgrading to an older version. + * <p> + * After rename the db file, if the module is rolled back, the legacy file is not present. + * The code will create a new legacy database, and will trigger onCreate path. The new + * database will continue to exist, but the legacy code does not know about it. + * <p> + * In later stage, if the module is rolled forward again, the legacy database will overwrite + * the new database, the user's data will be preserved. + */ + public static void maybeRenameDatabaseFile(Context context) { + final File legacyDb = context.getDatabasePath(LEGACY_DATABASE_FILENAME); + if (legacyDb.exists()) { + final File newDb = context.getDatabasePath(DATABASE_FILENAME); + final boolean result = legacyDb.renameTo(newDb); + if (!result) { + Log.w(TAG, "failed to rename the IP Memory store database to " + + DATABASE_FILENAME); + } + } + } } @NonNull @@ -308,6 +383,22 @@ return values; } + /** + * Convert a network event (including cluster, timestamp of when it happened, expiry and + * event type) into content values to store them in a table compliant with the ontract defined + * in NetworkEventsContract. + */ + @NonNull + private static ContentValues toContentValues(@NonNull final String cluster, + final long timestamp, final long expiry, final int eventType) { + final ContentValues values = new ContentValues(); + values.put(NetworkEventsContract.COLNAME_CLUSTER, cluster); + values.put(NetworkEventsContract.COLNAME_TIMESTAMP, timestamp); + values.put(NetworkEventsContract.COLNAME_EVENTTYPE, eventType); + values.put(NetworkEventsContract.COLNAME_EXPIRY, expiry); + return values; + } + @Nullable private static NetworkAttributes readNetworkAttributesLine(@NonNull final Cursor cursor) { // Make sure the data hasn't expired @@ -459,6 +550,7 @@ try { db.delete(NetworkAttributesContract.TABLENAME, null, null); db.delete(PrivateDataContract.TABLENAME, null, null); + db.delete(NetworkEventsContract.TABLENAME, null, null); try (Cursor cursorNetworkAttributes = db.query( // table name NetworkAttributesContract.TABLENAME, @@ -485,6 +577,19 @@ "1")) { // limit if (0 != cursorPrivateData.getCount()) continue; } + try (Cursor cursorNetworkEvents = db.query( + // table name + NetworkEventsContract.TABLENAME, + // column name + new String[] { NetworkEventsContract.COLNAME_CLUSTER }, + null, // selection + null, // selectionArgs + null, // groupBy + null, // having + null, // orderBy + "1")) { // limit + if (0 != cursorNetworkEvents.getCount()) continue; + } db.setTransactionSuccessful(); } catch (SQLiteException e) { Log.e(TAG, "Could not wipe the data in database", e); @@ -669,10 +774,15 @@ static int dropAllExpiredRecords(@NonNull final SQLiteDatabase db) { db.beginTransaction(); try { + final long currentTimestamp = System.currentTimeMillis(); // Deletes NetworkAttributes that have expired. db.delete(NetworkAttributesContract.TABLENAME, NetworkAttributesContract.COLNAME_EXPIRYDATE + " < ?", - new String[]{Long.toString(System.currentTimeMillis())}); + new String[]{Long.toString(currentTimestamp)}); + // Deletes NetworkEvents that have expired. + db.delete(NetworkEventsContract.TABLENAME, + NetworkEventsContract.COLNAME_EXPIRY + " < ?", + new String[]{Long.toString(currentTimestamp)}); db.setTransactionSuccessful(); } catch (SQLiteException e) { Log.e(TAG, "Could not delete data from memory store", e); @@ -755,15 +865,62 @@ static int storeNetworkEvent(@NonNull final SQLiteDatabase db, @NonNull final String cluster, final long timestamp, final long expiry, final int eventType) { - // TODO: implement this. - return Status.SUCCESS; + final ContentValues cv = toContentValues(cluster, timestamp, expiry, eventType); + db.beginTransaction(); + try { + final long resultId = db.insertOrThrow(NetworkEventsContract.TABLENAME, + null /* nullColumnHack */, cv); + if (resultId < 0) { + // Should not fail to insert a row to NetworkEvents table which doesn't have + // uniqueness constraint. + return Status.ERROR_STORAGE; + } + db.setTransactionSuccessful(); + return Status.SUCCESS; + } catch (SQLiteException e) { + // No space left on disk or something + Log.e(TAG, "Could not write to the memory store", e); + } finally { + db.endTransaction(); + } + return Status.ERROR_STORAGE; } static int[] retrieveNetworkEventCount(@NonNull final SQLiteDatabase db, @NonNull final String cluster, @NonNull final long[] sinceTimes, @NonNull final int[] eventTypes) { - // TODO: implement this. - return new int[0]; + final int[] counts = new int[sinceTimes.length]; + for (int i = 0; i < counts.length; i++) { + final String[] selectionArgs = new String[eventTypes.length + 2]; + selectionArgs[0] = cluster; + selectionArgs[1] = String.valueOf(sinceTimes[i]); + for (int j = 0; j < eventTypes.length; j++) { + selectionArgs[j + 2] = String.valueOf(eventTypes[j]); + } + final StringBuilder selectionBuilder = + new StringBuilder(NetworkEventsContract.COLNAME_CLUSTER + " = ? " + "AND " + + NetworkEventsContract.COLNAME_TIMESTAMP + " >= ? " + "AND " + + NetworkEventsContract.COLNAME_EVENTTYPE + " IN ("); + for (int k = 0; k < eventTypes.length; k++) { + selectionBuilder.append("?"); + if (k < eventTypes.length - 1) { + selectionBuilder.append(","); + } + } + selectionBuilder.append(")"); + try (Cursor cursor = db.query( + NetworkEventsContract.TABLENAME, + new String[] {"COUNT(*)"}, // columns + selectionBuilder.toString(), + selectionArgs, + null, // groupBy + null, // having + null)) { // orderBy + cursor.moveToFirst(); + counts[i] = cursor.getInt(0); + } + } + return counts; } // Helper methods
diff --git a/src/com/android/networkstack/ipmemorystore/IpMemoryStoreService.java b/src/com/android/networkstack/ipmemorystore/IpMemoryStoreService.java index a05c5cb..de06b64 100644 --- a/src/com/android/networkstack/ipmemorystore/IpMemoryStoreService.java +++ b/src/com/android/networkstack/ipmemorystore/IpMemoryStoreService.java
@@ -87,6 +87,9 @@ * @param context the context to access storage with. */ public IpMemoryStoreService(@NonNull final Context context) { + // Before doing anything at all, rename the legacy database if necessary. + IpMemoryStoreDatabase.DbHelper.maybeRenameDatabaseFile(context); + // Note that constructing the service will access the disk and block // for some time, but it should make no difference to the clients. Because // the interface is one-way, clients fire and forget requests, and the callback @@ -513,6 +516,11 @@ makeStatus(ERROR_ILLEGAL_ARGUMENT), new int[0] /* counts */); return; } + if (0 == sinceTimes.length) { + listener.onNetworkEventCountRetrieved( + makeStatus(ERROR_ILLEGAL_ARGUMENT), new int[0] /* counts */); + return; + } if (null == mDb) { listener.onNetworkEventCountRetrieved( makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED), new int[0] /* counts */);
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java index 0c1eb39..fce06e4 100755 --- a/src/com/android/networkstack/util/NetworkStackUtils.java +++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -280,6 +280,12 @@ */ public static final String DNS_DDR_VERSION = "dns_ddr_version"; + /** + * Experiment flag to ignore all NUD failures if we've seen too many NUD failure in a network. + */ + public static final String IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION = + "ip_reachability_ignore_nud_failure_version"; + /**** BEGIN Feature Kill Switch Flags ****/ /**
diff --git a/src/com/android/server/NetworkStackService.java b/src/com/android/server/NetworkStackService.java index 780642f..4d10c3b 100644 --- a/src/com/android/server/NetworkStackService.java +++ b/src/com/android/server/NetworkStackService.java
@@ -627,14 +627,6 @@ pw.println(" Format: <apfVersion>,<maxProgramSize>,<packetFormat>"); pw.println(" read"); pw.println(" reads and returns the current state of APF memory."); - pw.println(" start-capture"); - pw.println(" start capture packets in the received buffer."); - pw.println(" The capture is up to 300 sec, then it will stop."); - pw.println(" stop-capture"); - pw.println(" stop capture packets and clear the received buffer."); - pw.println(" matched-packet-counts <pkt-hex-string>"); - pw.println(" the <pkt-hex-string> starts from ether header."); - pw.println(" Expect to do full packet match."); } }
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java index 8c81de5..f7e1c4d 100644 --- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java +++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -41,8 +41,15 @@ import static android.net.ip.IpClient.CONFIG_IPV6_AUTOCONF_TIMEOUT; import static android.net.ip.IpClient.CONFIG_ACCEPT_RA_MIN_LFT; import static android.net.ip.IpClient.CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS; +import static android.net.ip.IpClient.CONFIG_NUD_FAILURE_COUNT_DAILY_THRESHOLD; +import static android.net.ip.IpClient.CONFIG_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD; import static android.net.ip.IpClient.DEFAULT_ACCEPT_RA_MIN_LFT; import static android.net.ip.IpClient.DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS; +import static android.net.ip.IpClient.DEFAULT_NUD_FAILURE_COUNT_DAILY_THRESHOLD; +import static android.net.ip.IpClient.DEFAULT_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD; +import static android.net.ip.IpClient.ONE_DAY_IN_MS; +import static android.net.ip.IpClient.ONE_WEEK_IN_MS; +import static android.net.ip.IpClient.SIX_HOURS_IN_MS; import static android.net.ip.IpClientLinkObserver.CLAT_PREFIX; import static android.net.ip.IpClientLinkObserver.CONFIG_SOCKET_RECV_BUFSIZE; import static android.net.ip.IpReachabilityMonitor.NUD_MCAST_RESOLICIT_NUM; @@ -83,6 +90,7 @@ import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION; +import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION; import static com.android.testutils.MiscAsserts.assertThrows; @@ -133,6 +141,7 @@ import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.DhcpResultsParcelable; +import android.net.IIpMemoryStore; import android.net.INetd; import android.net.InetAddresses; import android.net.InterfaceConfigurationParcel; @@ -167,6 +176,7 @@ import android.net.dhcp6.Dhcp6SolicitPacket; import android.net.ipmemorystore.NetworkAttributes; import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener; +import android.net.ipmemorystore.OnNetworkEventCountRetrievedListener; import android.net.ipmemorystore.Status; import android.net.networkstack.TestNetworkStackServiceClient; import android.net.networkstack.aidl.dhcp.DhcpOption; @@ -190,6 +200,7 @@ import android.system.ErrnoException; import android.system.Os; import android.util.Log; +import android.util.Pair; import androidx.annotation.NonNull; import androidx.test.InstrumentationRegistry; @@ -372,6 +383,7 @@ protected IpClient mIpc; protected Dependencies mDependencies; + protected List<Pair<String, Pair<Long, Integer>>> mNetworkEvents = new ArrayList<>(); /***** END signature required test members *****/ @@ -691,6 +703,11 @@ protected abstract int readNudSolicitNumPostRoamingFromResource(); + protected abstract void storeNetworkEvent(String cluster, long now, long expiry, int eventType); + + protected abstract int[] getStoredNetworkEventCount(String cluster, long[] sinceTimes, + int[] eventType, long timeout); + protected final boolean testSkipped() { if (!useNetworkStackSignature() && !TestNetworkStackServiceClient.isSupported()) { fail("Device running root tests doesn't support TestNetworkStackServiceClient."); @@ -821,6 +838,19 @@ when(mPackageManager.getPackagesForUid(TEST_DEVICE_OWNER_APP_UID)).thenReturn( new String[] { TEST_DEVICE_OWNER_APP_PACKAGE }); + // Retrieve the network event count. + doAnswer(invocation -> { + final String cluster = invocation.getArgument(0); + final long[] sinceTimes = invocation.getArgument(1); + final int[] eventType = invocation.getArgument(2); + ((OnNetworkEventCountRetrievedListener) invocation.getArgument(3)) + .onNetworkEventCountRetrieved( + new Status(SUCCESS), + getStoredNetworkEventCount(cluster, sinceTimes, eventType, + 0 /* timeout not used */)); + return null; + }).when(mIpMemoryStore).retrieveNetworkEventCount(eq(TEST_CLUSTER), any(), any(), any()); + setDeviceConfigProperty(IpClient.CONFIG_MIN_RDNSS_LIFETIME, 67); setDeviceConfigProperty(DhcpClient.DHCP_RESTART_CONFIG_DELAY, 10); setDeviceConfigProperty(DhcpClient.ARP_FIRST_PROBE_DELAY_MS, 10); @@ -844,6 +874,12 @@ // Set the polling interval to update APF data snapshot. setDeviceConfigProperty(CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS, DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS); + + // Set the NUD failure event count daily and weekly thresholds. + setDeviceConfigProperty(CONFIG_NUD_FAILURE_COUNT_DAILY_THRESHOLD, + DEFAULT_NUD_FAILURE_COUNT_DAILY_THRESHOLD); + setDeviceConfigProperty(CONFIG_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD, + DEFAULT_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD); } private void awaitIpClientShutdown() throws Exception { @@ -4307,6 +4343,9 @@ // neighbor reachability checking relevant test cases, that guarantees // avoidingBadLinks() always returns true which is expected. .withoutMultinetworkPolicyTracker() + // Make cluster as non-null to test the NUD failure event count query logic. + .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER, + MacAddress.fromString(TEST_DEFAULT_BSSID))) .build(); startIpClientProvisioning(config); verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true); @@ -5986,4 +6025,159 @@ doDhcpHostnameSettingTest(IIpClient.HOSTNAME_SETTING_DO_NOT_SEND, false /* isHostnameConfigurationEnabled */, false /* expectSendHostname */); } + + // Store the network event to database multiple times. + private void storeNudFailureEvents(long when, long expiry, int times, int eventType) { + for (int i = 0; i < times; i++) { + storeNetworkEvent(TEST_CLUSTER, when, expiry, eventType); + when += 60 * 1000; // event interval is 1min + expiry += 60 * 1000; // expiry also delays 1min + } + } + + @Test + @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true) + @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown") + public void testIgnoreNudFailuresIfTooManyInPastDay() throws Exception { + // // NUD failure event count exceeds daily threshold nor weekly. + final long when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago + final long expiry = when + ONE_WEEK_IN_MS; + storeNudFailureEvents(when, expiry, 10, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM); + + runIpReachabilityMonitorProbeFailedTest(); + assertNeverNotifyNeighborLost(); + } + + @Test + @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = false) + @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown") + public void testIgnoreNudFailuresIfTooManyInPastDay_flagOff() throws Exception { + // NUD failure event count exceeds daily threshold nor weekly. + final long when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago + final long expiry = when + ONE_WEEK_IN_MS; + storeNudFailureEvents(when, expiry, 19, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM); + + runIpReachabilityMonitorProbeFailedTest(); + assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */, + NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL); + } + + @Test + @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true) + @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown") + public void testIgnoreNudFailuresIfTooManyInPastDay_notUpToThreshold() + throws Exception { + // NUD failure event count doesn't exceed either weekly threshold nor daily. + final long when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago + final long expiry = when + ONE_WEEK_IN_MS; + storeNudFailureEvents(when, expiry, 9, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM); + + runIpReachabilityMonitorProbeFailedTest(); + assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */, + NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL); + } + + @Test + @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true) + @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown") + public void testIgnoreNudFailuresIfTooManyInPastWeek() throws Exception { + // NUD failure event count exceeds the weekly threshold, but not daily threshold in the past + // day. + long when = System.currentTimeMillis() - ONE_WEEK_IN_MS / 2; // half a week ago + long expiry = when + ONE_WEEK_IN_MS; + storeNudFailureEvents(when, expiry, 11, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM); + + when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago + expiry = when + ONE_WEEK_IN_MS; + storeNudFailureEvents(when, expiry, 9, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM); + + runIpReachabilityMonitorProbeFailedTest(); + assertNeverNotifyNeighborLost(); + } + + @Test + @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = false) + @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown") + public void testIgnoreNudFailuresIfTooManyInPastWeek_flagOff() throws Exception { + // NUD failure event count exceeds the weekly threshold, but not daily threshold in the past + // day. + long when = System.currentTimeMillis() - ONE_WEEK_IN_MS / 2; // half a week ago + long expiry = when + ONE_WEEK_IN_MS; + storeNudFailureEvents(when, expiry, 11, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM); + + when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago + expiry = when + ONE_WEEK_IN_MS; + storeNudFailureEvents(when, expiry, 9, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM); + + runIpReachabilityMonitorProbeFailedTest(); + assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */, + NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL); + } + + @Test + @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true) + @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown") + public void testIgnoreNudFailuresIfTooManyInPastWeek_notUpToThreshold() throws Exception { + // NUD failure event count doesn't exceed either weekly threshold nor daily. + long when = System.currentTimeMillis() - ONE_WEEK_IN_MS / 2; // half a week ago + long expiry = when + ONE_WEEK_IN_MS; + storeNudFailureEvents(when, expiry, 10, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM); + + when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago + expiry = when + ONE_WEEK_IN_MS; + storeNudFailureEvents(when, expiry, 9, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM); + + runIpReachabilityMonitorProbeFailedTest(); + assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */, + NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL); + } + + @Test + @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true) + @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown") + public void testIgnoreNudFailuresIfTooManyInPastWeek_stopWritingEvent() throws Exception { + long when = (long) (System.currentTimeMillis() - SIX_HOURS_IN_MS * 0.9); + long expiry = when + ONE_WEEK_IN_MS; + storeNudFailureEvents(when, expiry, 10, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC); + + runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER, + ROUTER_LINK_LOCAL /* targetIp */, + false /* expectNeighborLost */); + verify(mIpMemoryStore, never()).storeNetworkEvent(any(), anyLong(), anyLong(), + eq(IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC), any()); + } + + @Test + @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = false) + @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown") + public void testIgnoreNudFailuresIfTooManyInPastWeek_stopWritingEvent_flagOff() + throws Exception { + long when = (long) (System.currentTimeMillis() - SIX_HOURS_IN_MS * 0.9); + long expiry = when + ONE_WEEK_IN_MS; + storeNudFailureEvents(when, expiry, 10, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC); + + runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER, + ROUTER_LINK_LOCAL /* targetIp */, + true /* expectNeighborLost */); + verify(mIpMemoryStore, never()).storeNetworkEvent(any(), anyLong(), anyLong(), + eq(IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC), any()); + } + + @Test + @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true) + @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown") + public void testIgnoreNudFailuresIfTooManyInPastWeek_stopWritingEvent_notUpToThreshold() + throws Exception { + long when = (long) (System.currentTimeMillis() - SIX_HOURS_IN_MS * 0.9); + long expiry = when + ONE_WEEK_IN_MS; + storeNudFailureEvents(when, expiry, 9, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC); + + runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER, + ROUTER_LINK_LOCAL /* targetIp */, + true /* expectNeighborLost */); + assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */, + NudEventType.NUD_ORGANIC_FAILED_CRITICAL); + verify(mIpMemoryStore).storeNetworkEvent(any(), anyLong(), anyLong(), + eq(IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC), any()); + } }
diff --git a/tests/integration/root/android/net/ip/IpClientRootTest.kt b/tests/integration/root/android/net/ip/IpClientRootTest.kt index 2c7ce17..3a56139 100644 --- a/tests/integration/root/android/net/ip/IpClientRootTest.kt +++ b/tests/integration/root/android/net/ip/IpClientRootTest.kt
@@ -25,6 +25,7 @@ import android.net.NetworkStackIpMemoryStore import android.net.ipmemorystore.NetworkAttributes import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener +import android.net.ipmemorystore.OnNetworkEventCountRetrievedListener import android.net.ipmemorystore.Status import android.net.networkstack.TestNetworkStackServiceClient import android.os.Process @@ -39,6 +40,7 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -250,6 +252,23 @@ } } + private class TestNetworkEventCountRetrievedListener : OnNetworkEventCountRetrievedListener { + private val future = CompletableFuture<IntArray>() + override fun onNetworkEventCountRetrieved( + status: Status, + counts: IntArray + ) { + if (status.resultCode != Status.SUCCESS) { + fail("retrieved the network event count " + " status: " + status.resultCode) + } + future.complete(counts) + } + + fun getBlockingNetworkEventCount(timeout: Long): IntArray { + return future.get(timeout, TimeUnit.MILLISECONDS) + } + } + override fun getStoredNetworkAttributes(l2Key: String, timeout: Long): NetworkAttributes { val listener = TestAttributesRetrievedListener() mStore.retrieveNetworkAttributes(l2Key, listener) @@ -268,6 +287,23 @@ mStore.storeNetworkAttributes(l2Key, na, null /* listener */) } + override fun storeNetworkEvent(cluster: String, now: Long, expiry: Long, eventType: Int) { + mStore.storeNetworkEvent(cluster, now, expiry, eventType, null /* listener */) + } + + override fun getStoredNetworkEventCount( + cluster: String, + sinceTimes: LongArray, + eventType: IntArray, + timeout: Long + ): IntArray { + val listener = TestNetworkEventCountRetrievedListener() + mStore.retrieveNetworkEventCount(cluster, sinceTimes, eventType, listener) + val counts = listener.getBlockingNetworkEventCount(timeout) + assertFalse(counts.size == 0) + return counts + } + private fun readNudSolicitNumFromResource(name: String): Int { val packageName = nsClient.getNetworkStackPackageName() val resource = mContext.createPackageContext(packageName, 0).getResources()
diff --git a/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt b/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt index f401073..c3b189a 100644 --- a/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt +++ b/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt
@@ -21,6 +21,7 @@ import android.net.ipmemorystore.Status import android.net.ipmemorystore.Status.SUCCESS import android.util.ArrayMap +import android.util.Pair import org.mockito.ArgumentCaptor import org.mockito.Mockito.any import org.mockito.Mockito.doAnswer @@ -63,6 +64,7 @@ override fun getDeviceConfigProperty(name: String): String? { return mDeviceConfigProperties.get(name) } + override fun getStoredNetworkAttributes(l2Key: String, timeout: Long): NetworkAttributes { val networkAttributesCaptor = ArgumentCaptor.forClass(NetworkAttributes::class.java) @@ -71,6 +73,30 @@ return networkAttributesCaptor.value } + override fun getStoredNetworkEventCount( + cluster: String, + sinceTimes: LongArray, + eventType: IntArray, + timeout: Long + ): IntArray { + val counts = IntArray(sinceTimes.size) + val eventTypesSet = eventType.toSet() // Convert eventType to Set for faster contains check + + sinceTimes.forEachIndexed { index, sinceTime -> + var count = 0 + mNetworkEvents.forEach { event -> + val key = event.first + val value = event.second + if (key == cluster && eventTypesSet.contains(value.second) && + sinceTime <= value.first) { + count++ + } + } + counts[index] = count + } + return counts + } + override fun assertIpMemoryNeverStoreNetworkAttributes(l2Key: String, timeout: Long) { verify(mIpMemoryStore, never()).storeNetworkAttributes(eq(l2Key), any(), any()) } @@ -83,6 +109,11 @@ }.`when`(mIpMemoryStore).retrieveNetworkAttributes(eq(l2Key), any()) } + override fun storeNetworkEvent(cluster: String, now: Long, expiry: Long, eventType: Int) { + val event = Pair(cluster, Pair(now, eventType)) + mNetworkEvents.add(event) + } + override fun readNudSolicitNumInSteadyStateFromResource(): Int { return DEFAULT_NUD_SOLICIT_NUM_STEADY_STATE }
diff --git a/tests/unit/src/android/net/ip/ConnectivityPacketTrackerTest.kt b/tests/unit/src/android/net/ip/ConnectivityPacketTrackerTest.kt index 1ee0ca5..51a871d 100644 --- a/tests/unit/src/android/net/ip/ConnectivityPacketTrackerTest.kt +++ b/tests/unit/src/android/net/ip/ConnectivityPacketTrackerTest.kt
@@ -102,7 +102,7 @@ 010203040506000102030405080600010800060400015c857e3c74e1c0a8012200000000000000000000 """.replace("\\s+".toRegex(), "").trim().uppercase() val arpPktByteArray = HexDump.hexStringToByteArray(arpPkt) - assertEquals(0, getTotalCapturePacketCount(packetTracker)) + assertEquals(0, getCapturePacketTypeCount(packetTracker)) assertEquals(0, getMatchedPacketCount(packetTracker, arpPkt)) // start capture packet @@ -113,12 +113,12 @@ Thread.sleep(SLEEP_TIMEOUT_MS) } - assertEquals(1, getTotalCapturePacketCount(packetTracker)) + assertEquals(1, getCapturePacketTypeCount(packetTracker)) assertEquals(5, getMatchedPacketCount(packetTracker, arpPkt)) // stop capture packet setCapture(packetTracker, false) - assertEquals(0, getTotalCapturePacketCount(packetTracker)) + assertEquals(0, getCapturePacketTypeCount(packetTracker)) assertEquals(0, getMatchedPacketCount(packetTracker, arpPkt)) } @@ -157,7 +157,7 @@ assertEquals(1, getMatchedPacketCount(packetTracker, it)) } - assertEquals(mDependencies.maxCapturePktSize, getTotalCapturePacketCount(packetTracker)) + assertEquals(mDependencies.maxCapturePktSize, getCapturePacketTypeCount(packetTracker)) } @Throws(InterruptedIOException::class, ErrnoException::class) @@ -215,13 +215,13 @@ return result.get(TIMEOUT_MS, TimeUnit.MILLISECONDS) } - private fun getTotalCapturePacketCount( + private fun getCapturePacketTypeCount( packetTracker: ConnectivityPacketTracker ): Int { val result = CompletableFuture<Int>() handler.post { try { - val totalCnt = packetTracker.totalCapturePacketCount + val totalCnt = packetTracker.capturePacketTypeCount result.complete(totalCnt) } catch (e: Exception) { result.completeExceptionally(e)
diff --git a/tests/unit/src/com/android/networkstack/ipmemorystore/IpMemoryStoreServiceTest.java b/tests/unit/src/com/android/networkstack/ipmemorystore/IpMemoryStoreServiceTest.java index 7d11f80..ee6c48b 100644 --- a/tests/unit/src/com/android/networkstack/ipmemorystore/IpMemoryStoreServiceTest.java +++ b/tests/unit/src/com/android/networkstack/ipmemorystore/IpMemoryStoreServiceTest.java
@@ -16,6 +16,14 @@ package com.android.networkstack.ipmemorystore; +import static android.net.ip.IpClient.NETWORK_EVENT_NUD_FAILURE_TYPES; +import static android.net.ip.IpClient.ONE_DAY_IN_MS; +import static android.net.ip.IpClient.ONE_WEEK_IN_MS; +import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM; +import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_CONFIRM; +import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC; +import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED; + import static com.android.networkstack.ipmemorystore.IpMemoryStoreDatabase.DbHelper.SCHEMA_VERSION; import static com.android.networkstack.ipmemorystore.RegularMaintenanceJobService.InterruptMaintenance; @@ -36,6 +44,7 @@ import android.net.ipmemorystore.IOnBlobRetrievedListener; import android.net.ipmemorystore.IOnL2KeyResponseListener; import android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener; +import android.net.ipmemorystore.IOnNetworkEventCountRetrievedListener; import android.net.ipmemorystore.IOnSameL3NetworkResponseListener; import android.net.ipmemorystore.IOnStatusAndCountListener; import android.net.ipmemorystore.IOnStatusListener; @@ -92,6 +101,13 @@ private static final String TEST_CLIENT_ID = "testClientId"; private static final String TEST_DATA_NAME = "testData"; private static final String TEST_DATABASE_NAME = "test.db"; + private static final String TEST_CLUSTER = "testCluster12345"; + private static final String TEST_CLUSTER_1 = "testCluster01234"; + + private static final File FILES_DIR = InstrumentationRegistry.getContext().getFilesDir(); + private static final String OLD_DB_NAME = "IpMemoryStore.db"; + private static final File OLD_DB = new File(FILES_DIR, OLD_DB_NAME); + private static final File TEST_DB = new File(FILES_DIR, TEST_DATABASE_NAME); private static final int TEST_DATABASE_SIZE_THRESHOLD = 100 * 1024; //100KB private static final int DEFAULT_TIMEOUT_MS = 5000; @@ -104,7 +120,6 @@ private static final String[] FAKE_KEYS; private static final byte[] TEST_BLOB_DATA = new byte[]{-3, 6, 8, -9, 12, -128, 0, 89, 112, 91, -34}; - static { FAKE_KEYS = new String[FAKE_KEY_COUNT]; for (int i = 0; i < FAKE_KEYS.length; ++i) { @@ -120,16 +135,14 @@ private IpMemoryStoreService mService; - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - final Context context = InstrumentationRegistry.getContext(); - final File dir = context.getFilesDir(); - mDbFile = new File(dir, TEST_DATABASE_NAME); + private IpMemoryStoreService createService() { + mDbFile = TEST_DB; doReturn(mDbFile).when(mMockContext).getDatabasePath(anyString()); + doReturn(OLD_DB).when(mMockContext).getDatabasePath(OLD_DB_NAME); + doReturn(mMockJobScheduler).when(mMockContext) .getSystemService(Context.JOB_SCHEDULER_SERVICE); - mService = new IpMemoryStoreService(mMockContext) { + final IpMemoryStoreService service = new IpMemoryStoreService(mMockContext) { @Override protected int getDbSizeThreshold() { return TEST_DATABASE_SIZE_THRESHOLD; @@ -143,12 +156,20 @@ return super.isDbSizeOverThreshold(); } }; + return service; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mService = createService(); } @After public void tearDown() { mService.shutdown(); mDbFile.delete(); + if (OLD_DB.exists()) OLD_DB.delete(); } private void copyTestData(final File file) throws Exception { @@ -362,6 +383,37 @@ }; } + /** Helper method to make an IOnNetworkEventCountRetrievedListener */ + private interface OnNetworkEventCountRetrievedListener { + void onNetworkEventCountRetrieved(Status status, int[] counts); + } + + private IOnNetworkEventCountRetrievedListener onNetworkEventCountRetrieved( + final OnNetworkEventCountRetrievedListener functor) { + return new IOnNetworkEventCountRetrievedListener() { + @Override + public void onNetworkEventCountRetrieved(final StatusParcelable status, + final int[] counts) throws RemoteException { + functor.onNetworkEventCountRetrieved(new Status(status), counts); + } + + @Override + public IBinder asBinder() { + return null; + } + + @Override + public int getInterfaceVersion() { + return this.VERSION; + } + + @Override + public String getInterfaceHash() { + return this.HASH; + } + }; + } + // Helper method to factorize some boilerplate private void doLatched(final String timeoutMessage, final Consumer<CountDownLatch> functor) { doLatched(timeoutMessage, functor, DEFAULT_TIMEOUT_MS); @@ -411,6 +463,23 @@ }))); } + // Helper method to store network events (NUD failure) to database. + private void storeNetworkEventOrFail(final String cluster, final long now, + final long expiry, final int eventType) { + storeNetworkEventOrFail("Did not complete storing a network event", cluster, now, + expiry, eventType); + } + + private void storeNetworkEventOrFail(final String timeoutMessage, final String cluster, + final long now, final long expiry, final int eventType) { + doLatched(timeoutMessage, latch -> mService.storeNetworkEvent(cluster, now, expiry, + eventType, + onStatus(status -> { + assertTrue("Store not successful : " + status.resultCode, status.isSuccess()); + latch.countDown(); + }))); + } + /** * This method is used to generate test.db file. * @@ -452,6 +521,22 @@ } } + private void generateFakeNetworkEvents() { + final int fakeEventCount = 1000; + final int expiredRecordsCount = 500; + final long now = System.currentTimeMillis(); + for (int i = 0; i < fakeEventCount; i++) { + final long timestamp = + i < expiredRecordsCount ? now - ONE_WEEK_IN_MS - i : now + i; + final long expiry = timestamp + ONE_WEEK_IN_MS; + storeNetworkEventOrFail( + TEST_CLUSTER, + timestamp, + expiry, + NETWORK_EVENT_NUD_FAILURE_TYPES[i % 4]); + } + } + /** Wait for assigned time. */ private void waitForMs(long ms) { try { @@ -559,22 +644,26 @@ }))); } + private void assertPrivateDataPresent(IpMemoryStoreService service, String l2Key) { + doLatched("Did not complete retrieving private data", latch -> + service.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, onBlobRetrieved( + (status, key, name, data) -> { + assertTrue("Retrieve blob status not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(name, TEST_DATA_NAME); + assertTrue(Arrays.equals(TEST_BLOB_DATA, data)); + latch.countDown(); + }))); + } + @Test public void testPrivateData() { final String l2Key = FAKE_KEYS[0]; final Blob b = new Blob(); storeBlobOrFail(l2Key, b, TEST_BLOB_DATA); - doLatched("Did not complete retrieving private data", latch -> - mService.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, onBlobRetrieved( - (status, key, name, data) -> { - assertTrue("Retrieve blob status not successful : " + status.resultCode, - status.isSuccess()); - assertEquals(l2Key, key); - assertEquals(name, TEST_DATA_NAME); - assertTrue(Arrays.equals(b.data, data)); - latch.countDown(); - }))); + assertPrivateDataPresent(mService, l2Key); // Most puzzling error message ever doLatched("Did not complete retrieving nothing", latch -> @@ -821,6 +910,25 @@ } @Test + public void testFullMaintenance_networkEvents() throws Exception { + generateFakeNetworkEvents(); + // After inserting test data, the size of the DB should be larger than the threshold. + assertTrue(mService.isDbSizeOverThreshold()); + + final InterruptMaintenance im = new InterruptMaintenance(0/* Fake JobId */); + // Do full maintenance and then the db should go down in size and be under the threshold. + doLatched("Maintenance unexpectedly completed successfully", latch -> + mService.fullMaintenance(onStatus((status) -> { + assertTrue("Execute full maintenance failed: " + + status.resultCode, status.isSuccess()); + latch.countDown(); + }), im), LONG_TIMEOUT_MS); + + // If maintenance is successful, the db size shall meet the threshold. + assertFalse(mService.isDbSizeOverThreshold()); + } + + @Test public void testInterruptMaintenance() throws Exception { copyTestData(mDbFile); // After inserting test data, the size of the DB should be larger than the threshold. @@ -986,6 +1094,424 @@ assertFalse(ipMemoryStoreService.isDbSizeOverThreshold()); } + /** + * Setup the NetworkEvents table with multiple NUD failure events before running each testcase. + * times eventType cluster timestamp + * 10 NETWORK_EVENT_NUD_FAILURE_ROAM TEST_CLUSTER 1.5 weeks ago + * 10 NETWORK_EVENT_NUD_FAILURE_ORGANIC TEST_CLUSTER_1 1 weeks ago + * 10 NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED TEST_CLUSTER 0.8 weeks ago + * 10 NETWORK_EVENT_NUD_FAILURE_CONFIRM TEST_CLUSTER 0.6 weeks ago + * 10 NETWORK_EVENT_NUD_FAILURE_ROAM TEST_CLUSTER_1 0.5 weeks ago + * 10 NETWORK_EVENT_NUD_FAILURE_ORGANIC TEST_CLUSTER 6 hours ago + */ + private void storeNetworkEventsForNudFailures(final long now) { + // Insert 10 NUD failure events post roam happened 1.5 weeks ago to TEST_CLUSTER. + long timestamp = (long) (now - ONE_WEEK_IN_MS * 1.5); + long expiry = timestamp + ONE_WEEK_IN_MS; + for (int i = 0; i < 10; i++) { + storeNetworkEventOrFail(TEST_CLUSTER, timestamp, expiry, + NETWORK_EVENT_NUD_FAILURE_ROAM); + } + + // Insert 10 NUD failure events due to organic check happened 1 weeks ago to + // TEST_CLUSTER_1. + timestamp = now - ONE_WEEK_IN_MS; + expiry = timestamp + ONE_WEEK_IN_MS; + for (int i = 0; i < 10; i++) { + storeNetworkEventOrFail(TEST_CLUSTER_1, timestamp, expiry, + NETWORK_EVENT_NUD_FAILURE_ORGANIC); + } + + // Insert 10 NUD failure events due to mac address change happened 0.8 weeks ago to + // TEST_CLUSTER. + timestamp = (long) (now - ONE_WEEK_IN_MS * 0.8); + expiry = timestamp + ONE_WEEK_IN_MS; + for (int i = 0; i < 10; i++) { + storeNetworkEventOrFail(TEST_CLUSTER, timestamp, expiry, + NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED); + } + + // Insert 10 NUD failure events from confirm happened 0.6 weeks ago to TEST_CLUSTER. + timestamp = (long) (now - ONE_WEEK_IN_MS * 0.6); + expiry = timestamp + ONE_WEEK_IN_MS; + for (int i = 0; i < 10; i++) { + storeNetworkEventOrFail(TEST_CLUSTER, timestamp, expiry, + NETWORK_EVENT_NUD_FAILURE_CONFIRM); + } + + // Insert 10 NUD failure events from confirm happened 0.5 weeks ago to TEST_CLUSTER_1. + timestamp = (long) (now - ONE_WEEK_IN_MS * 0.5); + expiry = timestamp + ONE_WEEK_IN_MS; + for (int i = 0; i < 10; i++) { + storeNetworkEventOrFail(TEST_CLUSTER_1, timestamp, expiry, + NETWORK_EVENT_NUD_FAILURE_ROAM); + } + + // Insert 10 NUD failure events from organic check 6 hours ago to TEST_CLUSTER. + timestamp = now - ONE_DAY_IN_MS / 4; + expiry = timestamp + ONE_WEEK_IN_MS; + for (int i = 0; i < 10; i++) { + storeNetworkEventOrFail(TEST_CLUSTER, timestamp, expiry, + NETWORK_EVENT_NUD_FAILURE_ORGANIC); + } + } + + @Test + public void testNetworkEventsQuery() { + final long now = System.currentTimeMillis(); + storeNetworkEventsForNudFailures(now); + + // Query network event counts for NUD failures within TEST_CLUSTER. + final long[] sinceTimes = new long[2]; + sinceTimes[0] = now - ONE_WEEK_IN_MS; + sinceTimes[1] = now - ONE_DAY_IN_MS; + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(TEST_CLUSTER, + sinceTimes, + NETWORK_EVENT_NUD_FAILURE_TYPES, + onNetworkEventCountRetrieved( + (status, counts) -> { + assertTrue("Retrieve network event counts not successful : " + + status.resultCode, status.isSuccess()); + assertTrue(counts.length == 2); + assertEquals(30, counts[0]); + assertEquals(10, counts[1]); + latch.countDown(); + }))); + + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(TEST_CLUSTER_1, + sinceTimes, + NETWORK_EVENT_NUD_FAILURE_TYPES, + onNetworkEventCountRetrieved( + (status, counts) -> { + assertTrue("Retrieve network event counts not successful : " + + status.resultCode, status.isSuccess()); + assertTrue(counts.length == 2); + assertEquals(20, counts[0]); + assertEquals(0, counts[1]); + latch.countDown(); + }))); + } + + private int[] eventTypes(final int... eventTypes) { + return eventTypes; + } + + @Test + public void testNetworkEventsQuery_differentEventTypes() { + final long now = System.currentTimeMillis(); + storeNetworkEventsForNudFailures(now); + + final long[] sinceTimes = new long[2]; + sinceTimes[0] = now - ONE_WEEK_IN_MS; + sinceTimes[1] = now - ONE_DAY_IN_MS; + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(TEST_CLUSTER, + sinceTimes, + eventTypes(NETWORK_EVENT_NUD_FAILURE_ROAM, + NETWORK_EVENT_NUD_FAILURE_CONFIRM), + onNetworkEventCountRetrieved( + (status, counts) -> { + assertTrue("Retrieve network event counts not successful : " + + status.resultCode, status.isSuccess()); + assertTrue(counts.length == 2); + assertEquals(10, counts[0]); + assertEquals(0, counts[1]); + latch.countDown(); + }))); + + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(TEST_CLUSTER, + sinceTimes, + eventTypes(NETWORK_EVENT_NUD_FAILURE_ORGANIC, + NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED), + onNetworkEventCountRetrieved( + (status, counts) -> { + assertTrue("Retrieve network event counts not successful : " + + status.resultCode, status.isSuccess()); + assertTrue(counts.length == 2); + assertEquals(20, counts[0]); + assertEquals(10, counts[1]); + latch.countDown(); + }))); + + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(TEST_CLUSTER_1, + sinceTimes, + eventTypes(NETWORK_EVENT_NUD_FAILURE_ORGANIC, + NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED), + onNetworkEventCountRetrieved( + (status, counts) -> { + assertTrue("Retrieve network event counts not successful : " + + status.resultCode, status.isSuccess()); + assertTrue(counts.length == 2); + assertEquals(10, counts[0]); + assertEquals(0, counts[1]); + latch.countDown(); + }))); + + } + + @Test + public void testNetworkEventsQuery_querySinceLastOneWeek() { + final long now = System.currentTimeMillis(); + storeNetworkEventsForNudFailures(now); + + final long[] sinceTimes = new long[] { now - ONE_WEEK_IN_MS }; + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(TEST_CLUSTER, + sinceTimes, + NETWORK_EVENT_NUD_FAILURE_TYPES, + onNetworkEventCountRetrieved( + (status, counts) -> { + assertTrue("Retrieve network event counts not successful : " + + status.resultCode, status.isSuccess()); + assertTrue(counts.length == 1); + assertEquals(30, counts[0]); + latch.countDown(); + }))); + + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(TEST_CLUSTER_1, + sinceTimes, + NETWORK_EVENT_NUD_FAILURE_TYPES, + onNetworkEventCountRetrieved( + (status, counts) -> { + assertTrue("Retrieve network event counts not successful : " + + status.resultCode, status.isSuccess()); + assertTrue(counts.length == 1); + assertEquals(20, counts[0]); + latch.countDown(); + }))); + } + + @Test + public void testNetworkEventsQuery_querySinceLastOneDay() { + final long now = System.currentTimeMillis(); + storeNetworkEventsForNudFailures(now); + + // Query network event count for NUD failures in past day within the same cluster. + final long[] sinceTimes = new long[] { now - ONE_DAY_IN_MS }; + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(TEST_CLUSTER, + sinceTimes, + NETWORK_EVENT_NUD_FAILURE_TYPES, + onNetworkEventCountRetrieved( + (status, counts) -> { + assertTrue("Retrieve network event counts not successful : " + + status.resultCode, status.isSuccess()); + assertTrue(counts.length == 1); + assertEquals(10, counts[0]); + latch.countDown(); + }))); + + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(TEST_CLUSTER_1, + sinceTimes, + NETWORK_EVENT_NUD_FAILURE_TYPES, + onNetworkEventCountRetrieved( + (status, counts) -> { + assertTrue("Retrieve network event counts not successful : " + + status.resultCode, status.isSuccess()); + assertTrue(counts.length == 1); + assertEquals(0, counts[0]); + latch.countDown(); + }))); + } + + @Test + public void testNetworkEventsQuery_wrongCluster() { + final long now = System.currentTimeMillis(); + storeNetworkEventsForNudFailures(now); + + // Query network event count for NUD failures within the same cluster. + final long[] sinceTimes = new long[2]; + sinceTimes[0] = now - ONE_WEEK_IN_MS; + sinceTimes[1] = now - ONE_DAY_IN_MS; + final int[] eventTypes = new int[] { NETWORK_EVENT_NUD_FAILURE_ROAM }; + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount("wrong_cluster_to_query", + sinceTimes, + eventTypes, + onNetworkEventCountRetrieved( + (status, counts) -> { + assertTrue("Retrieve network event counts not successful : " + + status.resultCode, status.isSuccess()); + assertTrue(counts.length == 2); + assertEquals(0, counts[0]); + assertEquals(0, counts[1]); + latch.countDown(); + }))); + } + + @Test + public void testNetworkEventsQuery_nullCluster() { + final long now = System.currentTimeMillis(); + storeNetworkEventsForNudFailures(now); + + // Query network event count for NUD failures within the same cluster. + final long[] sinceTimes = new long[2]; + sinceTimes[0] = now - ONE_WEEK_IN_MS; + sinceTimes[1] = now - ONE_DAY_IN_MS; + final int[] eventTypes = new int[] { NETWORK_EVENT_NUD_FAILURE_ROAM }; + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(null /* cluster */, + sinceTimes, + eventTypes, + onNetworkEventCountRetrieved( + (status, counts) -> { + assertFalse("Success retrieving network event count", + status.isSuccess()); + assertEquals(Status.ERROR_ILLEGAL_ARGUMENT, status.resultCode); + assertTrue(counts.length == 0); + latch.countDown(); + }))); + } + + @Test + public void testNetworkEventsQuery_emptyQueryEventType() { + final long now = System.currentTimeMillis(); + storeNetworkEventsForNudFailures(now); + + // Query network event count for NUD failure within the same cluster but event type to + // be queried is empty, an empty counts should be returned. + final long[] sinceTimes = new long[2]; + sinceTimes[0] = now - ONE_WEEK_IN_MS; + sinceTimes[1] = now - ONE_DAY_IN_MS; + final int[] eventTypes = new int[0]; + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(TEST_CLUSTER, + sinceTimes, + eventTypes, + onNetworkEventCountRetrieved( + (status, counts) -> { + assertTrue("Retrieve network event counts not successful : " + + status.resultCode, status.isSuccess()); + assertTrue(counts.length == 2); + assertEquals(0, counts[0]); + assertEquals(0, counts[1]); + latch.countDown(); + }))); + } + + @Test + public void testNetworkEventsQuery_emptySinceTimes() { + final long now = System.currentTimeMillis(); + storeNetworkEventsForNudFailures(now); + + // Query network event count for NUD failure within the same cluster but sinceTimes is + // empty, en empty count array will be returned and ERROR_ILLEGAL_ARGUMENT status. + final long[] sinceTimes = new long[0]; + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(TEST_CLUSTER, + sinceTimes, + NETWORK_EVENT_NUD_FAILURE_TYPES, + onNetworkEventCountRetrieved( + (status, counts) -> { + assertFalse("Success retrieving network event count", + status.isSuccess()); + assertEquals(Status.ERROR_ILLEGAL_ARGUMENT, status.resultCode); + assertTrue(counts.length == 0); + latch.countDown(); + }))); + } + + @Test + public void testNetworkEventsQuery_wrongEventType() { + final long now = System.currentTimeMillis(); + final long expiry = now + ONE_WEEK_IN_MS; + storeNetworkEventOrFail(TEST_CLUSTER, now, expiry, -1 /* nonexistent event type */); + + // Query network event count for NUD failure within the same cluster but event type doesn't + // match. + final long[] sinceTimes = new long[2]; + sinceTimes[0] = now - ONE_WEEK_IN_MS; + sinceTimes[1] = now - ONE_DAY_IN_MS; + doLatched("Did not complete retrieving network event count", latch -> + mService.retrieveNetworkEventCount(TEST_CLUSTER, + sinceTimes, + NETWORK_EVENT_NUD_FAILURE_TYPES, + onNetworkEventCountRetrieved( + (status, counts) -> { + assertTrue("Retrieve network event counts not successful : " + + status.resultCode, status.isSuccess()); + assertTrue(counts.length == 2); + assertEquals(0, counts[0]); + assertEquals(0, counts[1]); + latch.countDown(); + }))); + } + + @Test + public void testStoreNetworkEvent_nullCluster() { + final long now = System.currentTimeMillis(); + final long expiry = now + ONE_WEEK_IN_MS; + doLatched("Did not complete storing a network event", latch -> + mService.storeNetworkEvent(null /* cluster */, now, expiry, + NETWORK_EVENT_NUD_FAILURE_ROAM, + onStatus(status -> { + assertFalse("Success storing a network event with null cluster", + status.isSuccess()); + assertEquals(Status.ERROR_ILLEGAL_ARGUMENT, status.resultCode); + latch.countDown(); + }))); + } + + @Test + public void testRenameDb_noExistingDb_newDbCreated() throws Exception { + mService.shutdown(); + TEST_DB.delete(); + assertFalse(TEST_DB.exists()); + + assertFalse(OLD_DB.exists()); + assertFalse(TEST_DB.exists()); + + final IpMemoryStoreService service = createService(); + service.shutdown(); + assertFalse(OLD_DB.exists()); + assertTrue(TEST_DB.exists()); + } + + @Test + public void testRenameDb_existingDb_becomesNewDb() throws Exception { + mService.shutdown(); + TEST_DB.delete(); + assertFalse(TEST_DB.exists()); + + assertFalse(OLD_DB.exists()); + copyTestData(OLD_DB); + assertTrue(OLD_DB.exists()); + + final IpMemoryStoreService service = createService(); + assertPrivateDataPresent(service, FAKE_KEYS[0]); + assertFalse(OLD_DB.exists()); + assertTrue(TEST_DB.exists()); + + service.shutdown(); + } + + @Test + public void testRenameDb_existingDb_overwritesNewDb() throws Exception { + mService.shutdown(); + // Replace the new DB with garbage. This lets us check that the data survives the rename. + try (FileOutputStream out = new FileOutputStream(TEST_DB, false /* append */)) { + out.write(new byte[]{'g', 'a', 'r', 'b', 'a', 'g', 'e'}); + } + assertTrue(TEST_DB.exists()); + + assertFalse(OLD_DB.exists()); + copyTestData(OLD_DB); + assertTrue(OLD_DB.exists()); + + final IpMemoryStoreService service = createService(); + assertPrivateDataPresent(service, FAKE_KEYS[0]); + assertFalse(OLD_DB.exists()); + assertTrue(TEST_DB.exists()); + + service.shutdown(); + } + private void doTestDowngradeAndUpgrade(int downgradeVersion) { SQLiteOpenHelper dbHelper = new IpMemoryStoreDatabase.DbHelper( mMockContext, downgradeVersion); @@ -1000,7 +1526,7 @@ } @Test - public void testDowngradeClearsTablesAndriggers() { + public void testDowngradeClearsTablesAndTriggers() { final String l2Key = FAKE_KEYS[0]; final Blob b = new Blob(); storeBlobOrFail(l2Key, b, TEST_BLOB_DATA);
diff --git a/tests/unit/src/com/android/server/connectivity/FakeDns.java b/tests/unit/src/com/android/server/connectivity/FakeDns.java new file mode 100644 index 0000000..e689f49 --- /dev/null +++ b/tests/unit/src/com/android/server/connectivity/FakeDns.java
@@ -0,0 +1,279 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static android.net.DnsResolver.TYPE_A; +import static android.net.DnsResolver.TYPE_AAAA; +import static android.net.InetAddresses.parseNumericAddress; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doAnswer; + +import android.net.DnsResolver; +import android.net.Network; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Fakes DNS responses. + * + * Allows test methods to configure the IP addresses that will be resolved by + * Network#getAllByName and by various DnsResolver query methods. + */ +public class FakeDns { + private static final int HANDLER_TIMEOUT_MS = 1000; + + @NonNull + private final Network mNetwork; + @NonNull + private final DnsResolver mDnsResolver; + private final ArrayList<DnsEntry> mAnswers = new ArrayList<>(); + private boolean mNonBypassPrivateDnsWorking = true; + + public FakeDns(@NonNull Network network, @NonNull DnsResolver dnsResolver) { + + mNetwork = Objects.requireNonNull(network); + mDnsResolver = Objects.requireNonNull(dnsResolver); + } + + /** Data class to record the Dns entry. */ + private static class DnsEntry { + final String mHostname; + final int mType; + final AnswerSupplier mAnswerSupplier; + DnsEntry(String host, int type, AnswerSupplier answerSupplier) { + mHostname = host; + mType = type; + mAnswerSupplier = answerSupplier; + } + // Full match or partial match that target host contains the entry hostname to support + // random private dns probe hostname. + private boolean matches(String hostname, int type) { + return hostname.endsWith(mHostname) && type == mType; + } + } + + public interface AnswerSupplier { + /** Supplies the answer to one DnsResolver query method call. */ + List<String> get() throws DnsResolver.DnsException; + } + + private static class InstantAnswerSupplier implements AnswerSupplier { + private final List<String> mAnswers; + InstantAnswerSupplier(List<String> answers) { + mAnswers = answers; + } + @Override + public List<String> get() { + return mAnswers; + } + } + + /** Whether DNS queries on mNonBypassPrivateDnsWorking should succeed. */ + public void setNonBypassPrivateDnsWorking(boolean working) { + mNonBypassPrivateDnsWorking = working; + } + + /** Clears all DNS entries. */ + public void clearAll() { + synchronized (mAnswers) { + mAnswers.clear(); + } + } + + /** Returns the answer for a given name and type on the given mock network. */ + private CompletableFuture<List<String>> getAnswer(Network mockNetwork, String hostname, + int type) { + if (mNetwork.equals(mockNetwork) && !mNonBypassPrivateDnsWorking) { + return CompletableFuture.completedFuture(null); + } + + final AnswerSupplier answerSupplier; + + synchronized (mAnswers) { + answerSupplier = mAnswers.stream() + .filter(e -> e.matches(hostname, type)) + .map(answer -> answer.mAnswerSupplier).findFirst().orElse(null); + } + if (answerSupplier == null) { + return CompletableFuture.completedFuture(null); + } + + if (answerSupplier instanceof InstantAnswerSupplier) { + // Save latency waiting for a query thread if the answer is hardcoded. + return CompletableFuture.completedFuture( + ((InstantAnswerSupplier) answerSupplier).get()); + } + final CompletableFuture<List<String>> answerFuture = new CompletableFuture<>(); + new Thread(() -> { + try { + answerFuture.complete(answerSupplier.get()); + } catch (DnsResolver.DnsException e) { + answerFuture.completeExceptionally(e); + } + }).start(); + return answerFuture; + } + + /** Sets the answer for a given name and type. */ + public void setAnswer(String hostname, String[] answer, int type) { + setAnswer(hostname, new InstantAnswerSupplier( + (answer == null) ? null : Arrays.asList(answer)), type); + } + + /** Sets the answer for a given name and type. */ + public void setAnswer(String hostname, AnswerSupplier answerSupplier, int type) { + DnsEntry record = new DnsEntry(hostname, type, answerSupplier); + synchronized (mAnswers) { + // Remove the existing one. + mAnswers.removeIf(entry -> entry.matches(hostname, type)); + // Add or replace a new record. + mAnswers.add(record); + } + } + + /** Simulates a getAllByName call for the specified name on the specified mock network. */ + private InetAddress[] getAllByName(Network mockNetwork, String hostname) + throws UnknownHostException { + final List<InetAddress> answer; + try { + answer = stringsToInetAddresses(queryAllTypes(mockNetwork, hostname).get( + HANDLER_TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + throw new AssertionError("No mock DNS reply within timeout", e); + } + if (answer == null || answer.size() == 0) { + throw new UnknownHostException(hostname); + } + return answer.toArray(new InetAddress[0]); + } + + // Regardless of the type, depends on what the responses contained in the network. + @SuppressWarnings("FutureReturnValueIgnored") + private CompletableFuture<List<String>> queryAllTypes( + Network mockNetwork, String hostname) { + if (mNetwork.equals(mockNetwork) && !mNonBypassPrivateDnsWorking) { + return CompletableFuture.completedFuture(null); + } + + final CompletableFuture<List<String>> aFuture = + getAnswer(mockNetwork, hostname, TYPE_A) + .exceptionally(e -> Collections.emptyList()); + final CompletableFuture<List<String>> aaaaFuture = + getAnswer(mockNetwork, hostname, TYPE_AAAA) + .exceptionally(e -> Collections.emptyList()); + + final CompletableFuture<List<String>> combinedFuture = new CompletableFuture<>(); + aFuture.thenAcceptBoth(aaaaFuture, (res1, res2) -> { + final List<String> answer = new ArrayList<>(); + if (res1 != null) answer.addAll(res1); + if (res2 != null) answer.addAll(res2); + combinedFuture.complete(answer); + }); + return combinedFuture; + } + + /** Starts mocking DNS queries. */ + public void startMocking() throws UnknownHostException { + // Queries on mNetwork using getAllByName. + doAnswer(invocation -> { + return getAllByName((Network) invocation.getMock(), invocation.getArgument(0)); + }).when(mNetwork).getAllByName(any()); + + // Queries on mCleartextDnsNetwork using DnsResolver#query. + doAnswer(invocation -> { + return mockQuery(invocation, 0 /* posNetwork */, 1 /* posHostname */, + 3 /* posExecutor */, 5 /* posCallback */, -1 /* posType */); + }).when(mDnsResolver).query(any(), any(), anyInt(), any(), any(), any()); + + // Queries on mCleartextDnsNetwork using DnsResolver#query with QueryType. + doAnswer(invocation -> { + return mockQuery(invocation, 0 /* posNetwork */, 1 /* posHostname */, + 4 /* posExecutor */, 6 /* posCallback */, 2 /* posType */); + }).when(mDnsResolver).query(any(), any(), anyInt(), anyInt(), any(), any(), any()); + } + + private List<InetAddress> stringsToInetAddresses(List<String> addrs) { + if (addrs == null) return null; + final List<InetAddress> out = new ArrayList<>(); + for (String addr : addrs) { + out.add(parseNumericAddress(addr)); + } + return out; + } + + // Mocks all the DnsResolver query methods used in this test. + @SuppressWarnings("FutureReturnValueIgnored") + private Answer mockQuery(InvocationOnMock invocation, int posNetwork, int posHostname, + int posExecutor, int posCallback, int posType) { + String hostname = invocation.getArgument(posHostname); + Executor executor = invocation.getArgument(posExecutor); + Network network = invocation.getArgument(posNetwork); + DnsResolver.Callback callback = invocation.getArgument(posCallback); + + final CompletableFuture<List<String>> answerFuture = (posType != -1) + ? getAnswer(network, hostname, invocation.getArgument(posType)) + : queryAllTypes(network, hostname); + + answerFuture.whenComplete((answer, exception) -> { + new Handler(Looper.getMainLooper()).post(() -> executor.execute(() -> { + if (exception != null) { + if (!(exception instanceof DnsResolver.DnsException)) { + throw new AssertionError("Test error building DNS response", exception); + } + callback.onError((DnsResolver.DnsException) exception); + return; + } + if (answer != null && answer.size() > 0) { + final int qtype = (posType != -1) + ? invocation.getArgument(posType) : TYPE_AAAA; + switch (qtype) { + // Assume A and AAAA queries use the List<InetAddress> callback. + case TYPE_A: + case TYPE_AAAA: + callback.onAnswer(stringsToInetAddresses(answer), 0); + break; + default: + throw new UnsupportedOperationException( + "Unsupported qtype: " + qtype + ", update this fake"); + } + } + })); + }); + // If the future does not complete or has no answer do nothing. The timeout should fire. + return null; + } +}
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java index 9a86d9f..64196dd 100644 --- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java +++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -138,9 +138,7 @@ import android.os.Build; import android.os.Bundle; import android.os.ConditionVariable; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; @@ -200,8 +198,6 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -223,13 +219,9 @@ import java.util.Map; import java.util.Objects; import java.util.Random; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.function.Predicate; import javax.net.ssl.SSLHandshakeException; @@ -375,224 +367,6 @@ .addCapability(NET_CAPABILITY_OEM_PAID) .removeCapability(NET_CAPABILITY_NOT_RESTRICTED); - /** - * Fakes DNS responses. - * - * Allows test methods to configure the IP addresses that will be resolved by - * Network#getAllByName and by DnsResolver#query. - */ - class FakeDns { - /** Data class to record the Dns entry. */ - class DnsEntry { - final String mHostname; - final int mType; - final AnswerSupplier mAnswerSupplier; - DnsEntry(String host, int type, AnswerSupplier answerSupplier) { - mHostname = host; - mType = type; - mAnswerSupplier = answerSupplier; - } - // Full match or partial match that target host contains the entry hostname to support - // random private dns probe hostname. - private boolean matches(String hostname, int type) { - return hostname.endsWith(mHostname) && type == mType; - } - } - interface AnswerSupplier { - List<String> get() throws DnsResolver.DnsException; - } - - class InstantAnswerSupplier implements AnswerSupplier { - private final List<String> mAnswers; - InstantAnswerSupplier(List<String> answers) { - mAnswers = answers; - } - @Override - public List<String> get() { - return mAnswers; - } - } - - private final ArrayList<DnsEntry> mAnswers = new ArrayList<>(); - private boolean mNonBypassPrivateDnsWorking = true; - - /** Whether DNS queries on mNonBypassPrivateDnsWorking should succeed. */ - private void setNonBypassPrivateDnsWorking(boolean working) { - mNonBypassPrivateDnsWorking = working; - } - - /** Clears all DNS entries. */ - private void clearAll() { - synchronized (mAnswers) { - mAnswers.clear(); - } - } - - /** Returns the answer for a given name and type on the given mock network. */ - private CompletableFuture<List<String>> getAnswer(Network mockNetwork, String hostname, - int type) { - if (mockNetwork == mNetwork && !mNonBypassPrivateDnsWorking) { - return CompletableFuture.completedFuture(null); - } - - final AnswerSupplier answerSupplier; - - synchronized (mAnswers) { - answerSupplier = mAnswers.stream() - .filter(e -> e.matches(hostname, type)) - .map(answer -> answer.mAnswerSupplier).findFirst().orElse(null); - } - if (answerSupplier == null) { - return CompletableFuture.completedFuture(null); - } - - if (answerSupplier instanceof InstantAnswerSupplier) { - // Save latency waiting for a query thread if the answer is hardcoded. - return CompletableFuture.completedFuture( - ((InstantAnswerSupplier) answerSupplier).get()); - } - final CompletableFuture<List<String>> answerFuture = new CompletableFuture<>(); - new Thread(() -> { - try { - answerFuture.complete(answerSupplier.get()); - } catch (DnsResolver.DnsException e) { - answerFuture.completeExceptionally(e); - } - }).start(); - return answerFuture; - } - - /** Sets the answer for a given name and type. */ - private void setAnswer(String hostname, String[] answer, int type) { - setAnswer(hostname, new InstantAnswerSupplier( - (answer == null) ? null : Arrays.asList(answer)), type); - } - - private void setAnswer(String hostname, AnswerSupplier answerSupplier, int type) { - DnsEntry record = new DnsEntry(hostname, type, answerSupplier); - synchronized (mAnswers) { - // Remove the existing one. - mAnswers.removeIf(entry -> entry.matches(hostname, type)); - // Add or replace a new record. - mAnswers.add(record); - } - } - - private byte[] makeSvcbResponse(String hostname, List<String> answer) { - throw new UnsupportedOperationException("Not supported, update this fake"); - } - - /** Simulates a getAllByName call for the specified name on the specified mock network. */ - private InetAddress[] getAllByName(Network mockNetwork, String hostname) - throws UnknownHostException { - final List<InetAddress> answer; - try { - answer = stringsToInetAddresses(queryAllTypes(mockNetwork, hostname).get( - HANDLER_TIMEOUT_MS, TimeUnit.MILLISECONDS)); - } catch (ExecutionException | InterruptedException | TimeoutException e) { - throw new AssertionError("No mock DNS reply within timeout", e); - } - if (answer == null || answer.size() == 0) { - throw new UnknownHostException(hostname); - } - return answer.toArray(new InetAddress[0]); - } - - // Regardless of the type, depends on what the responses contained in the network. - private CompletableFuture<List<String>> queryAllTypes( - Network mockNetwork, String hostname) { - if (mockNetwork == mNetwork && !mNonBypassPrivateDnsWorking) { - return CompletableFuture.completedFuture(null); - } - - final CompletableFuture<List<String>> aFuture = - getAnswer(mockNetwork, hostname, TYPE_A) - .exceptionally(e -> Collections.emptyList()); - final CompletableFuture<List<String>> aaaaFuture = - getAnswer(mockNetwork, hostname, TYPE_AAAA) - .exceptionally(e -> Collections.emptyList()); - - final CompletableFuture<List<String>> combinedFuture = new CompletableFuture<>(); - aFuture.thenAcceptBoth(aaaaFuture, (res1, res2) -> { - final List<String> answer = new ArrayList<>(); - if (res1 != null) answer.addAll(res1); - if (res2 != null) answer.addAll(res2); - combinedFuture.complete(answer); - }); - return combinedFuture; - } - - /** Starts mocking DNS queries. */ - private void startMocking() throws UnknownHostException { - // Queries on mNetwork using getAllByName. - doAnswer(invocation -> { - return getAllByName((Network) invocation.getMock(), invocation.getArgument(0)); - }).when(mNetwork).getAllByName(any()); - - // Queries on mCleartextDnsNetwork using DnsResolver#query. - doAnswer(invocation -> { - return mockQuery(invocation, 0 /* posNetwork */, 1 /* posHostname */, - 3 /* posExecutor */, 5 /* posCallback */, -1 /* posType */); - }).when(mDnsResolver).query(any(), any(), anyInt(), any(), any(), any()); - - // Queries on mCleartextDnsNetwork using DnsResolver#query with QueryType. - doAnswer(invocation -> { - return mockQuery(invocation, 0 /* posNetwork */, 1 /* posHostname */, - 4 /* posExecutor */, 6 /* posCallback */, 2 /* posType */); - }).when(mDnsResolver).query(any(), any(), anyInt(), anyInt(), any(), any(), any()); - } - - private List<InetAddress> stringsToInetAddresses(List<String> addrs) { - if (addrs == null) return null; - final List<InetAddress> out = new ArrayList<>(); - for (String addr : addrs) { - out.add(parseNumericAddress(addr)); - } - return out; - } - - // Mocking queries on DnsResolver#query. - private Answer mockQuery(InvocationOnMock invocation, int posNetwork, int posHostname, - int posExecutor, int posCallback, int posType) { - String hostname = (String) invocation.getArgument(posHostname); - Executor executor = (Executor) invocation.getArgument(posExecutor); - Network network = invocation.getArgument(posNetwork); - DnsResolver.Callback callback = invocation.getArgument(posCallback); - - final CompletableFuture<List<String>> answerFuture = (posType != -1) - ? getAnswer(network, hostname, invocation.getArgument(posType)) - : queryAllTypes(network, hostname); - - answerFuture.whenComplete((answer, exception) -> { - new Handler(Looper.getMainLooper()).post(() -> executor.execute(() -> { - if (exception != null) { - if (!(exception instanceof DnsResolver.DnsException)) { - throw new AssertionError("Test error building DNS response", exception); - } - callback.onError((DnsResolver.DnsException) exception); - return; - } - if (answer != null && answer.size() > 0) { - final int qtype = (posType != -1) - ? invocation.getArgument(posType) : TYPE_AAAA; - switch (qtype) { - // Assume A and AAAA queries use the List<InetAddress> callback. - case TYPE_A: - case TYPE_AAAA: - callback.onAnswer(stringsToInetAddresses(answer), 0); - break; - default: - throw new UnsupportedOperationException( - "Unsupported qtype: " + qtype + ", update this fake"); - } - } - })); - }); - // If the future does not complete or has no answer do nothing. The timeout should fire. - return null; - } - } - private FakeDns mFakeDns; @GuardedBy("mThreadsToBeCleared") @@ -694,7 +468,7 @@ initHttpConnection(mFallbackConnection); initHttpConnection(mOtherFallbackConnection); - mFakeDns = new FakeDns(); + mFakeDns = new FakeDns(mNetwork, mDnsResolver); mFakeDns.startMocking(); // Set private dns suffix answer. sendPrivateDnsProbe() in NetworkMonitor send probe with // one time hostname. The hostname will be [random generated UUID] + HOST_SUFFIX differently