diff options
27 files changed, 2003 insertions, 24 deletions
diff --git a/Android.mk b/Android.mk index a7cb362b677e..c4f222ea7174 100644 --- a/Android.mk +++ b/Android.mk @@ -398,6 +398,7 @@ LOCAL_SRC_FILES += \ core/java/com/android/internal/backup/IObbBackupService.aidl \ core/java/com/android/internal/car/ICarServiceHelper.aidl \ core/java/com/android/internal/inputmethod/IInputContentUriToken.aidl \ + core/java/com/android/internal/net/INetworkWatchlistManager.aidl \ core/java/com/android/internal/policy/IKeyguardDrawnCallback.aidl \ core/java/com/android/internal/policy/IKeyguardDismissCallback.aidl \ core/java/com/android/internal/policy/IKeyguardExitCallback.aidl \ diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 6989db64c42e..4efc2c79faed 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -81,6 +81,7 @@ import android.net.INetworkPolicyManager; import android.net.IpSecManager; import android.net.NetworkPolicyManager; import android.net.NetworkScoreManager; +import android.net.NetworkWatchlistManager; import android.net.lowpan.ILowpanManager; import android.net.lowpan.LowpanManager; import android.net.nsd.INsdManager; @@ -150,6 +151,7 @@ import com.android.internal.app.IAppOpsService; import com.android.internal.app.IBatteryStats; import com.android.internal.app.ISoundTriggerService; import com.android.internal.appwidget.IAppWidgetService; +import com.android.internal.net.INetworkWatchlistManager; import com.android.internal.os.IDropBoxManagerService; import com.android.internal.policy.PhoneLayoutInflater; @@ -862,6 +864,17 @@ final class SystemServiceRegistry { return new ShortcutManager(ctx, IShortcutService.Stub.asInterface(b)); }}); + registerService(Context.NETWORK_WATCHLIST_SERVICE, NetworkWatchlistManager.class, + new CachedServiceFetcher<NetworkWatchlistManager>() { + @Override + public NetworkWatchlistManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder b = + ServiceManager.getServiceOrThrow(Context.NETWORK_WATCHLIST_SERVICE); + return new NetworkWatchlistManager(ctx, + INetworkWatchlistManager.Stub.asInterface(b)); + }}); + registerService(Context.SYSTEM_HEALTH_SERVICE, SystemHealthManager.class, new CachedServiceFetcher<SystemHealthManager>() { @Override diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index c165fb3e925c..01ad3ad2dcca 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3413,6 +3413,8 @@ public abstract class Context { public static final String NETWORK_STATS_SERVICE = "netstats"; /** {@hide} */ public static final String NETWORK_POLICY_SERVICE = "netpolicy"; + /** {@hide} */ + public static final String NETWORK_WATCHLIST_SERVICE = "network_watchlist"; /** * Use with {@link #getSystemService} to retrieve a {@link diff --git a/core/java/android/net/IIpConnectivityMetrics.aidl b/core/java/android/net/IIpConnectivityMetrics.aidl index 6f07b3153833..aeaf09d8fafe 100644 --- a/core/java/android/net/IIpConnectivityMetrics.aidl +++ b/core/java/android/net/IIpConnectivityMetrics.aidl @@ -30,11 +30,11 @@ interface IIpConnectivityMetrics { int logEvent(in ConnectivityMetricsEvent event); /** - * At most one callback can be registered (by DevicePolicyManager). + * Callback can be registered by DevicePolicyManager or NetworkWatchlistService only. * @return status {@code true} if registering/unregistering of the callback was successful, * {@code false} otherwise (might happen if IIpConnectivityMetrics is not available, * if it happens make sure you call it when the service is up in the caller) */ - boolean registerNetdEventCallback(in INetdEventCallback callback); - boolean unregisterNetdEventCallback(); + boolean addNetdEventCallback(in int callerType, in INetdEventCallback callback); + boolean removeNetdEventCallback(in int callerType); } diff --git a/core/java/android/net/INetdEventCallback.aidl b/core/java/android/net/INetdEventCallback.aidl index 49436beadc51..1fd9423b6128 100644 --- a/core/java/android/net/INetdEventCallback.aidl +++ b/core/java/android/net/INetdEventCallback.aidl @@ -19,6 +19,10 @@ package android.net; /** {@hide} */ oneway interface INetdEventCallback { + // Possible addNetdEventCallback callers. + const int CALLBACK_CALLER_DEVICE_POLICY = 0; + const int CALLBACK_CALLER_NETWORK_WATCHLIST = 1; + /** * Reports a single DNS lookup function call. * This method must not block or perform long-running operations. diff --git a/core/java/android/net/NetworkWatchlistManager.java b/core/java/android/net/NetworkWatchlistManager.java new file mode 100644 index 000000000000..42e43c8aea1e --- /dev/null +++ b/core/java/android/net/NetworkWatchlistManager.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.annotation.SystemService; +import android.content.Context; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +import com.android.internal.net.INetworkWatchlistManager; +import com.android.internal.util.Preconditions; + +/** + * Class that manage network watchlist in system. + * @hide + */ +@SystemService(Context.NETWORK_WATCHLIST_SERVICE) +public class NetworkWatchlistManager { + + private static final String TAG = "NetworkWatchlistManager"; + private static final String SHARED_MEMORY_TAG = "NETWORK_WATCHLIST_SHARED_MEMORY"; + + private final Context mContext; + private final INetworkWatchlistManager mNetworkWatchlistManager; + + /** + * @hide + */ + public NetworkWatchlistManager(Context context, INetworkWatchlistManager manager) { + mContext = context; + mNetworkWatchlistManager = manager; + } + + /** + * @hide + */ + public NetworkWatchlistManager(Context context) { + mContext = Preconditions.checkNotNull(context, "missing context"); + mNetworkWatchlistManager = (INetworkWatchlistManager) + INetworkWatchlistManager.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_WATCHLIST_SERVICE)); + } + + /** + * Report network watchlist records if necessary. + * + * Watchlist report process will run summarize records into a single report, then the + * report will be processed by differential privacy framework and store it on disk. + * + * @hide + */ + public void reportWatchlistIfNecessary() { + try { + mNetworkWatchlistManager.reportWatchlistIfNecessary(); + } catch (RemoteException e) { + Log.e(TAG, "Cannot report records", e); + e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 0a20c4348462..69c218076efa 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -8513,6 +8513,13 @@ public final class Settings { public static final String NETWORK_METERED_MULTIPATH_PREFERENCE = "network_metered_multipath_preference"; + /** + * Network watchlist last report time. + * @hide + */ + public static final String NETWORK_WATCHLIST_LAST_REPORT_TIME = + "network_watchlist_last_report_time"; + /** * The thresholds of the wifi throughput badging (SD, HD etc.) as a comma-delimited list of * colon-delimited key-value pairs. The key is the badging enum value defined in diff --git a/core/java/com/android/internal/net/INetworkWatchlistManager.aidl b/core/java/com/android/internal/net/INetworkWatchlistManager.aidl new file mode 100644 index 000000000000..7e88369055b8 --- /dev/null +++ b/core/java/com/android/internal/net/INetworkWatchlistManager.aidl @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.net; + +import android.os.SharedMemory; + +/** {@hide} */ +interface INetworkWatchlistManager { + boolean startWatchlistLogging(); + boolean stopWatchlistLogging(); + void reportWatchlistIfNecessary(); +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 49de135d8ca5..b8d3a5744db5 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3944,6 +3944,10 @@ <service android:name="com.android.server.timezone.TimeZoneUpdateIdler" android:permission="android.permission.BIND_JOB_SERVICE" > </service> + + <service android:name="com.android.server.net.watchlist.ReportWatchlistJobService" + android:permission="android.permission.BIND_JOB_SERVICE" > + </service> </application> </manifest> diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java index 1002939c7fa4..d36ed639b4b9 100644 --- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java +++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java @@ -26,7 +26,6 @@ import static java.lang.reflect.Modifier.isFinal; import static java.lang.reflect.Modifier.isPublic; import static java.lang.reflect.Modifier.isStatic; -import android.platform.test.annotations.Presubmit; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -264,6 +263,7 @@ public class SettingsBackupTest { Settings.Global.NETSTATS_UID_TAG_ROTATE_AGE, Settings.Global.NETWORK_AVOID_BAD_WIFI, Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE, + Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME, Settings.Global.NETWORK_PREFERENCE, Settings.Global.NETWORK_RECOMMENDATIONS_PACKAGE, Settings.Global.NETWORK_RECOMMENDATION_REQUEST_TIMEOUT_MS, diff --git a/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java b/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java index 5cc390a02b86..f4278196b576 100644 --- a/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java +++ b/services/core/java/com/android/server/connectivity/IpConnectivityMetrics.java @@ -23,8 +23,6 @@ import android.net.INetdEventCallback; import android.net.metrics.ApfProgramEvent; import android.net.metrics.IpConnectivityLog; import android.os.Binder; -import android.os.IBinder; -import android.os.Parcelable; import android.os.Process; import android.provider.Settings; import android.text.TextUtils; @@ -322,22 +320,22 @@ final public class IpConnectivityMetrics extends SystemService { } @Override - public boolean registerNetdEventCallback(INetdEventCallback callback) { + public boolean addNetdEventCallback(int callerType, INetdEventCallback callback) { enforceNetdEventListeningPermission(); if (mNetdListener == null) { return false; } - return mNetdListener.registerNetdEventCallback(callback); + return mNetdListener.addNetdEventCallback(callerType, callback); } @Override - public boolean unregisterNetdEventCallback() { + public boolean removeNetdEventCallback(int callerType) { enforceNetdEventListeningPermission(); if (mNetdListener == null) { // if the service is null, we aren't registered anyway return true; } - return mNetdListener.unregisterNetdEventCallback(); + return mNetdListener.removeNetdEventCallback(callerType); } }; diff --git a/services/core/java/com/android/server/connectivity/NetdEventListenerService.java b/services/core/java/com/android/server/connectivity/NetdEventListenerService.java index 61b11e18e3cd..af138b936fa7 100644 --- a/services/core/java/com/android/server/connectivity/NetdEventListenerService.java +++ b/services/core/java/com/android/server/connectivity/NetdEventListenerService.java @@ -98,21 +98,55 @@ public class NetdEventListenerService extends INetdEventListener.Stub { @GuardedBy("this") private final TokenBucket mConnectTb = new TokenBucket(CONNECT_LATENCY_FILL_RATE, CONNECT_LATENCY_BURST_LIMIT); - // Callback should only be registered/unregistered when logging is being enabled/disabled in DPM - // by the device owner. It's DevicePolicyManager's responsibility to ensure that. + + + /** + * There are only 2 possible callbacks. + * + * mNetdEventCallbackList[CALLBACK_CALLER_DEVICE_POLICY]. + * Callback registered/unregistered when logging is being enabled/disabled in DPM + * by the device owner. It's DevicePolicyManager's responsibility to ensure that. + * + * mNetdEventCallbackList[CALLBACK_CALLER_NETWORK_WATCHLIST] + * Callback registered/unregistered by NetworkWatchlistService. + */ + @GuardedBy("this") + private static final int[] ALLOWED_CALLBACK_TYPES = { + INetdEventCallback.CALLBACK_CALLER_DEVICE_POLICY, + INetdEventCallback.CALLBACK_CALLER_NETWORK_WATCHLIST + }; + @GuardedBy("this") - private INetdEventCallback mNetdEventCallback; + private INetdEventCallback[] mNetdEventCallbackList = + new INetdEventCallback[ALLOWED_CALLBACK_TYPES.length]; - public synchronized boolean registerNetdEventCallback(INetdEventCallback callback) { - mNetdEventCallback = callback; + public synchronized boolean addNetdEventCallback(int callerType, INetdEventCallback callback) { + if (!isValidCallerType(callerType)) { + Log.e(TAG, "Invalid caller type: " + callerType); + return false; + } + mNetdEventCallbackList[callerType] = callback; return true; } - public synchronized boolean unregisterNetdEventCallback() { - mNetdEventCallback = null; + public synchronized boolean removeNetdEventCallback(int callerType) { + if (!isValidCallerType(callerType)) { + Log.e(TAG, "Invalid caller type: " + callerType); + return false; + } + mNetdEventCallbackList[callerType] = null; return true; } + private static boolean isValidCallerType(int callerType) { + for (int i = 0; i < ALLOWED_CALLBACK_TYPES.length; i++) { + if (callerType == ALLOWED_CALLBACK_TYPES[i]) { + return true; + } + } + return false; + } + public NetdEventListenerService(Context context) { this(context.getSystemService(ConnectivityManager.class)); } @@ -169,8 +203,10 @@ public class NetdEventListenerService extends INetdEventListener.Stub { long timestamp = System.currentTimeMillis(); getMetricsForNetwork(timestamp, netId).addDnsResult(eventType, returnCode, latencyMs); - if (mNetdEventCallback != null) { - mNetdEventCallback.onDnsEvent(hostname, ipAddresses, ipAddressesCount, timestamp, uid); + for (INetdEventCallback callback : mNetdEventCallbackList) { + if (callback != null) { + callback.onDnsEvent(hostname, ipAddresses, ipAddressesCount, timestamp, uid); + } } } @@ -184,8 +220,14 @@ public class NetdEventListenerService extends INetdEventListener.Stub { long timestamp = System.currentTimeMillis(); getMetricsForNetwork(timestamp, netId).addConnectResult(error, latencyMs, ipAddr); - if (mNetdEventCallback != null) { - mNetdEventCallback.onConnectEvent(ipAddr, port, timestamp, uid); + for (INetdEventCallback callback : mNetdEventCallbackList) { + if (callback != null) { + // TODO(rickywai): Remove this checking to collect ip in watchlist. + if (callback == + mNetdEventCallbackList[INetdEventCallback.CALLBACK_CALLER_DEVICE_POLICY]) { + callback.onConnectEvent(ipAddr, port, timestamp, uid); + } + } } } diff --git a/services/core/java/com/android/server/net/watchlist/DigestUtils.java b/services/core/java/com/android/server/net/watchlist/DigestUtils.java new file mode 100644 index 000000000000..57becb002a59 --- /dev/null +++ b/services/core/java/com/android/server/net/watchlist/DigestUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net.watchlist; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Utils for calculating digests. + */ +public class DigestUtils { + + private static final int FILE_READ_BUFFER_SIZE = 16 * 1024; + + private DigestUtils() {} + + /** @return SHA256 hash of the provided file */ + public static byte[] getSha256Hash(File apkFile) throws IOException, NoSuchAlgorithmException { + try (InputStream stream = new FileInputStream(apkFile)) { + return getSha256Hash(stream); + } + } + + /** @return SHA256 hash of data read from the provided input stream */ + public static byte[] getSha256Hash(InputStream stream) + throws IOException, NoSuchAlgorithmException { + MessageDigest digester = MessageDigest.getInstance("SHA256"); + + int bytesRead; + byte[] buf = new byte[FILE_READ_BUFFER_SIZE]; + while ((bytesRead = stream.read(buf)) >= 0) { + digester.update(buf, 0, bytesRead); + } + return digester.digest(); + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/net/watchlist/HarmfulDigests.java b/services/core/java/com/android/server/net/watchlist/HarmfulDigests.java new file mode 100644 index 000000000000..27c22cea1782 --- /dev/null +++ b/services/core/java/com/android/server/net/watchlist/HarmfulDigests.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net.watchlist; + +import com.android.internal.util.HexDump; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Helper class to store all harmful digests in memory. + * TODO: Optimize memory usage using byte array with binary search. + */ +class HarmfulDigests { + + private final Set<String> mDigestSet; + + HarmfulDigests(List<byte[]> digests) { + final HashSet<String> tmpDigestSet = new HashSet<>(); + final int size = digests.size(); + for (int i = 0; i < size; i++) { + tmpDigestSet.add(HexDump.toHexString(digests.get(i))); + } + mDigestSet = Collections.unmodifiableSet(tmpDigestSet); + } + + public boolean contains(byte[] digest) { + return mDigestSet.contains(HexDump.toHexString(digest)); + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + for (String digest : mDigestSet) { + pw.println(digest); + } + pw.println(""); + } +} diff --git a/services/core/java/com/android/server/net/watchlist/NetworkWatchlistService.java b/services/core/java/com/android/server/net/watchlist/NetworkWatchlistService.java new file mode 100644 index 000000000000..171703ac8933 --- /dev/null +++ b/services/core/java/com/android/server/net/watchlist/NetworkWatchlistService.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net.watchlist; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.IIpConnectivityMetrics; +import android.net.INetdEventCallback; +import android.net.NetworkWatchlistManager; +import android.net.metrics.IpConnectivityLog; +import android.os.Binder; +import android.os.Process; +import android.os.SharedMemory; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Slog; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.DumpUtils; +import com.android.internal.net.INetworkWatchlistManager; +import com.android.server.ServiceThread; +import com.android.server.SystemService; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +/** + * Implementation of network watchlist service. + */ +public class NetworkWatchlistService extends INetworkWatchlistManager.Stub { + + private static final String TAG = NetworkWatchlistService.class.getSimpleName(); + static final boolean DEBUG = false; + + private static final String PROPERTY_NETWORK_WATCHLIST_ENABLED = + "ro.network_watchlist_enabled"; + + private static final int MAX_NUM_OF_WATCHLIST_DIGESTS = 10000; + + public static class Lifecycle extends SystemService { + private NetworkWatchlistService mService; + + public Lifecycle(Context context) { + super(context); + } + + @Override + public void onStart() { + if (!SystemProperties.getBoolean(PROPERTY_NETWORK_WATCHLIST_ENABLED, false)) { + // Watchlist service is disabled + return; + } + mService = new NetworkWatchlistService(getContext()); + publishBinderService(Context.NETWORK_WATCHLIST_SERVICE, mService); + } + + @Override + public void onBootPhase(int phase) { + if (!SystemProperties.getBoolean(PROPERTY_NETWORK_WATCHLIST_ENABLED, false)) { + // Watchlist service is disabled + return; + } + if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) { + try { + mService.initIpConnectivityMetrics(); + mService.startWatchlistLogging(); + } catch (RemoteException e) { + // Should not happen + } + ReportWatchlistJobService.schedule(getContext()); + } + } + } + + private volatile boolean mIsLoggingEnabled = false; + private final Object mLoggingSwitchLock = new Object(); + + private final WatchlistSettings mSettings; + private final Context mContext; + + // Separate thread to handle expensive watchlist logging work. + private final ServiceThread mHandlerThread; + + @VisibleForTesting + IIpConnectivityMetrics mIpConnectivityMetrics; + @VisibleForTesting + WatchlistLoggingHandler mNetworkWatchlistHandler; + + public NetworkWatchlistService(Context context) { + mContext = context; + mSettings = WatchlistSettings.getInstance(); + mHandlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, + /* allowIo */ false); + mHandlerThread.start(); + mNetworkWatchlistHandler = new WatchlistLoggingHandler(mContext, + mHandlerThread.getLooper()); + mNetworkWatchlistHandler.reportWatchlistIfNecessary(); + } + + // For testing only + @VisibleForTesting + NetworkWatchlistService(Context context, ServiceThread handlerThread, + WatchlistLoggingHandler handler, IIpConnectivityMetrics ipConnectivityMetrics) { + mContext = context; + mSettings = WatchlistSettings.getInstance(); + mHandlerThread = handlerThread; + mNetworkWatchlistHandler = handler; + mIpConnectivityMetrics = ipConnectivityMetrics; + } + + private void initIpConnectivityMetrics() { + mIpConnectivityMetrics = (IIpConnectivityMetrics) IIpConnectivityMetrics.Stub.asInterface( + ServiceManager.getService(IpConnectivityLog.SERVICE_NAME)); + } + + private final INetdEventCallback mNetdEventCallback = new INetdEventCallback.Stub() { + @Override + public void onDnsEvent(String hostname, String[] ipAddresses, int ipAddressesCount, + long timestamp, int uid) { + if (!mIsLoggingEnabled) { + return; + } + mNetworkWatchlistHandler.asyncNetworkEvent(hostname, ipAddresses, uid); + } + + @Override + public void onConnectEvent(String ipAddr, int port, long timestamp, int uid) { + if (!mIsLoggingEnabled) { + return; + } + mNetworkWatchlistHandler.asyncNetworkEvent(null, new String[]{ipAddr}, uid); + } + }; + + @VisibleForTesting + protected boolean startWatchlistLoggingImpl() throws RemoteException { + if (DEBUG) { + Slog.i(TAG, "Starting watchlist logging."); + } + synchronized (mLoggingSwitchLock) { + if (mIsLoggingEnabled) { + Slog.w(TAG, "Watchlist logging is already running"); + return true; + } + try { + if (mIpConnectivityMetrics.addNetdEventCallback( + INetdEventCallback.CALLBACK_CALLER_NETWORK_WATCHLIST, mNetdEventCallback)) { + mIsLoggingEnabled = true; + return true; + } else { + return false; + } + } catch (RemoteException re) { + // Should not happen + return false; + } + } + } + + @Override + public boolean startWatchlistLogging() throws RemoteException { + enforceWatchlistLoggingPermission(); + return startWatchlistLoggingImpl(); + } + + @VisibleForTesting + protected boolean stopWatchlistLoggingImpl() { + if (DEBUG) { + Slog.i(TAG, "Stopping watchlist logging"); + } + synchronized (mLoggingSwitchLock) { + if (!mIsLoggingEnabled) { + Slog.w(TAG, "Watchlist logging is not running"); + return true; + } + // stop the logging regardless of whether we fail to unregister listener + mIsLoggingEnabled = false; + + try { + return mIpConnectivityMetrics.removeNetdEventCallback( + INetdEventCallback.CALLBACK_CALLER_NETWORK_WATCHLIST); + } catch (RemoteException re) { + // Should not happen + return false; + } + } + } + + @Override + public boolean stopWatchlistLogging() throws RemoteException { + enforceWatchlistLoggingPermission(); + return stopWatchlistLoggingImpl(); + } + + private void enforceWatchlistLoggingPermission() { + final int uid = Binder.getCallingUid(); + if (uid != Process.SYSTEM_UID) { + throw new SecurityException(String.format("Uid %d has no permission to change watchlist" + + " setting.", uid)); + } + } + + /** + * Set a new network watchlist. + * This method should be called by ConfigUpdater only. + * + * @return True if network watchlist is updated. + */ + public boolean setNetworkSecurityWatchlist(List<byte[]> domainsCrc32Digests, + List<byte[]> domainsSha256Digests, + List<byte[]> ipAddressesCrc32Digests, + List<byte[]> ipAddressesSha256Digests) { + Slog.i(TAG, "Setting network watchlist"); + if (domainsCrc32Digests == null || domainsSha256Digests == null + || ipAddressesCrc32Digests == null || ipAddressesSha256Digests == null) { + Slog.e(TAG, "Parameters cannot be null"); + return false; + } + if (domainsCrc32Digests.size() != domainsSha256Digests.size() + || ipAddressesCrc32Digests.size() != ipAddressesSha256Digests.size()) { + Slog.e(TAG, "Must need to have the same number of CRC32 and SHA256 digests"); + return false; + } + if (domainsSha256Digests.size() + ipAddressesSha256Digests.size() + > MAX_NUM_OF_WATCHLIST_DIGESTS) { + Slog.e(TAG, "Total watchlist size cannot exceed " + MAX_NUM_OF_WATCHLIST_DIGESTS); + return false; + } + mSettings.writeSettingsToDisk(domainsCrc32Digests, domainsSha256Digests, + ipAddressesCrc32Digests, ipAddressesSha256Digests); + Slog.i(TAG, "Set network watchlist: Success"); + return true; + } + + @Override + public void reportWatchlistIfNecessary() { + // Allow any apps to trigger report event, as we won't run it if it's too early. + mNetworkWatchlistHandler.reportWatchlistIfNecessary(); + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; + mSettings.dump(fd, pw, args); + } + +} diff --git a/services/core/java/com/android/server/net/watchlist/ReportWatchlistJobService.java b/services/core/java/com/android/server/net/watchlist/ReportWatchlistJobService.java new file mode 100644 index 000000000000..dfeb1b2d2eab --- /dev/null +++ b/services/core/java/com/android/server/net/watchlist/ReportWatchlistJobService.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net.watchlist; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.net.NetworkWatchlistManager; +import android.util.Slog; + +import java.util.concurrent.TimeUnit; + +/** + * A job that periodically report watchlist records. + */ +public class ReportWatchlistJobService extends JobService { + + private static final boolean DEBUG = NetworkWatchlistService.DEBUG; + private static final String TAG = "WatchlistJobService"; + + // Unique job id used in system service, other jobs should not use the same value. + public static final int REPORT_WATCHLIST_RECORDS_JOB_ID = 0xd7689; + public static final long REPORT_WATCHLIST_RECORDS_PERIOD_MILLIS = + TimeUnit.HOURS.toMillis(12); + + @Override + public boolean onStartJob(final JobParameters jobParameters) { + if (jobParameters.getJobId() != REPORT_WATCHLIST_RECORDS_JOB_ID) { + return false; + } + if (DEBUG) Slog.d(TAG, "Start scheduled job."); + new NetworkWatchlistManager(this).reportWatchlistIfNecessary(); + jobFinished(jobParameters, false); + return true; + } + + @Override + public boolean onStopJob(JobParameters jobParameters) { + return true; // Reschedule when possible. + } + + /** + * Schedule the {@link ReportWatchlistJobService} to run periodically. + */ + public static void schedule(Context context) { + if (DEBUG) Slog.d(TAG, "Scheduling records aggregator task"); + final JobScheduler scheduler = + (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + scheduler.schedule(new JobInfo.Builder(REPORT_WATCHLIST_RECORDS_JOB_ID, + new ComponentName(context, ReportWatchlistJobService.class)) + //.setOverrideDeadline(45 * 1000) // Schedule job soon, for testing. + .setPeriodic(REPORT_WATCHLIST_RECORDS_PERIOD_MILLIS) + .setRequiresDeviceIdle(true) + .setRequiresBatteryNotLow(true) + .setPersisted(false) + .build()); + } + +} diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java b/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java new file mode 100644 index 000000000000..2247558476c4 --- /dev/null +++ b/services/core/java/com/android/server/net/watchlist/WatchlistLoggingHandler.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net.watchlist; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.os.DropBoxManager; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; + +import java.io.File; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +/** + * A Handler class for network watchlist logging on a background thread. + */ +class WatchlistLoggingHandler extends Handler { + + private static final String TAG = WatchlistLoggingHandler.class.getSimpleName(); + private static final boolean DEBUG = NetworkWatchlistService.DEBUG; + + @VisibleForTesting + static final int LOG_WATCHLIST_EVENT_MSG = 1; + @VisibleForTesting + static final int REPORT_RECORDS_IF_NECESSARY_MSG = 2; + + private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); + private static final String DROPBOX_TAG = "network_watchlist_report"; + + private final Context mContext; + private final ContentResolver mResolver; + private final PackageManager mPm; + private final WatchlistReportDbHelper mDbHelper; + private final WatchlistSettings mSettings; + // A cache for uid and apk digest mapping. + // As uid won't be reused until reboot, it's safe to assume uid is unique per signature and app. + // TODO: Use more efficient data structure. + private final HashMap<Integer, byte[]> mCachedUidDigestMap = new HashMap<>(); + + private interface WatchlistEventKeys { + String HOST = "host"; + String IP_ADDRESSES = "ipAddresses"; + String UID = "uid"; + String TIMESTAMP = "timestamp"; + } + + WatchlistLoggingHandler(Context context, Looper looper) { + super(looper); + mContext = context; + mPm = mContext.getPackageManager(); + mResolver = mContext.getContentResolver(); + mDbHelper = WatchlistReportDbHelper.getInstance(context); + mSettings = WatchlistSettings.getInstance(); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case LOG_WATCHLIST_EVENT_MSG: { + final Bundle data = msg.getData(); + handleNetworkEvent( + data.getString(WatchlistEventKeys.HOST), + data.getStringArray(WatchlistEventKeys.IP_ADDRESSES), + data.getInt(WatchlistEventKeys.UID), + data.getLong(WatchlistEventKeys.TIMESTAMP) + ); + break; + } + case REPORT_RECORDS_IF_NECESSARY_MSG: + tryAggregateRecords(); + break; + default: { + Slog.d(TAG, "WatchlistLoggingHandler received an unknown of message."); + break; + } + } + } + + /** + * Report network watchlist records if we collected enough data. + */ + public void reportWatchlistIfNecessary() { + final Message msg = obtainMessage(REPORT_RECORDS_IF_NECESSARY_MSG); + sendMessage(msg); + } + + /** + * Insert network traffic event to watchlist async queue processor. + */ + public void asyncNetworkEvent(String host, String[] ipAddresses, int uid) { + final Message msg = obtainMessage(LOG_WATCHLIST_EVENT_MSG); + final Bundle bundle = new Bundle(); + bundle.putString(WatchlistEventKeys.HOST, host); + bundle.putStringArray(WatchlistEventKeys.IP_ADDRESSES, ipAddresses); + bundle.putInt(WatchlistEventKeys.UID, uid); + bundle.putLong(WatchlistEventKeys.TIMESTAMP, System.currentTimeMillis()); + msg.setData(bundle); + sendMessage(msg); + } + + private void handleNetworkEvent(String hostname, String[] ipAddresses, + int uid, long timestamp) { + if (DEBUG) { + Slog.i(TAG, "handleNetworkEvent with host: " + hostname + ", uid: " + uid); + } + final String cncDomain = searchAllSubDomainsInWatchlist(hostname); + if (cncDomain != null) { + insertRecord(getDigestFromUid(uid), cncDomain, timestamp); + } else { + final String cncIp = searchIpInWatchlist(ipAddresses); + if (cncIp != null) { + insertRecord(getDigestFromUid(uid), cncIp, timestamp); + } + } + } + + private boolean insertRecord(byte[] digest, String cncHost, long timestamp) { + final boolean result = mDbHelper.insertNewRecord(digest, cncHost, timestamp); + tryAggregateRecords(); + return result; + } + + private boolean shouldReportNetworkWatchlist() { + final long lastReportTime = Settings.Global.getLong(mResolver, + Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME, 0L); + final long currentTimestamp = System.currentTimeMillis(); + if (currentTimestamp < lastReportTime) { + Slog.i(TAG, "Last report time is larger than current time, reset report"); + mDbHelper.cleanup(); + return false; + } + return currentTimestamp >= lastReportTime + ONE_DAY_MS; + } + + private void tryAggregateRecords() { + if (shouldReportNetworkWatchlist()) { + Slog.i(TAG, "Start aggregating watchlist records."); + final DropBoxManager dbox = mContext.getSystemService(DropBoxManager.class); + if (dbox != null && !dbox.isTagEnabled(DROPBOX_TAG)) { + final WatchlistReportDbHelper.AggregatedResult aggregatedResult = + mDbHelper.getAggregatedRecords(); + final byte[] encodedResult = encodeAggregatedResult(aggregatedResult); + if (encodedResult != null) { + addEncodedReportToDropBox(encodedResult); + } + } + mDbHelper.cleanup(); + Settings.Global.putLong(mResolver, Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME, + System.currentTimeMillis()); + } else { + Slog.i(TAG, "No need to aggregate record yet."); + } + } + + private byte[] encodeAggregatedResult( + WatchlistReportDbHelper.AggregatedResult aggregatedResult) { + // TODO: Encode results using differential privacy. + return null; + } + + private void addEncodedReportToDropBox(byte[] encodedReport) { + final DropBoxManager dbox = mContext.getSystemService(DropBoxManager.class); + dbox.addData(DROPBOX_TAG, encodedReport, 0); + } + + /** + * Get app digest from app uid. + */ + private byte[] getDigestFromUid(int uid) { + final byte[] cachedDigest = mCachedUidDigestMap.get(uid); + if (cachedDigest != null) { + return cachedDigest; + } + final String[] packageNames = mPm.getPackagesForUid(uid); + final int userId = UserHandle.getUserId(uid); + if (!ArrayUtils.isEmpty(packageNames)) { + for (String packageName : packageNames) { + try { + final String apkPath = mPm.getPackageInfoAsUser(packageName, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId) + .applicationInfo.publicSourceDir; + if (TextUtils.isEmpty(apkPath)) { + Slog.w(TAG, "Cannot find apkPath for " + packageName); + continue; + } + final byte[] digest = DigestUtils.getSha256Hash(new File(apkPath)); + mCachedUidDigestMap.put(uid, digest); + return digest; + } catch (NameNotFoundException | NoSuchAlgorithmException | IOException e) { + Slog.e(TAG, "Should not happen", e); + return null; + } + } + } else { + Slog.e(TAG, "Should not happen"); + } + return null; + } + + /** + * Search if any ip addresses are in watchlist. + * + * @param ipAddresses Ip address that you want to search in watchlist. + * @return Ip address that exists in watchlist, null if it does not match anything. + */ + private String searchIpInWatchlist(String[] ipAddresses) { + for (String ipAddress : ipAddresses) { + if (isIpInWatchlist(ipAddress)) { + return ipAddress; + } + } + return null; + } + + /** Search if the ip is in watchlist */ + private boolean isIpInWatchlist(String ipAddr) { + if (ipAddr == null) { + return false; + } + return mSettings.containsIp(ipAddr); + } + + /** Search if the host is in watchlist */ + private boolean isHostInWatchlist(String host) { + if (host == null) { + return false; + } + return mSettings.containsDomain(host); + } + + /** + * Search if any sub-domain in host is in watchlist. + * + * @param host Host that we want to search. + * @return Domain that exists in watchlist, null if it does not match anything. + */ + private String searchAllSubDomainsInWatchlist(String host) { + if (host == null) { + return null; + } + final String[] subDomains = getAllSubDomains(host); + for (String subDomain : subDomains) { + if (isHostInWatchlist(subDomain)) { + return subDomain; + } + } + return null; + } + + /** Get all sub-domains in a host */ + @VisibleForTesting + static String[] getAllSubDomains(String host) { + if (host == null) { + return null; + } + final ArrayList<String> subDomainList = new ArrayList<>(); + subDomainList.add(host); + int index = host.indexOf("."); + while (index != -1) { + host = host.substring(index + 1); + if (!TextUtils.isEmpty(host)) { + subDomainList.add(host); + } + index = host.indexOf("."); + } + return subDomainList.toArray(new String[0]); + } +} diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java new file mode 100644 index 000000000000..f48463f5ae63 --- /dev/null +++ b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net.watchlist; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Pair; + +import com.android.internal.util.HexDump; + +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Helper class to process watchlist read / save watchlist reports. + */ +class WatchlistReportDbHelper extends SQLiteOpenHelper { + + private static final String TAG = "WatchlistReportDbHelper"; + + private static final String NAME = "watchlist_report.db"; + private static final int VERSION = 2; + + private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000; + + private static class WhiteListReportContract { + private static final String TABLE = "records"; + private static final String APP_DIGEST = "app_digest"; + private static final String CNC_DOMAIN = "cnc_domain"; + private static final String TIMESTAMP = "timestamp"; + } + + private static final String CREATE_TABLE_MODEL = "CREATE TABLE " + + WhiteListReportContract.TABLE + "(" + + WhiteListReportContract.APP_DIGEST + " BLOB," + + WhiteListReportContract.CNC_DOMAIN + " TEXT," + + WhiteListReportContract.TIMESTAMP + " INTEGER DEFAULT 0" + " )"; + + private static final int INDEX_DIGEST = 0; + private static final int INDEX_CNC_DOMAIN = 1; + private static final int INDEX_TIMESTAMP = 2; + + private static final String[] DIGEST_DOMAIN_PROJECTION = + new String[] { + WhiteListReportContract.APP_DIGEST, + WhiteListReportContract.CNC_DOMAIN + }; + + private static WatchlistReportDbHelper sInstance; + + /** + * Aggregated watchlist records. + */ + public static class AggregatedResult { + // A list of digests that visited c&c domain or ip before. + Set<String> appDigestList; + + // The c&c domain or ip visited before. + String cncDomainVisited; + + // A list of app digests and c&c domain visited. + HashMap<String, String> appDigestCNCList; + } + + private WatchlistReportDbHelper(Context context) { + super(context, WatchlistSettings.getSystemWatchlistFile(NAME).getAbsolutePath(), + null, VERSION); + // Memory optimization - close idle connections after 30s of inactivity + setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS); + } + + public static synchronized WatchlistReportDbHelper getInstance(Context context) { + if (sInstance != null) { + return sInstance; + } + sInstance = new WatchlistReportDbHelper(context); + return sInstance; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_MODEL); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // TODO: For now, drop older tables and recreate new ones. + db.execSQL("DROP TABLE IF EXISTS " + WhiteListReportContract.TABLE); + onCreate(db); + } + + /** + * Insert new watchlist record. + * + * @param appDigest The digest of an app. + * @param cncDomain C&C domain that app visited. + * @return True if success. + */ + public boolean insertNewRecord(byte[] appDigest, String cncDomain, + long timestamp) { + final SQLiteDatabase db = getWritableDatabase(); + final ContentValues values = new ContentValues(); + values.put(WhiteListReportContract.APP_DIGEST, appDigest); + values.put(WhiteListReportContract.CNC_DOMAIN, cncDomain); + values.put(WhiteListReportContract.TIMESTAMP, timestamp); + return db.insert(WhiteListReportContract.TABLE, null, values) != -1; + } + + /** + * Aggregate the records in database, and return a rappor encoded result. + */ + public AggregatedResult getAggregatedRecords() { + final long twoDaysBefore = getTwoDaysBeforeTimestamp(); + final long yesterday = getYesterdayTimestamp(); + final String selectStatement = WhiteListReportContract.TIMESTAMP + " >= ? AND " + + WhiteListReportContract.TIMESTAMP + " <= ?"; + + final SQLiteDatabase db = getReadableDatabase(); + Cursor c = null; + try { + c = db.query(true /* distinct */, + WhiteListReportContract.TABLE, DIGEST_DOMAIN_PROJECTION, selectStatement, + new String[]{"" + twoDaysBefore, "" + yesterday}, null, null, + null, null); + if (c == null || c.getCount() == 0) { + return null; + } + final AggregatedResult result = new AggregatedResult(); + result.cncDomainVisited = null; + // After aggregation, each digest maximum will have only 1 record. + result.appDigestList = new HashSet<>(); + result.appDigestCNCList = new HashMap<>(); + while (c.moveToNext()) { + // We use hex string here as byte[] cannot be a key in HashMap. + String digestHexStr = HexDump.toHexString(c.getBlob(INDEX_DIGEST)); + String cncDomain = c.getString(INDEX_CNC_DOMAIN); + + result.appDigestList.add(digestHexStr); + if (result.cncDomainVisited != null) { + result.cncDomainVisited = cncDomain; + } + result.appDigestCNCList.put(digestHexStr, cncDomain); + } + return result; + } finally { + if (c != null) { + c.close(); + } + } + } + + /** + * Remove all the records before yesterday. + * + * @return True if success. + */ + public boolean cleanup() { + final SQLiteDatabase db = getWritableDatabase(); + final long twoDaysBefore = getTwoDaysBeforeTimestamp(); + final String clause = WhiteListReportContract.TIMESTAMP + "< " + twoDaysBefore; + return db.delete(WhiteListReportContract.TABLE, clause, null) != 0; + } + + static long getTwoDaysBeforeTimestamp() { + return getMidnightTimestamp(2); + } + + static long getYesterdayTimestamp() { + return getMidnightTimestamp(1); + } + + static long getMidnightTimestamp(int daysBefore) { + java.util.Calendar date = new GregorianCalendar(); + // reset hour, minutes, seconds and millis + date.set(java.util.Calendar.HOUR_OF_DAY, 0); + date.set(java.util.Calendar.MINUTE, 0); + date.set(java.util.Calendar.SECOND, 0); + date.set(java.util.Calendar.MILLISECOND, 0); + date.add(java.util.Calendar.DAY_OF_MONTH, -daysBefore); + return date.getTimeInMillis(); + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java b/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java new file mode 100644 index 000000000000..c50f0d56c992 --- /dev/null +++ b/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net.watchlist; + +import android.os.Environment; +import android.util.AtomicFile; +import android.util.Log; +import android.util.Xml; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.HexDump; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.CRC32; + +/** + * A util class to do watchlist settings operations, like setting watchlist, query if a domain + * exists in watchlist. + */ +class WatchlistSettings { + private static final String TAG = "WatchlistSettings"; + + // Settings xml will be stored in /data/system/network_watchlist/watchlist_settings.xml + static final String SYSTEM_WATCHLIST_DIR = "network_watchlist"; + + private static final String WATCHLIST_XML_FILE = "watchlist_settings.xml"; + + private static class XmlTags { + private static final String WATCHLIST_SETTINGS = "watchlist-settings"; + private static final String SHA256_DOMAIN = "sha256-domain"; + private static final String CRC32_DOMAIN = "crc32-domain"; + private static final String SHA256_IP = "sha256-ip"; + private static final String CRC32_IP = "crc32-ip"; + private static final String HASH = "hash"; + } + + private static WatchlistSettings sInstance = new WatchlistSettings(); + private final AtomicFile mXmlFile; + private final Object mLock = new Object(); + private HarmfulDigests mCrc32DomainDigests = new HarmfulDigests(new ArrayList<>()); + private HarmfulDigests mSha256DomainDigests = new HarmfulDigests(new ArrayList<>()); + private HarmfulDigests mCrc32IpDigests = new HarmfulDigests(new ArrayList<>()); + private HarmfulDigests mSha256IpDigests = new HarmfulDigests(new ArrayList<>()); + + public static synchronized WatchlistSettings getInstance() { + return sInstance; + } + + private WatchlistSettings() { + this(getSystemWatchlistFile(WATCHLIST_XML_FILE)); + } + + @VisibleForTesting + protected WatchlistSettings(File xmlFile) { + mXmlFile = new AtomicFile(xmlFile); + readSettingsLocked(); + } + + static File getSystemWatchlistFile(String filename) { + final File dataSystemDir = Environment.getDataSystemDirectory(); + final File systemWatchlistDir = new File(dataSystemDir, SYSTEM_WATCHLIST_DIR); + systemWatchlistDir.mkdirs(); + return new File(systemWatchlistDir, filename); + } + + private void readSettingsLocked() { + synchronized (mLock) { + FileInputStream stream; + try { + stream = mXmlFile.openRead(); + } catch (FileNotFoundException e) { + Log.i(TAG, "No watchlist settings: " + mXmlFile.getBaseFile().getAbsolutePath()); + return; + } + + final List<byte[]> crc32DomainList = new ArrayList<>(); + final List<byte[]> sha256DomainList = new ArrayList<>(); + final List<byte[]> crc32IpList = new ArrayList<>(); + final List<byte[]> sha256IpList = new ArrayList<>(); + + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(stream, StandardCharsets.UTF_8.name()); + parser.nextTag(); + parser.require(XmlPullParser.START_TAG, null, XmlTags.WATCHLIST_SETTINGS); + while (parser.nextTag() == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + switch (tagName) { + case XmlTags.CRC32_DOMAIN: + parseHash(parser, tagName, crc32DomainList); + break; + case XmlTags.CRC32_IP: + parseHash(parser, tagName, crc32IpList); + break; + case XmlTags.SHA256_DOMAIN: + parseHash(parser, tagName, sha256DomainList); + break; + case XmlTags.SHA256_IP: + parseHash(parser, tagName, sha256IpList); + break; + default: + Log.w(TAG, "Unknown element: " + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + parser.require(XmlPullParser.END_TAG, null, XmlTags.WATCHLIST_SETTINGS); + writeSettingsToMemory(crc32DomainList, sha256DomainList, crc32IpList, sha256IpList); + } catch (IllegalStateException | NullPointerException | NumberFormatException | + XmlPullParserException | IOException | IndexOutOfBoundsException e) { + Log.w(TAG, "Failed parsing " + e); + } finally { + try { + stream.close(); + } catch (IOException e) { + } + } + } + } + + private void parseHash(XmlPullParser parser, String tagName, List<byte[]> hashSet) + throws IOException, XmlPullParserException { + parser.require(XmlPullParser.START_TAG, null, tagName); + while (parser.nextTag() == XmlPullParser.START_TAG) { + parser.require(XmlPullParser.START_TAG, null, XmlTags.HASH); + byte[] hash = HexDump.hexStringToByteArray(parser.nextText()); + parser.require(XmlPullParser.END_TAG, null, XmlTags.HASH); + hashSet.add(hash); + } + parser.require(XmlPullParser.END_TAG, null, tagName); + } + + /** + * Write network watchlist settings to disk. + * Adb should not use it, should use writeSettingsToMemory directly instead. + */ + public void writeSettingsToDisk(List<byte[]> newCrc32DomainList, + List<byte[]> newSha256DomainList, + List<byte[]> newCrc32IpList, + List<byte[]> newSha256IpList) { + synchronized (mLock) { + FileOutputStream stream; + try { + stream = mXmlFile.startWrite(); + } catch (IOException e) { + Log.w(TAG, "Failed to write display settings: " + e); + return; + } + + try { + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(stream, StandardCharsets.UTF_8.name()); + out.startDocument(null, true); + out.startTag(null, XmlTags.WATCHLIST_SETTINGS); + + writeHashSetToXml(out, XmlTags.SHA256_DOMAIN, newSha256DomainList); + writeHashSetToXml(out, XmlTags.SHA256_IP, newSha256IpList); + writeHashSetToXml(out, XmlTags.CRC32_DOMAIN, newCrc32DomainList); + writeHashSetToXml(out, XmlTags.CRC32_IP, newCrc32IpList); + + out.endTag(null, XmlTags.WATCHLIST_SETTINGS); + out.endDocument(); + mXmlFile.finishWrite(stream); + writeSettingsToMemory(newCrc32DomainList, newSha256DomainList, newCrc32IpList, + newSha256IpList); + } catch (IOException e) { + Log.w(TAG, "Failed to write display settings, restoring backup.", e); + mXmlFile.failWrite(stream); + } + } + } + + /** + * Write network watchlist settings to memory. + */ + public void writeSettingsToMemory(List<byte[]> newCrc32DomainList, + List<byte[]> newSha256DomainList, + List<byte[]> newCrc32IpList, + List<byte[]> newSha256IpList) { + synchronized (mLock) { + mCrc32DomainDigests = new HarmfulDigests(newCrc32DomainList); + mCrc32IpDigests = new HarmfulDigests(newCrc32IpList); + mSha256DomainDigests = new HarmfulDigests(newSha256DomainList); + mSha256IpDigests = new HarmfulDigests(newSha256IpList); + } + } + + private static void writeHashSetToXml(XmlSerializer out, String tagName, List<byte[]> hashSet) + throws IOException { + out.startTag(null, tagName); + for (byte[] hash : hashSet) { + out.startTag(null, XmlTags.HASH); + out.text(HexDump.toHexString(hash)); + out.endTag(null, XmlTags.HASH); + } + out.endTag(null, tagName); + } + + public boolean containsDomain(String domain) { + // First it does a quick CRC32 check. + final byte[] crc32 = getCrc32(domain); + if (!mCrc32DomainDigests.contains(crc32)) { + return false; + } + // Now we do a slow SHA256 check. + final byte[] sha256 = getSha256(domain); + return mSha256DomainDigests.contains(sha256); + } + + public boolean containsIp(String ip) { + // First it does a quick CRC32 check. + final byte[] crc32 = getCrc32(ip); + if (!mCrc32IpDigests.contains(crc32)) { + return false; + } + // Now we do a slow SHA256 check. + final byte[] sha256 = getSha256(ip); + return mSha256IpDigests.contains(sha256); + } + + + /** Get CRC32 of a string */ + private byte[] getCrc32(String str) { + final CRC32 crc = new CRC32(); + crc.update(str.getBytes()); + final long tmp = crc.getValue(); + return new byte[]{(byte)(tmp >> 24 & 255), (byte)(tmp >> 16 & 255), + (byte)(tmp >> 8 & 255), (byte)(tmp & 255)}; + } + + /** Get SHA256 of a string */ + private byte[] getSha256(String str) { + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA256"); + } catch (NoSuchAlgorithmException e) { + /* can't happen */ + return null; + } + messageDigest.update(str.getBytes()); + return messageDigest.digest(); + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("Domain CRC32 digest list:"); + mCrc32DomainDigests.dump(fd, pw, args); + pw.println("Domain SHA256 digest list:"); + mSha256DomainDigests.dump(fd, pw, args); + pw.println("Ip CRC32 digest list:"); + mCrc32IpDigests.dump(fd, pw, args); + pw.println("Ip SHA256 digest list:"); + mSha256IpDigests.dump(fd, pw, args); + } +} diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java index 00859311af84..0aaf32cbe275 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/NetworkLogger.java @@ -107,7 +107,8 @@ final class NetworkLogger { return false; } try { - if (mIpConnectivityMetrics.registerNetdEventCallback(mNetdEventCallback)) { + if (mIpConnectivityMetrics.addNetdEventCallback( + INetdEventCallback.CALLBACK_CALLER_DEVICE_POLICY, mNetdEventCallback)) { mHandlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, /* allowIo */ false); mHandlerThread.start(); @@ -138,7 +139,8 @@ final class NetworkLogger { // logging is forcefully disabled even if unregistering fails return true; } - return mIpConnectivityMetrics.unregisterNetdEventCallback(); + return mIpConnectivityMetrics.removeNetdEventCallback( + INetdEventCallback.CALLBACK_CALLER_DEVICE_POLICY); } catch (RemoteException re) { Slog.wtf(TAG, "Failed to make remote calls to unregister the callback", re); return true; diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index f8bcb73a8f83..e2e491a050de 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -83,6 +83,7 @@ import com.android.server.media.MediaSessionService; import com.android.server.media.projection.MediaProjectionManagerService; import com.android.server.net.NetworkPolicyManagerService; import com.android.server.net.NetworkStatsService; +import com.android.server.net.watchlist.NetworkWatchlistService; import com.android.server.notification.NotificationManagerService; import com.android.server.oemlock.OemLockService; import com.android.server.om.OverlayManagerService; @@ -884,6 +885,10 @@ public final class SystemServer { mSystemServiceManager.startService(IpConnectivityMetrics.class); traceEnd(); + traceBeginAndSlog("NetworkWatchlistService"); + mSystemServiceManager.startService(NetworkWatchlistService.Lifecycle.class); + traceEnd(); + traceBeginAndSlog("PinnerService"); mSystemServiceManager.startService(PinnerService.class); traceEnd(); diff --git a/services/tests/servicestests/assets/NetworkWatchlistTest/watchlist_settings_test1.xml b/services/tests/servicestests/assets/NetworkWatchlistTest/watchlist_settings_test1.xml new file mode 100644 index 000000000000..bb97e9431f72 --- /dev/null +++ b/services/tests/servicestests/assets/NetworkWatchlistTest/watchlist_settings_test1.xml @@ -0,0 +1,27 @@ +<?xml version='1.0'?> +<watchlist-settings> + <sha256-domain> + <!-- test-cc-domain.com --> + <hash>8E7DCD2AEB4F364358242BB3F403263E61E3B4AECE4E2500FF28BF32E52FF0F1</hash> + <!-- test-cc-match-sha256-only.com --> + <hash>F0905DA7549614957B449034C281EF7BDEFDBC2B6E050AD1E78D6DE18FBD0D5F</hash> + </sha256-domain> + <sha256-ip> + <!-- 127.0.0.2 --> + <hash>1EDD62868F2767A1FFF68DF0A4CB3C23448E45100715768DB9310B5E719536A1</hash> + <!-- 127.0.0.3, match in sha256 only --> + <hash>18DD41C9F2E8E4879A1575FB780514EF33CF6E1F66578C4AE7CCA31F49B9F2ED</hash> + </sha256-ip> + <crc32-domain> + <!-- test-cc-domain.com --> + <hash>6C67059D</hash> + <!-- test-cc-match-crc32-only.com --> + <hash>3DC775F8</hash> + </crc32-domain> + <crc32-ip> + <!-- 127.0.0.2 --> + <hash>4EBEB612</hash> + <!-- 127.0.0.4, match in crc32 only --> + <hash>A7DD1327</hash> + </crc32-ip> +</watchlist-settings> diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 9d23fe9691b1..6de3395e6fdf 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -3193,7 +3193,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { // setUp() adds a secondary user for CALLER_USER_HANDLE. Remove it as otherwise the // feature is disabled because there are non-affiliated secondary users. getServices().removeUser(DpmMockContext.CALLER_USER_HANDLE); - when(getServices().iipConnectivityMetrics.registerNetdEventCallback(anyObject())) + when(getServices().iipConnectivityMetrics.addNetdEventCallback(anyInt(), anyObject())) .thenReturn(true); // No logs were retrieved so far. diff --git a/services/tests/servicestests/src/com/android/server/net/watchlist/HarmfulDigestsTests.java b/services/tests/servicestests/src/com/android/server/net/watchlist/HarmfulDigestsTests.java new file mode 100644 index 000000000000..a34f95eed040 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/net/watchlist/HarmfulDigestsTests.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net.watchlist; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.android.internal.util.HexDump; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; + +/** + * runtest frameworks-services -c com.android.server.net.watchlist.HarmfulDigestsTests + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class HarmfulDigestsTests { + + private static final byte[] TEST_DIGEST_1 = HexDump.hexStringToByteArray("AAAAAA"); + private static final byte[] TEST_DIGEST_2 = HexDump.hexStringToByteArray("BBBBBB"); + private static final byte[] TEST_DIGEST_3 = HexDump.hexStringToByteArray("AAAABB"); + private static final byte[] TEST_DIGEST_4 = HexDump.hexStringToByteArray("BBBBAA"); + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testHarmfulDigests_setAndContains() throws Exception { + HarmfulDigests harmfulDigests = new HarmfulDigests( + Arrays.asList(new byte[][] {TEST_DIGEST_1})); + assertTrue(harmfulDigests.contains(TEST_DIGEST_1)); + assertFalse(harmfulDigests.contains(TEST_DIGEST_2)); + assertFalse(harmfulDigests.contains(TEST_DIGEST_3)); + assertFalse(harmfulDigests.contains(TEST_DIGEST_4)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/net/watchlist/NetworkWatchlistServiceTests.java b/services/tests/servicestests/src/com/android/server/net/watchlist/NetworkWatchlistServiceTests.java new file mode 100644 index 000000000000..ccd3cdd3a20b --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/net/watchlist/NetworkWatchlistServiceTests.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net.watchlist; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.net.ConnectivityMetricsEvent; +import android.net.IIpConnectivityMetrics; +import android.net.INetdEventCallback; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Process; +import android.os.RemoteException; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.MediumTest; +import android.support.test.runner.AndroidJUnit4; + +import com.android.server.ServiceThread; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * runtest frameworks-services -c com.android.server.net.watchlist.NetworkWatchlistServiceTests + */ +@RunWith(AndroidJUnit4.class) +@MediumTest +public class NetworkWatchlistServiceTests { + + private static final long NETWOR_EVENT_TIMEOUT_SEC = 1; + private static final String TEST_HOST = "testhost.com"; + private static final String TEST_IP = "7.6.8.9"; + private static final String[] TEST_IPS = + new String[] {"1.2.3.4", "4.6.8.9", "2001:0db8:0001:0000:0000:0ab9:C0A8:0102"}; + + private static class TestHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case WatchlistLoggingHandler.LOG_WATCHLIST_EVENT_MSG: + onLogEvent(); + break; + case WatchlistLoggingHandler.REPORT_RECORDS_IF_NECESSARY_MSG: + onAggregateEvent(); + break; + default: + fail("Unexpected message: " + msg.what); + } + } + + public void onLogEvent() {} + public void onAggregateEvent() {} + } + + private static class TestIIpConnectivityMetrics implements IIpConnectivityMetrics { + + int counter = 0; + INetdEventCallback callback = null; + + @Override + public IBinder asBinder() { + return null; + } + + @Override + public int logEvent(ConnectivityMetricsEvent connectivityMetricsEvent) + throws RemoteException { + return 0; + } + + @Override + public boolean addNetdEventCallback(int callerType, INetdEventCallback callback) { + counter++; + this.callback = callback; + return true; + } + + @Override + public boolean removeNetdEventCallback(int callerType) { + counter--; + return true; + } + }; + + ServiceThread mHandlerThread; + WatchlistLoggingHandler mWatchlistHandler; + NetworkWatchlistService mWatchlistService; + + @Before + public void setUp() { + mHandlerThread = new ServiceThread("NetworkWatchlistServiceTests", + Process.THREAD_PRIORITY_BACKGROUND, /* allowIo */ false); + mHandlerThread.start(); + mWatchlistHandler = new WatchlistLoggingHandler(InstrumentationRegistry.getContext(), + mHandlerThread.getLooper()); + mWatchlistService = new NetworkWatchlistService(InstrumentationRegistry.getContext(), + mHandlerThread, mWatchlistHandler, null); + } + + @After + public void tearDown() { + mHandlerThread.quitSafely(); + } + + @Test + public void testStartStopWatchlistLogging() throws Exception { + TestIIpConnectivityMetrics connectivityMetrics = new TestIIpConnectivityMetrics() { + @Override + public boolean addNetdEventCallback(int callerType, INetdEventCallback callback) { + super.addNetdEventCallback(callerType, callback); + assertEquals(callerType, INetdEventCallback.CALLBACK_CALLER_NETWORK_WATCHLIST); + return true; + } + + @Override + public boolean removeNetdEventCallback(int callerType) { + super.removeNetdEventCallback(callerType); + assertEquals(callerType, INetdEventCallback.CALLBACK_CALLER_NETWORK_WATCHLIST); + return true; + } + }; + assertEquals(connectivityMetrics.counter, 0); + mWatchlistService.mIpConnectivityMetrics = connectivityMetrics; + assertTrue(mWatchlistService.startWatchlistLoggingImpl()); + assertEquals(connectivityMetrics.counter, 1); + assertTrue(mWatchlistService.startWatchlistLoggingImpl()); + assertEquals(connectivityMetrics.counter, 1); + assertTrue(mWatchlistService.stopWatchlistLoggingImpl()); + assertEquals(connectivityMetrics.counter, 0); + assertTrue(mWatchlistService.stopWatchlistLoggingImpl()); + assertEquals(connectivityMetrics.counter, 0); + assertTrue(mWatchlistService.startWatchlistLoggingImpl()); + assertEquals(connectivityMetrics.counter, 1); + assertTrue(mWatchlistService.stopWatchlistLoggingImpl()); + assertEquals(connectivityMetrics.counter, 0); + } + + @Test + public void testNetworkEvents() throws Exception { + TestIIpConnectivityMetrics connectivityMetrics = new TestIIpConnectivityMetrics(); + mWatchlistService.mIpConnectivityMetrics = connectivityMetrics; + assertTrue(mWatchlistService.startWatchlistLoggingImpl()); + + // Test DNS events + final CountDownLatch testDnsLatch = new CountDownLatch(1); + final Object[] dnsParams = new Object[3]; + final WatchlistLoggingHandler testDnsHandler = + new WatchlistLoggingHandler(InstrumentationRegistry.getContext(), + mHandlerThread.getLooper()) { + @Override + public void asyncNetworkEvent(String host, String[] ipAddresses, int uid) { + dnsParams[0] = host; + dnsParams[1] = ipAddresses; + dnsParams[2] = uid; + testDnsLatch.countDown(); + } + }; + mWatchlistService.mNetworkWatchlistHandler = testDnsHandler; + connectivityMetrics.callback.onDnsEvent(TEST_HOST, TEST_IPS, TEST_IPS.length, 123L, 456); + if (!testDnsLatch.await(NETWOR_EVENT_TIMEOUT_SEC, TimeUnit.SECONDS)) { + fail("Timed out waiting for network event"); + } + assertEquals(TEST_HOST, dnsParams[0]); + for (int i = 0; i < TEST_IPS.length; i++) { + assertEquals(TEST_IPS[i], ((String[])dnsParams[1])[i]); + } + assertEquals(456, dnsParams[2]); + + // Test connect events + final CountDownLatch testConnectLatch = new CountDownLatch(1); + final Object[] connectParams = new Object[3]; + final WatchlistLoggingHandler testConnectHandler = + new WatchlistLoggingHandler(InstrumentationRegistry.getContext(), + mHandlerThread.getLooper()) { + @Override + public void asyncNetworkEvent(String host, String[] ipAddresses, int uid) { + connectParams[0] = host; + connectParams[1] = ipAddresses; + connectParams[2] = uid; + testConnectLatch.countDown(); + } + }; + mWatchlistService.mNetworkWatchlistHandler = testConnectHandler; + connectivityMetrics.callback.onConnectEvent(TEST_IP, 80, 123L, 456); + if (!testConnectLatch.await(NETWOR_EVENT_TIMEOUT_SEC, TimeUnit.SECONDS)) { + fail("Timed out waiting for network event"); + } + assertNull(connectParams[0]); + assertEquals(1, ((String[]) connectParams[1]).length); + assertEquals(TEST_IP, ((String[]) connectParams[1])[0]); + assertEquals(456, connectParams[2]); + } +} diff --git a/services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistLoggingHandlerTests.java b/services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistLoggingHandlerTests.java new file mode 100644 index 000000000000..e356b13d01d5 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistLoggingHandlerTests.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net.watchlist; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; + +/** + * runtest frameworks-services -c com.android.server.net.watchlist.WatchlistLoggingHandlerTests + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class WatchlistLoggingHandlerTests { + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testWatchlistLoggingHandler_getAllSubDomains() throws Exception { + String[] subDomains = WatchlistLoggingHandler.getAllSubDomains("abc.def.gh.i.jkl.mm"); + assertTrue(Arrays.equals(subDomains, new String[] {"abc.def.gh.i.jkl.mm", + "def.gh.i.jkl.mm", "gh.i.jkl.mm", "i.jkl.mm", "jkl.mm", "mm"})); + subDomains = WatchlistLoggingHandler.getAllSubDomains(null); + assertNull(subDomains); + subDomains = WatchlistLoggingHandler.getAllSubDomains("jkl.mm"); + assertTrue(Arrays.equals(subDomains, new String[] {"jkl.mm", "mm"})); + subDomains = WatchlistLoggingHandler.getAllSubDomains("abc"); + assertTrue(Arrays.equals(subDomains, new String[] {"abc"})); + subDomains = WatchlistLoggingHandler.getAllSubDomains("jkl.mm."); + assertTrue(Arrays.equals(subDomains, new String[] {"jkl.mm.", "mm."})); + } +} diff --git a/services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistSettingsTests.java b/services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistSettingsTests.java new file mode 100644 index 000000000000..f3cb98078602 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/net/watchlist/WatchlistSettingsTests.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net.watchlist; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.android.internal.util.HexDump; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; + +/** + * runtest frameworks-services -c com.android.server.net.watchlist.WatchlistSettingsTests + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class WatchlistSettingsTests { + + private static final String TEST_XML_1 = "NetworkWatchlistTest/watchlist_settings_test1.xml"; + private static final String TEST_CC_DOMAIN = "test-cc-domain.com"; + private static final String TEST_CC_IP = "127.0.0.2"; + private static final String TEST_NOT_EXIST_CC_DOMAIN = "test-not-exist-cc-domain.com"; + private static final String TEST_NOT_EXIST_CC_IP = "1.2.3.4"; + private static final String TEST_SHA256_ONLY_DOMAIN = "test-cc-match-sha256-only.com"; + private static final String TEST_SHA256_ONLY_IP = "127.0.0.3"; + private static final String TEST_CRC32_ONLY_DOMAIN = "test-cc-match-crc32-only.com"; + private static final String TEST_CRC32_ONLY_IP = "127.0.0.4"; + + private static final String TEST_NEW_CC_DOMAIN = "test-new-cc-domain.com"; + private static final byte[] TEST_NEW_CC_DOMAIN_SHA256 = HexDump.hexStringToByteArray( + "B86F9D37425340B635F43D6BC2506630761ADA71F5E6BBDBCA4651C479F9FB43"); + private static final byte[] TEST_NEW_CC_DOMAIN_CRC32 = HexDump.hexStringToByteArray("76795BD3"); + + private static final String TEST_NEW_CC_IP = "1.1.1.2"; + private static final byte[] TEST_NEW_CC_IP_SHA256 = HexDump.hexStringToByteArray( + "721BAB5E313CF0CC76B10F9592F18B9D1B8996497501A3306A55B3AE9F1CC87C"); + private static final byte[] TEST_NEW_CC_IP_CRC32 = HexDump.hexStringToByteArray("940B8BEE"); + + private Context mContext; + private File mTestXmlFile; + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getContext(); + mTestXmlFile = new File(mContext.getFilesDir(), "test_watchlist_settings.xml"); + mTestXmlFile.delete(); + } + + @After + public void tearDown() throws Exception { + mTestXmlFile.delete(); + } + + @Test + public void testWatchlistSettings_parsing() throws Exception { + copyWatchlistSettingsXml(mContext, TEST_XML_1, mTestXmlFile); + WatchlistSettings settings = new WatchlistSettings(mTestXmlFile); + assertTrue(settings.containsDomain(TEST_CC_DOMAIN)); + assertTrue(settings.containsIp(TEST_CC_IP)); + assertFalse(settings.containsDomain(TEST_NOT_EXIST_CC_DOMAIN)); + assertFalse(settings.containsIp(TEST_NOT_EXIST_CC_IP)); + assertFalse(settings.containsDomain(TEST_SHA256_ONLY_DOMAIN)); + assertFalse(settings.containsIp(TEST_SHA256_ONLY_IP)); + assertFalse(settings.containsDomain(TEST_CRC32_ONLY_DOMAIN)); + assertFalse(settings.containsIp(TEST_CRC32_ONLY_IP)); + } + + @Test + public void testWatchlistSettings_writeSettingsToDisk() throws Exception { + copyWatchlistSettingsXml(mContext, TEST_XML_1, mTestXmlFile); + WatchlistSettings settings = new WatchlistSettings(mTestXmlFile); + settings.writeSettingsToDisk(Arrays.asList(TEST_NEW_CC_DOMAIN_CRC32), + Arrays.asList(TEST_NEW_CC_DOMAIN_SHA256), Arrays.asList(TEST_NEW_CC_IP_CRC32), + Arrays.asList(TEST_NEW_CC_IP_SHA256)); + // Ensure old watchlist is not in memory + assertFalse(settings.containsDomain(TEST_CC_DOMAIN)); + assertFalse(settings.containsIp(TEST_CC_IP)); + assertFalse(settings.containsDomain(TEST_NOT_EXIST_CC_DOMAIN)); + assertFalse(settings.containsIp(TEST_NOT_EXIST_CC_IP)); + assertFalse(settings.containsDomain(TEST_SHA256_ONLY_DOMAIN)); + assertFalse(settings.containsIp(TEST_SHA256_ONLY_IP)); + assertFalse(settings.containsDomain(TEST_CRC32_ONLY_DOMAIN)); + assertFalse(settings.containsIp(TEST_CRC32_ONLY_IP)); + // Ensure new watchlist is in memory + assertTrue(settings.containsDomain(TEST_NEW_CC_DOMAIN)); + assertTrue(settings.containsIp(TEST_NEW_CC_IP)); + // Reload settings from disk and test again + settings = new WatchlistSettings(mTestXmlFile); + // Ensure old watchlist is not in memory + assertFalse(settings.containsDomain(TEST_CC_DOMAIN)); + assertFalse(settings.containsIp(TEST_CC_IP)); + assertFalse(settings.containsDomain(TEST_NOT_EXIST_CC_DOMAIN)); + assertFalse(settings.containsIp(TEST_NOT_EXIST_CC_IP)); + assertFalse(settings.containsDomain(TEST_SHA256_ONLY_DOMAIN)); + assertFalse(settings.containsIp(TEST_SHA256_ONLY_IP)); + assertFalse(settings.containsDomain(TEST_CRC32_ONLY_DOMAIN)); + assertFalse(settings.containsIp(TEST_CRC32_ONLY_IP)); + // Ensure new watchlist is in memory + assertTrue(settings.containsDomain(TEST_NEW_CC_DOMAIN)); + assertTrue(settings.containsIp(TEST_NEW_CC_IP)); + } + + @Test + public void testWatchlistSettings_writeSettingsToMemory() throws Exception { + copyWatchlistSettingsXml(mContext, TEST_XML_1, mTestXmlFile); + WatchlistSettings settings = new WatchlistSettings(mTestXmlFile); + settings.writeSettingsToMemory(Arrays.asList(TEST_NEW_CC_DOMAIN_CRC32), + Arrays.asList(TEST_NEW_CC_DOMAIN_SHA256), Arrays.asList(TEST_NEW_CC_IP_CRC32), + Arrays.asList(TEST_NEW_CC_IP_SHA256)); + // Ensure old watchlist is not in memory + assertFalse(settings.containsDomain(TEST_CC_DOMAIN)); + assertFalse(settings.containsIp(TEST_CC_IP)); + assertFalse(settings.containsDomain(TEST_NOT_EXIST_CC_DOMAIN)); + assertFalse(settings.containsIp(TEST_NOT_EXIST_CC_IP)); + assertFalse(settings.containsDomain(TEST_SHA256_ONLY_DOMAIN)); + assertFalse(settings.containsIp(TEST_SHA256_ONLY_IP)); + assertFalse(settings.containsDomain(TEST_CRC32_ONLY_DOMAIN)); + assertFalse(settings.containsIp(TEST_CRC32_ONLY_IP)); + // Ensure new watchlist is in memory + assertTrue(settings.containsDomain(TEST_NEW_CC_DOMAIN)); + assertTrue(settings.containsIp(TEST_NEW_CC_IP)); + // Reload settings from disk and test again + settings = new WatchlistSettings(mTestXmlFile); + // Ensure old watchlist is in memory + assertTrue(settings.containsDomain(TEST_CC_DOMAIN)); + assertTrue(settings.containsIp(TEST_CC_IP)); + assertFalse(settings.containsDomain(TEST_NOT_EXIST_CC_DOMAIN)); + assertFalse(settings.containsIp(TEST_NOT_EXIST_CC_IP)); + assertFalse(settings.containsDomain(TEST_SHA256_ONLY_DOMAIN)); + assertFalse(settings.containsIp(TEST_SHA256_ONLY_IP)); + assertFalse(settings.containsDomain(TEST_CRC32_ONLY_DOMAIN)); + assertFalse(settings.containsIp(TEST_CRC32_ONLY_IP)); + // Ensure new watchlist is not in memory + assertFalse(settings.containsDomain(TEST_NEW_CC_DOMAIN)); + assertFalse(settings.containsIp(TEST_NEW_CC_IP));; + } + + private static void copyWatchlistSettingsXml(Context context, String xmlAsset, File outFile) + throws IOException { + writeToFile(outFile, readAsset(context, xmlAsset)); + + } + + private static String readAsset(Context context, String assetPath) throws IOException { + final StringBuilder sb = new StringBuilder(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader( + context.getResources().getAssets().open(assetPath)))) { + String line; + while ((line = br.readLine()) != null) { + sb.append(line); + sb.append(System.lineSeparator()); + } + } + return sb.toString(); + } + + private static void writeToFile(File path, String content) + throws IOException { + path.getParentFile().mkdirs(); + + try (FileWriter writer = new FileWriter(path)) { + writer.write(content); + } + } +} |