Merge "Create net-utils-framework-ipsec for IPsec module" into main
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 08129eb..5f8f0e3 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -500,7 +500,6 @@
@FlaggedApi("com.android.net.thread.flags.configuration_enabled") public final class ThreadConfiguration implements android.os.Parcelable {
method public int describeContents();
- method public boolean isDhcpv6PdEnabled();
method public boolean isNat64Enabled();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ThreadConfiguration> CREATOR;
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index fd73b29..16f32c4 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -73,7 +73,9 @@
new CertificateTransparencyInstaller());
}
- void registerReceiver() {
+ void initialize() {
+ mInstaller.addCompatibilityVersion(Config.COMPATIBILITY_VERSION);
+
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
@@ -185,7 +187,7 @@
String contentUrl = mDataStore.getProperty(Config.CONTENT_URL_PENDING);
String metadataUrl = mDataStore.getProperty(Config.METADATA_URL_PENDING);
try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
- success = mInstaller.install(inputStream, version);
+ success = mInstaller.install(Config.COMPATIBILITY_VERSION, inputStream, version);
} catch (IOException e) {
Log.e(TAG, "Could not install new content", e);
return;
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
index 914af06..0ae982d 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -43,7 +43,7 @@
void initialize() {
mDataStore.load();
- mCertificateTransparencyDownloader.registerReceiver();
+ mCertificateTransparencyDownloader.initialize();
DeviceConfig.addOnPropertiesChangedListener(
Config.NAMESPACE_NETWORK_SECURITY, Executors.newSingleThreadExecutor(), this);
if (Config.DEBUG) {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
index 82dcadf..4ca97eb 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
@@ -15,148 +15,78 @@
*/
package com.android.server.net.ct;
-import android.annotation.SuppressLint;
-import android.system.ErrnoException;
-import android.system.Os;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
-import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.Map;
/** Installer of CT log lists. */
public class CertificateTransparencyInstaller {
private static final String TAG = "CertificateTransparencyInstaller";
- private static final String CT_DIR_NAME = "/data/misc/keychain/ct/";
- static final String LOGS_DIR_PREFIX = "logs-";
- static final String LOGS_LIST_FILE_NAME = "log_list.json";
- static final String CURRENT_DIR_SYMLINK_NAME = "current";
+ private final Map<String, CompatibilityVersion> mCompatVersions = new HashMap<>();
- private final File mCertificateTransparencyDir;
- private final File mCurrentDirSymlink;
+ // The CT root directory.
+ private final File mRootDirectory;
- CertificateTransparencyInstaller(File certificateTransparencyDir) {
- mCertificateTransparencyDir = certificateTransparencyDir;
- mCurrentDirSymlink = new File(certificateTransparencyDir, CURRENT_DIR_SYMLINK_NAME);
+ public CertificateTransparencyInstaller(File rootDirectory) {
+ mRootDirectory = rootDirectory;
}
- CertificateTransparencyInstaller() {
- this(new File(CT_DIR_NAME));
+ public CertificateTransparencyInstaller(String rootDirectoryPath) {
+ this(new File(rootDirectoryPath));
+ }
+
+ public CertificateTransparencyInstaller() {
+ this(Config.CT_ROOT_DIRECTORY_PATH);
+ }
+
+ void addCompatibilityVersion(String versionName) {
+ removeCompatibilityVersion(versionName);
+ CompatibilityVersion newCompatVersion =
+ new CompatibilityVersion(new File(mRootDirectory, versionName));
+ mCompatVersions.put(versionName, newCompatVersion);
+ }
+
+ void removeCompatibilityVersion(String versionName) {
+ CompatibilityVersion compatVersion = mCompatVersions.remove(versionName);
+ if (compatVersion != null && !compatVersion.delete()) {
+ Log.w(TAG, "Could not delete compatibility version directory.");
+ }
+ }
+
+ CompatibilityVersion getCompatibilityVersion(String versionName) {
+ return mCompatVersions.get(versionName);
}
/**
* Install a new log list to use during SCT verification.
*
+ * @param compatibilityVersion the compatibility version of the new log list
* @param newContent an input stream providing the log list
- * @param version the version of the new log list
+ * @param version the minor version of the new log list
* @return true if the log list was installed successfully, false otherwise.
* @throws IOException if the list cannot be saved in the CT directory.
*/
- public boolean install(InputStream newContent, String version) throws IOException {
- // To support atomically replacing the old configuration directory with the new there's a
- // bunch of steps. We create a new directory with the logs and then do an atomic update of
- // the current symlink to point to the new directory.
- // 1. Ensure that the update dir exists and is readable.
- makeDir(mCertificateTransparencyDir);
-
- File newLogsDir = new File(mCertificateTransparencyDir, LOGS_DIR_PREFIX + version);
- // 2. Handle the corner case where the new directory already exists.
- if (newLogsDir.exists()) {
- // If the symlink has already been updated then the update died between steps 6 and 7
- // and so we cannot delete the directory since it is in use.
- if (newLogsDir.getCanonicalPath().equals(mCurrentDirSymlink.getCanonicalPath())) {
- deleteOldLogDirectories();
- return false;
- }
- // If the symlink has not been updated then the previous installation failed and this is
- // a re-attempt. Clean-up leftover files and try again.
- deleteContentsAndDir(newLogsDir);
- }
- try {
- // 3. Create /data/misc/keychain/ct/logs-<new_version>/ .
- makeDir(newLogsDir);
-
- // 4. Move the log list json file in logs-<new_version>/ .
- File logListFile = new File(newLogsDir, LOGS_LIST_FILE_NAME);
- if (Files.copy(newContent, logListFile.toPath()) == 0) {
- throw new IOException("The log list appears empty");
- }
- setWorldReadable(logListFile);
-
- // 5. Create temp symlink. We rename this to the target symlink to get an atomic update.
- File tempSymlink = new File(mCertificateTransparencyDir, "new_symlink");
- try {
- Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
- } catch (ErrnoException e) {
- throw new IOException("Failed to create symlink", e);
- }
-
- // 6. Update the symlink target, this is the actual update step.
- tempSymlink.renameTo(mCurrentDirSymlink.getAbsoluteFile());
- } catch (IOException | RuntimeException e) {
- deleteContentsAndDir(newLogsDir);
- throw e;
- }
- Log.i(TAG, "CT log directory updated to " + newLogsDir.getAbsolutePath());
- // 7. Cleanup
- deleteOldLogDirectories();
- return true;
- }
-
- private void makeDir(File dir) throws IOException {
- dir.mkdir();
- if (!dir.isDirectory()) {
- throw new IOException("Unable to make directory " + dir.getCanonicalPath());
- }
- setWorldReadable(dir);
- }
-
- // CT files and directories are readable by all apps.
- @SuppressLint("SetWorldReadable")
- private void setWorldReadable(File file) throws IOException {
- if (!file.setReadable(true, false)) {
- throw new IOException("Failed to set " + file.getCanonicalPath() + " readable");
- }
- }
-
- private void deleteOldLogDirectories() throws IOException {
- if (!mCertificateTransparencyDir.exists()) {
- return;
- }
- File currentTarget = mCurrentDirSymlink.getCanonicalFile();
- for (File file : mCertificateTransparencyDir.listFiles()) {
- if (!currentTarget.equals(file.getCanonicalFile())
- && file.getName().startsWith(LOGS_DIR_PREFIX)) {
- deleteContentsAndDir(file);
- }
- }
- }
-
- static boolean deleteContentsAndDir(File dir) {
- if (deleteContents(dir)) {
- return dir.delete();
- } else {
+ public boolean install(String compatibilityVersion, InputStream newContent, String version)
+ throws IOException {
+ CompatibilityVersion compatVersion = mCompatVersions.get(compatibilityVersion);
+ if (compatVersion == null) {
+ Log.e(TAG, "No compatibility version for " + compatibilityVersion);
return false;
}
- }
+ // Ensure root directory exists and is readable.
+ DirectoryUtils.makeDir(mRootDirectory);
- private static boolean deleteContents(File dir) {
- File[] files = dir.listFiles();
- boolean success = true;
- if (files != null) {
- for (File file : files) {
- if (file.isDirectory()) {
- success &= deleteContents(file);
- }
- if (!file.delete()) {
- Log.w(TAG, "Failed to delete " + file);
- success = false;
- }
- }
+ if (!compatVersion.install(newContent, version)) {
+ Log.e(TAG, "Failed to install logs for compatibility version " + compatibilityVersion);
+ return false;
}
- return success;
+ Log.i(TAG, "New logs installed at " + compatVersion.getLogsDir());
+ return true;
}
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
new file mode 100644
index 0000000..27488b5
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java
@@ -0,0 +1,135 @@
+/*
+ * 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.net.ct;
+
+import android.system.ErrnoException;
+import android.system.Os;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+
+/** Represents a compatibility version directory. */
+class CompatibilityVersion {
+
+ static final String LOGS_DIR_PREFIX = "logs-";
+ static final String LOGS_LIST_FILE_NAME = "log_list.json";
+
+ private static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current";
+
+ private final File mRootDirectory;
+ private final File mCurrentLogsDirSymlink;
+
+ private File mCurrentLogsDir = null;
+
+ CompatibilityVersion(File rootDirectory) {
+ mRootDirectory = rootDirectory;
+ mCurrentLogsDirSymlink = new File(mRootDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME);
+ }
+
+ /**
+ * Installs a log list within this compatibility version directory.
+ *
+ * @param newContent an input stream providing the log list
+ * @param version the version number of the log list
+ * @return true if the log list was installed successfully, false otherwise.
+ * @throws IOException if the list cannot be saved in the CT directory.
+ */
+ boolean install(InputStream newContent, String version) throws IOException {
+ // To support atomically replacing the old configuration directory with the new there's a
+ // bunch of steps. We create a new directory with the logs and then do an atomic update of
+ // the current symlink to point to the new directory.
+ // 1. Ensure that the root directory exists and is readable.
+ DirectoryUtils.makeDir(mRootDirectory);
+
+ File newLogsDir = new File(mRootDirectory, LOGS_DIR_PREFIX + version);
+ // 2. Handle the corner case where the new directory already exists.
+ if (newLogsDir.exists()) {
+ // If the symlink has already been updated then the update died between steps 6 and 7
+ // and so we cannot delete the directory since it is in use.
+ if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) {
+ deleteOldLogDirectories();
+ return false;
+ }
+ // If the symlink has not been updated then the previous installation failed and this is
+ // a re-attempt. Clean-up leftover files and try again.
+ DirectoryUtils.removeDir(newLogsDir);
+ }
+ try {
+ // 3. Create a new logs-<new_version>/ directory to store the new list.
+ DirectoryUtils.makeDir(newLogsDir);
+
+ // 4. Move the log list json file in logs-<new_version>/ .
+ File logListFile = new File(newLogsDir, LOGS_LIST_FILE_NAME);
+ if (Files.copy(newContent, logListFile.toPath()) == 0) {
+ throw new IOException("The log list appears empty");
+ }
+ DirectoryUtils.setWorldReadable(logListFile);
+
+ // 5. Create temp symlink. We rename this to the target symlink to get an atomic update.
+ File tempSymlink = new File(mRootDirectory, "new_symlink");
+ try {
+ Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
+ } catch (ErrnoException e) {
+ throw new IOException("Failed to create symlink", e);
+ }
+
+ // 6. Update the symlink target, this is the actual update step.
+ tempSymlink.renameTo(mCurrentLogsDirSymlink.getAbsoluteFile());
+ } catch (IOException | RuntimeException e) {
+ DirectoryUtils.removeDir(newLogsDir);
+ throw e;
+ }
+ // 7. Cleanup
+ mCurrentLogsDir = newLogsDir;
+ deleteOldLogDirectories();
+ return true;
+ }
+
+ File getRootDir() {
+ return mRootDirectory;
+ }
+
+ File getLogsDir() {
+ return mCurrentLogsDir;
+ }
+
+ File getLogsDirSymlink() {
+ return mCurrentLogsDirSymlink;
+ }
+
+ File getLogsFile() {
+ return new File(mCurrentLogsDir, LOGS_LIST_FILE_NAME);
+ }
+
+ boolean delete() {
+ return DirectoryUtils.removeDir(mRootDirectory);
+ }
+
+ private void deleteOldLogDirectories() throws IOException {
+ if (!mRootDirectory.exists()) {
+ return;
+ }
+ File currentTarget = mCurrentLogsDirSymlink.getCanonicalFile();
+ for (File file : mRootDirectory.listFiles()) {
+ if (!currentTarget.equals(file.getCanonicalFile())
+ && file.getName().startsWith(LOGS_DIR_PREFIX)) {
+ DirectoryUtils.removeDir(file);
+ }
+ }
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index 611a5c7..242f13a 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -33,6 +33,10 @@
private static final String PREFERENCES_FILE_NAME = "ct.preferences";
static final File PREFERENCES_FILE = new File(DEVICE_PROTECTED_DATA_DIR, PREFERENCES_FILE_NAME);
+ // CT directory
+ static final String CT_ROOT_DIRECTORY_PATH = "/data/misc/keychain/ct/";
+ static final String COMPATIBILITY_VERSION = "v1";
+
// Phenotype flags
static final String NAMESPACE_NETWORK_SECURITY = "network_security";
private static final String FLAGS_PREFIX = "CertificateTransparencyLogList__";
diff --git a/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java b/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java
new file mode 100644
index 0000000..e3b4124
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java
@@ -0,0 +1,69 @@
+/*
+ * 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.net.ct;
+
+import android.annotation.SuppressLint;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Utility class to manipulate CT directories. */
+class DirectoryUtils {
+
+ static void makeDir(File dir) throws IOException {
+ dir.mkdir();
+ if (!dir.isDirectory()) {
+ throw new IOException("Unable to make directory " + dir.getCanonicalPath());
+ }
+ setWorldReadable(dir);
+ }
+
+ // CT files and directories are readable by all apps.
+ @SuppressLint("SetWorldReadable")
+ static void setWorldReadable(File file) throws IOException {
+ if (!file.setReadable(true, false)) {
+ throw new IOException("Failed to set " + file.getCanonicalPath() + " readable");
+ }
+ }
+
+ static boolean removeDir(File dir) {
+ return deleteContentsAndDir(dir);
+ }
+
+ private static boolean deleteContentsAndDir(File dir) {
+ if (deleteContents(dir)) {
+ return dir.delete();
+ } else {
+ return false;
+ }
+ }
+
+ private static boolean deleteContents(File dir) {
+ File[] files = dir.listFiles();
+ boolean success = true;
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ success &= deleteContents(file);
+ }
+ if (!file.delete()) {
+ success = false;
+ }
+ }
+ }
+ return success;
+ }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 1aad028..df02446 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -159,7 +159,9 @@
Base64.getEncoder().encodeToString(mPublicKey.getEncoded()));
setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
- when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(true);
+ when(mCertificateTransparencyInstaller.install(
+ eq(Config.COMPATIBILITY_VERSION), any(), eq(version)))
+ .thenReturn(true);
assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
@@ -168,7 +170,8 @@
mCertificateTransparencyDownloader.onReceive(
mContext, makeDownloadCompleteIntent(contentId));
- verify(mCertificateTransparencyInstaller, times(1)).install(any(), eq(version));
+ verify(mCertificateTransparencyInstaller, times(1))
+ .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version));
assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isEqualTo(contentUri.toString());
assertThat(mDataStore.getProperty(Config.METADATA_URL)).isEqualTo(metadataUri.toString());
@@ -185,7 +188,9 @@
Uri metadataUri = Uri.fromFile(metadataFile);
setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
- when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(false);
+ when(mCertificateTransparencyInstaller.install(
+ eq(Config.COMPATIBILITY_VERSION), any(), eq(version)))
+ .thenReturn(false);
mCertificateTransparencyDownloader.onReceive(
mContext, makeDownloadCompleteIntent(contentId));
@@ -208,7 +213,8 @@
mCertificateTransparencyDownloader.onReceive(
mContext, makeDownloadCompleteIntent(contentId));
- verify(mCertificateTransparencyInstaller, never()).install(any(), eq(version));
+ verify(mCertificateTransparencyInstaller, never())
+ .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version));
assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
@@ -230,7 +236,8 @@
mCertificateTransparencyDownloader.onReceive(
mContext, makeDownloadCompleteIntent(contentId));
- verify(mCertificateTransparencyInstaller, never()).install(any(), eq(version));
+ verify(mCertificateTransparencyInstaller, never())
+ .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version));
assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
index bfb8bdf..50d3f23 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
@@ -17,11 +17,9 @@
import static com.google.common.truth.Truth.assertThat;
-import android.system.ErrnoException;
-import android.system.Os;
-
import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,98 +37,134 @@
@RunWith(JUnit4.class)
public class CertificateTransparencyInstallerTest {
+ private static final String TEST_VERSION = "test-v1";
+
private File mTestDir =
new File(
InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
"test-dir");
- private File mTestSymlink =
- new File(mTestDir, CertificateTransparencyInstaller.CURRENT_DIR_SYMLINK_NAME);
private CertificateTransparencyInstaller mCertificateTransparencyInstaller =
new CertificateTransparencyInstaller(mTestDir);
@Before
public void setUp() {
- CertificateTransparencyInstaller.deleteContentsAndDir(mTestDir);
+ mCertificateTransparencyInstaller.addCompatibilityVersion(TEST_VERSION);
+ }
+
+ @After
+ public void tearDown() {
+ mCertificateTransparencyInstaller.removeCompatibilityVersion(TEST_VERSION);
+ DirectoryUtils.removeDir(mTestDir);
+ }
+
+ @Test
+ public void testCompatibilityVersion_installSuccessful() throws IOException {
+ assertThat(mTestDir.mkdir()).isTrue();
+ String content = "i_am_compatible";
+ String version = "i_am_version";
+ CompatibilityVersion compatVersion =
+ mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
+
+ try (InputStream inputStream = asStream(content)) {
+ assertThat(compatVersion.install(inputStream, version)).isTrue();
+ }
+ File logsDir = compatVersion.getLogsDir();
+ assertThat(logsDir.exists()).isTrue();
+ assertThat(logsDir.isDirectory()).isTrue();
+ assertThat(logsDir.getAbsolutePath())
+ .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
+ File logsListFile = compatVersion.getLogsFile();
+ assertThat(logsListFile.exists()).isTrue();
+ assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
+ assertThat(readAsString(logsListFile)).isEqualTo(content);
+ File logsSymlink = compatVersion.getLogsDirSymlink();
+ assertThat(logsSymlink.exists()).isTrue();
+ assertThat(logsSymlink.isDirectory()).isTrue();
+ assertThat(logsSymlink.getAbsolutePath())
+ .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION + "/current");
+ assertThat(logsSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath());
+
+ assertThat(compatVersion.delete()).isTrue();
+ assertThat(logsDir.exists()).isFalse();
+ assertThat(logsSymlink.exists()).isFalse();
+ assertThat(logsListFile.exists()).isFalse();
+ }
+
+ @Test
+ public void testCompatibilityVersion_versionInstalledFailed() throws IOException {
+ assertThat(mTestDir.mkdir()).isTrue();
+
+ CompatibilityVersion compatVersion =
+ mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
+ File rootDir = compatVersion.getRootDir();
+ assertThat(rootDir.mkdir()).isTrue();
+
+ String existingVersion = "666";
+ File existingLogDir =
+ new File(rootDir, CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion);
+ assertThat(existingLogDir.mkdir()).isTrue();
+
+ String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through";
+ File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME);
+ assertThat(logsListFile.createNewFile()).isTrue();
+ writeToFile(logsListFile, existingContent);
+
+ String newContent = "i_am_the_real_content";
+ try (InputStream inputStream = asStream(newContent)) {
+ assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
+ }
+
+ assertThat(readAsString(logsListFile)).isEqualTo(newContent);
}
@Test
public void testCertificateTransparencyInstaller_installSuccessfully() throws IOException {
String content = "i_am_a_certificate_and_i_am_transparent";
String version = "666";
- boolean success = false;
try (InputStream inputStream = asStream(content)) {
- success = mCertificateTransparencyInstaller.install(inputStream, version);
+ assertThat(
+ mCertificateTransparencyInstaller.install(
+ TEST_VERSION, inputStream, version))
+ .isTrue();
}
- assertThat(success).isTrue();
assertThat(mTestDir.exists()).isTrue();
assertThat(mTestDir.isDirectory()).isTrue();
- assertThat(mTestSymlink.exists()).isTrue();
- assertThat(mTestSymlink.isDirectory()).isTrue();
-
- File logsDir =
- new File(mTestDir, CertificateTransparencyInstaller.LOGS_DIR_PREFIX + version);
+ CompatibilityVersion compatVersion =
+ mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
+ File logsDir = compatVersion.getLogsDir();
assertThat(logsDir.exists()).isTrue();
assertThat(logsDir.isDirectory()).isTrue();
- assertThat(mTestSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath());
-
- File logsListFile = new File(logsDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
+ assertThat(logsDir.getAbsolutePath())
+ .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION);
+ File logsListFile = compatVersion.getLogsFile();
assertThat(logsListFile.exists()).isTrue();
+ assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath());
assertThat(readAsString(logsListFile)).isEqualTo(content);
}
@Test
public void testCertificateTransparencyInstaller_versionIsAlreadyInstalled()
- throws IOException, ErrnoException {
+ throws IOException {
String existingVersion = "666";
String existingContent = "i_was_already_installed_successfully";
- File existingLogDir =
- new File(
- mTestDir,
- CertificateTransparencyInstaller.LOGS_DIR_PREFIX + existingVersion);
- assertThat(mTestDir.mkdir()).isTrue();
- assertThat(existingLogDir.mkdir()).isTrue();
- Os.symlink(existingLogDir.getCanonicalPath(), mTestSymlink.getCanonicalPath());
- File logsListFile =
- new File(existingLogDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
- logsListFile.createNewFile();
- writeToFile(logsListFile, existingContent);
- boolean success = false;
+ CompatibilityVersion compatVersion =
+ mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION);
+
+ DirectoryUtils.makeDir(mTestDir);
+ try (InputStream inputStream = asStream(existingContent)) {
+ assertThat(compatVersion.install(inputStream, existingVersion)).isTrue();
+ }
try (InputStream inputStream = asStream("i_will_be_ignored")) {
- success = mCertificateTransparencyInstaller.install(inputStream, existingVersion);
+ assertThat(
+ mCertificateTransparencyInstaller.install(
+ TEST_VERSION, inputStream, existingVersion))
+ .isFalse();
}
- assertThat(success).isFalse();
- assertThat(readAsString(logsListFile)).isEqualTo(existingContent);
- }
-
- @Test
- public void testCertificateTransparencyInstaller_versionInstalledFailed()
- throws IOException, ErrnoException {
- String existingVersion = "666";
- String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through";
- String newContent = "i_am_the_real_certificate";
- File existingLogDir =
- new File(
- mTestDir,
- CertificateTransparencyInstaller.LOGS_DIR_PREFIX + existingVersion);
- assertThat(mTestDir.mkdir()).isTrue();
- assertThat(existingLogDir.mkdir()).isTrue();
- File logsListFile =
- new File(existingLogDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
- logsListFile.createNewFile();
- writeToFile(logsListFile, existingContent);
- boolean success = false;
-
- try (InputStream inputStream = asStream(newContent)) {
- success = mCertificateTransparencyInstaller.install(inputStream, existingVersion);
- }
-
- assertThat(success).isTrue();
- assertThat(mTestSymlink.getCanonicalPath()).isEqualTo(existingLogDir.getCanonicalPath());
- assertThat(readAsString(logsListFile)).isEqualTo(newContent);
+ assertThat(readAsString(compatVersion.getLogsFile())).isEqualTo(existingContent);
}
private static InputStream asStream(String string) throws IOException {
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 665e6f9..e503312 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -6003,12 +6003,10 @@
// TODO : The only way out of this is to diff old defaults and new defaults, and only
// remove ranges for those requests that won't have a replacement
final NetworkAgentInfo satisfier = nri.getSatisfier();
- if (null != satisfier && !satisfier.isDestroyed()) {
+ if (null != satisfier) {
try {
- mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
- satisfier.network.getNetId(),
- toUidRangeStableParcels(nri.getUids()),
- nri.getPreferenceOrderForNetd()));
+ modifyNetworkUidRanges(false /* add */, satisfier, nri.getUids(),
+ nri.getPreferenceOrderForNetd());
} catch (RemoteException e) {
loge("Exception setting network preference default network", e);
}
@@ -10267,8 +10265,7 @@
return stableRanges;
}
- private void maybeCloseSockets(NetworkAgentInfo nai, Set<UidRange> ranges,
- UidRangeParcel[] uidRangeParcels, int[] exemptUids) {
+ private void maybeCloseSockets(NetworkAgentInfo nai, Set<UidRange> ranges, int[] exemptUids) {
if (nai.isVPN() && !nai.networkAgentConfig.allowBypass) {
try {
if (mDeps.isAtLeastU()) {
@@ -10278,7 +10275,7 @@
}
mDeps.destroyLiveTcpSockets(UidRange.toIntRanges(ranges), exemptUidSet);
} else {
- mNetd.socketDestroy(uidRangeParcels, exemptUids);
+ mNetd.socketDestroy(toUidRangeStableParcels(ranges), exemptUids);
}
} catch (Exception e) {
loge("Exception in socket destroy: ", e);
@@ -10286,6 +10283,28 @@
}
}
+ private void modifyNetworkUidRanges(boolean add, NetworkAgentInfo nai, UidRangeParcel[] ranges,
+ int preference) throws RemoteException {
+ // UID ranges can be added or removed to a network that has already been destroyed (e.g., if
+ // the network disconnects, or a a multilayer request is filed after
+ // unregisterAfterReplacement is called).
+ if (nai.isDestroyed()) {
+ return;
+ }
+ final NativeUidRangeConfig config = new NativeUidRangeConfig(nai.network.netId,
+ ranges, preference);
+ if (add) {
+ mNetd.networkAddUidRangesParcel(config);
+ } else {
+ mNetd.networkRemoveUidRangesParcel(config);
+ }
+ }
+
+ private void modifyNetworkUidRanges(boolean add, NetworkAgentInfo nai, Set<UidRange> uidRanges,
+ int preference) throws RemoteException {
+ modifyNetworkUidRanges(add, nai, toUidRangeStableParcels(uidRanges), preference);
+ }
+
private void updateVpnUidRanges(boolean add, NetworkAgentInfo nai, Set<UidRange> uidRanges) {
int[] exemptUids = new int[2];
// TODO: Excluding VPN_UID is necessary in order to not to kill the TCP connection used
@@ -10293,24 +10312,17 @@
// starting a legacy VPN, and remove VPN_UID here. (b/176542831)
exemptUids[0] = VPN_UID;
exemptUids[1] = nai.networkCapabilities.getOwnerUid();
- UidRangeParcel[] ranges = toUidRangeStableParcels(uidRanges);
// Close sockets before modifying uid ranges so that RST packets can reach to the server.
- maybeCloseSockets(nai, uidRanges, ranges, exemptUids);
+ maybeCloseSockets(nai, uidRanges, exemptUids);
try {
- if (add) {
- mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
- nai.network.netId, ranges, PREFERENCE_ORDER_VPN));
- } else {
- mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
- nai.network.netId, ranges, PREFERENCE_ORDER_VPN));
- }
+ modifyNetworkUidRanges(add, nai, uidRanges, PREFERENCE_ORDER_VPN);
} catch (Exception e) {
loge("Exception while " + (add ? "adding" : "removing") + " uid ranges " + uidRanges +
" on netId " + nai.network.netId + ". " + e);
}
// Close sockets that established connection while requesting netd.
- maybeCloseSockets(nai, uidRanges, ranges, exemptUids);
+ maybeCloseSockets(nai, uidRanges, exemptUids);
}
private boolean isProxySetOnAnyDefaultNetwork() {
@@ -10424,16 +10436,12 @@
toAdd.removeAll(prevUids);
try {
if (!toAdd.isEmpty()) {
- mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
- nai.network.netId,
- intsToUidRangeStableParcels(toAdd),
- PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT));
+ modifyNetworkUidRanges(true /* add */, nai, intsToUidRangeStableParcels(toAdd),
+ PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT);
}
if (!toRemove.isEmpty()) {
- mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
- nai.network.netId,
- intsToUidRangeStableParcels(toRemove),
- PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT));
+ modifyNetworkUidRanges(false /* add */, nai, intsToUidRangeStableParcels(toRemove),
+ PREFERENCE_ORDER_IRRELEVANT_BECAUSE_NOT_DEFAULT);
}
} catch (ServiceSpecificException e) {
// Has the interface disappeared since the network was built ?
@@ -10788,16 +10796,12 @@
+ " any applications to set as the default." + nri);
}
if (null != newDefaultNetwork) {
- mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
- newDefaultNetwork.network.getNetId(),
- toUidRangeStableParcels(nri.getUids()),
- nri.getPreferenceOrderForNetd()));
+ modifyNetworkUidRanges(true /* add */, newDefaultNetwork, nri.getUids(),
+ nri.getPreferenceOrderForNetd());
}
if (null != oldDefaultNetwork) {
- mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
- oldDefaultNetwork.network.getNetId(),
- toUidRangeStableParcels(nri.getUids()),
- nri.getPreferenceOrderForNetd()));
+ modifyNetworkUidRanges(false /* add */, oldDefaultNetwork, nri.getUids(),
+ nri.getPreferenceOrderForNetd());
}
} catch (RemoteException | ServiceSpecificException e) {
loge("Exception setting app default network", e);
diff --git a/staticlibs/tests/unit/host/python/assert_utils_test.py b/staticlibs/tests/unit/host/python/assert_utils_test.py
index 7a33373..1d85a12 100644
--- a/staticlibs/tests/unit/host/python/assert_utils_test.py
+++ b/staticlibs/tests/unit/host/python/assert_utils_test.py
@@ -14,7 +14,9 @@
from mobly import asserts
from mobly import base_test
-from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError, expect_with_retry
+from net_tests_utils.host.python.assert_utils import (
+ UnexpectedBehaviorError, UnexpectedExceptionError, expect_with_retry, expect_throws
+)
class TestAssertUtils(base_test.BaseTestClass):
@@ -92,3 +94,22 @@
retry_interval_sec=0,
)
asserts.assert_true(retry_action_called, "retry_action not called.")
+
+ def test_expect_exception_throws(self):
+ def raise_unexpected_behavior_error():
+ raise UnexpectedBehaviorError()
+
+ expect_throws(raise_unexpected_behavior_error, UnexpectedBehaviorError)
+
+ def test_unexpect_exception_throws(self):
+ def raise_value_error():
+ raise ValueError()
+
+ with asserts.assert_raises(UnexpectedExceptionError):
+ expect_throws(raise_value_error, UnexpectedBehaviorError)
+
+ def test_no_exception_throws(self):
+ def raise_no_error():
+ return
+
+ expect_throws(raise_no_error, UnexpectedBehaviorError)
\ No newline at end of file
diff --git a/staticlibs/testutils/host/python/assert_utils.py b/staticlibs/testutils/host/python/assert_utils.py
index da1bb9e..40094a2 100644
--- a/staticlibs/testutils/host/python/assert_utils.py
+++ b/staticlibs/testutils/host/python/assert_utils.py
@@ -19,6 +19,8 @@
class UnexpectedBehaviorError(Exception):
"""Raised when there is an unexpected behavior during applying a procedure."""
+class UnexpectedExceptionError(Exception):
+ """Raised when there is an unexpected exception throws during applying a procedure"""
def expect_with_retry(
predicate: Callable[[], bool],
@@ -41,3 +43,17 @@
raise UnexpectedBehaviorError(
"Predicate didn't become true after " + str(max_retries) + " retries."
)
+
+def expect_throws(runnable: callable, exception_class) -> None:
+ try:
+ runnable()
+ raise UnexpectedBehaviorError("Expected an exception, but none was thrown")
+ except exception_class:
+ pass
+ except UnexpectedBehaviorError as e:
+ raise e
+ except Exception as e:
+ raise UnexpectedExceptionError(
+ f"Expected exception of type {exception_class.__name__}, "
+ f"but got {type(e).__name__}: {e}"
+ )
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
index 5ca7fcc..58420c0 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
@@ -163,19 +163,36 @@
doTestSatelliteNeverBecomeDefaultNetwork(restricted = false)
}
- private fun doTestUnregisterAfterReplacementSatisfier(destroyed: Boolean) {
+ private fun doTestUnregisterAfterReplacementSatisfier(destroyBeforeRequest: Boolean = false,
+ destroyAfterRequest: Boolean = false) {
val satelliteAgent = createSatelliteAgent("satellite0")
satelliteAgent.connect()
+ if (destroyBeforeRequest) {
+ satelliteAgent.unregisterAfterReplacement(timeoutMs = 5000)
+ }
+
val uids = setOf(TEST_PACKAGE_UID)
updateSatelliteNetworkFallbackUids(uids)
- if (destroyed) {
+ if (destroyBeforeRequest) {
+ verify(netd, never()).networkAddUidRangesParcel(any())
+ } else {
+ verify(netd).networkAddUidRangesParcel(
+ NativeUidRangeConfig(
+ satelliteAgent.network.netId,
+ toUidRangeStableParcels(uidRangesForUids(uids)),
+ PREFERENCE_ORDER_SATELLITE_FALLBACK
+ )
+ )
+ }
+
+ if (destroyAfterRequest) {
satelliteAgent.unregisterAfterReplacement(timeoutMs = 5000)
}
updateSatelliteNetworkFallbackUids(setOf())
- if (destroyed) {
+ if (destroyBeforeRequest || destroyAfterRequest) {
// If the network is already destroyed, networkRemoveUidRangesParcel should not be
// called.
verify(netd, never()).networkRemoveUidRangesParcel(any())
@@ -191,13 +208,18 @@
}
@Test
- fun testUnregisterAfterReplacementSatisfier_destroyed() {
- doTestUnregisterAfterReplacementSatisfier(destroyed = true)
+ fun testUnregisterAfterReplacementSatisfier_destroyBeforeRequest() {
+ doTestUnregisterAfterReplacementSatisfier(destroyBeforeRequest = true)
+ }
+
+ @Test
+ fun testUnregisterAfterReplacementSatisfier_destroyAfterRequest() {
+ doTestUnregisterAfterReplacementSatisfier(destroyAfterRequest = true)
}
@Test
fun testUnregisterAfterReplacementSatisfier_notDestroyed() {
- doTestUnregisterAfterReplacementSatisfier(destroyed = false)
+ doTestUnregisterAfterReplacementSatisfier()
}
private fun assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(uids: Set<Int>) {
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
index e6fa1ef..edb5021 100644
--- a/thread/framework/java/android/net/thread/ThreadConfiguration.java
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -61,7 +61,11 @@
return mNat64Enabled;
}
- /** Returns {@code true} if DHCPv6 Prefix Delegation is enabled. */
+ /**
+ * Returns {@code true} if DHCPv6 Prefix Delegation is enabled.
+ *
+ * @hide
+ */
public boolean isDhcpv6PdEnabled() {
return mDhcpv6PdEnabled;
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 653b2fb..d5d24ac 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -78,6 +78,8 @@
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.LocalNetworkConfig;
import android.net.LocalNetworkInfo;
@@ -120,6 +122,8 @@
import com.android.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.RoutingCoordinatorManager;
+import com.android.net.module.util.IIpv4PrefixRequest;
import com.android.net.module.util.SharedLog;
import com.android.server.ServiceManagerWrapper;
import com.android.server.connectivity.ConnectivityResources;
@@ -193,10 +197,12 @@
private final NetworkProvider mNetworkProvider;
private final Supplier<IOtDaemon> mOtDaemonSupplier;
private final ConnectivityManager mConnectivityManager;
+ private final RoutingCoordinatorManager mRoutingCoordinatorManager;
private final TunInterfaceController mTunIfController;
private final InfraInterfaceController mInfraIfController;
private final NsdPublisher mNsdPublisher;
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
+ private final Nat64CidrController mNat64CidrController = new Nat64CidrController();
private final ConnectivityResources mResources;
private final Supplier<String> mCountryCodeSupplier;
private final Map<IConfigurationReceiver, IBinder.DeathRecipient> mConfigurationReceivers =
@@ -229,6 +235,7 @@
NetworkProvider networkProvider,
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
+ RoutingCoordinatorManager routingCoordinatorManager,
TunInterfaceController tunIfController,
InfraInterfaceController infraIfController,
ThreadPersistentSettings persistentSettings,
@@ -242,6 +249,7 @@
mNetworkProvider = networkProvider;
mOtDaemonSupplier = otDaemonSupplier;
mConnectivityManager = connectivityManager;
+ mRoutingCoordinatorManager = routingCoordinatorManager;
mTunIfController = tunIfController;
mInfraIfController = infraIfController;
mUpstreamNetworkRequest = newUpstreamNetworkRequest();
@@ -266,13 +274,19 @@
NetworkProvider networkProvider =
new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
Map<Network, LinkProperties> networkToLinkProperties = new HashMap<>();
+ final ConnectivityManager connectivityManager =
+ context.getSystemService(ConnectivityManager.class);
+ final RoutingCoordinatorManager routingCoordinatorManager =
+ new RoutingCoordinatorManager(
+ context, connectivityManager.getRoutingCoordinatorService());
return new ThreadNetworkControllerService(
context,
handler,
networkProvider,
() -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
- context.getSystemService(ConnectivityManager.class),
+ connectivityManager,
+ routingCoordinatorManager,
new TunInterfaceController(TUN_IF_NAME),
new InfraInterfaceController(),
persistentSettings,
@@ -351,6 +365,7 @@
mCountryCodeSupplier.get());
otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
+ mHandler.post(mNat64CidrController::maybeUpdateNat64Cidr);
return mOtDaemon;
}
@@ -589,6 +604,7 @@
} catch (RemoteException | ThreadNetworkException e) {
LOG.e("otDaemon.setConfiguration failed. Config: " + configuration, e);
}
+ mNat64CidrController.maybeUpdateNat64Cidr();
}
private static OtDaemonConfiguration newOtDaemonConfig(
@@ -833,7 +849,7 @@
mHandler.getLooper(),
LOG.getTag(),
netCaps,
- mTunIfController.getLinkProperties(),
+ getTunIfLinkProperties(),
newLocalNetworkConfig(),
score,
new NetworkAgentConfig.Builder().build(),
@@ -1391,9 +1407,7 @@
// The OT daemon can send link property updates before the networkAgent is
// registered
- if (mNetworkAgent != null) {
- mNetworkAgent.sendLinkProperties(mTunIfController.getLinkProperties());
- }
+ maybeSendLinkProperties();
}
private void handlePrefixChanged(List<OnMeshPrefixConfig> onMeshPrefixConfigList) {
@@ -1403,9 +1417,18 @@
// The OT daemon can send link property updates before the networkAgent is
// registered
- if (mNetworkAgent != null) {
- mNetworkAgent.sendLinkProperties(mTunIfController.getLinkProperties());
+ maybeSendLinkProperties();
+ }
+
+ private void maybeSendLinkProperties() {
+ if (mNetworkAgent == null) {
+ return;
}
+ mNetworkAgent.sendLinkProperties(getTunIfLinkProperties());
+ }
+
+ private LinkProperties getTunIfLinkProperties() {
+ return mTunIfController.getLinkPropertiesWithNat64Cidr(mNat64CidrController.mNat64Cidr);
}
@RequiresPermission(
@@ -1851,4 +1874,64 @@
mHandler.post(() -> handlePrefixChanged(onMeshPrefixConfigList));
}
}
+
+ private final class Nat64CidrController extends IIpv4PrefixRequest.Stub {
+ private static final int RETRY_DELAY_ON_FAILURE_MILLIS = 600_000; // 10 minutes
+
+ @Nullable private LinkAddress mNat64Cidr;
+
+ @Override
+ public void onIpv4PrefixConflict(IpPrefix prefix) {
+ mHandler.post(() -> onIpv4PrefixConflictInternal(prefix));
+ }
+
+ private void onIpv4PrefixConflictInternal(IpPrefix prefix) {
+ checkOnHandlerThread();
+
+ LOG.i("Conflict on NAT64 CIDR: " + prefix);
+ maybeReleaseNat64Cidr();
+ maybeUpdateNat64Cidr();
+ }
+
+ public void maybeUpdateNat64Cidr() {
+ checkOnHandlerThread();
+
+ if (mPersistentSettings.getConfiguration().isNat64Enabled()) {
+ maybeRequestNat64Cidr();
+ } else {
+ maybeReleaseNat64Cidr();
+ }
+ try {
+ getOtDaemon()
+ .setNat64Cidr(
+ mNat64Cidr == null ? null : mNat64Cidr.toString(),
+ new LoggingOtStatusReceiver("setNat64Cidr"));
+ } catch (RemoteException | ThreadNetworkException e) {
+ LOG.e("Failed to set NAT64 CIDR at otd-daemon", e);
+ }
+ maybeSendLinkProperties();
+ }
+
+ private void maybeRequestNat64Cidr() {
+ if (mNat64Cidr != null) {
+ return;
+ }
+ final LinkAddress downstreamAddress =
+ mRoutingCoordinatorManager.requestDownstreamAddress(this);
+ if (downstreamAddress == null) {
+ mHandler.postDelayed(() -> maybeUpdateNat64Cidr(), RETRY_DELAY_ON_FAILURE_MILLIS);
+ }
+ mNat64Cidr = downstreamAddress;
+ LOG.i("Allocated NAT64 CIDR: " + mNat64Cidr);
+ }
+
+ private void maybeReleaseNat64Cidr() {
+ if (mNat64Cidr == null) {
+ return;
+ }
+ LOG.i("Released NAT64 CIDR: " + mNat64Cidr);
+ mNat64Cidr = null;
+ mRoutingCoordinatorManager.releaseDownstream(this);
+ }
+ }
}
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
index 3bff9c6..520a434 100644
--- a/thread/service/java/com/android/server/thread/TunInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -92,10 +92,21 @@
}
/** Returns link properties of the Thread TUN interface. */
- public LinkProperties getLinkProperties() {
+ private LinkProperties getLinkProperties() {
return new LinkProperties(mLinkProperties);
}
+ /** Returns link properties of the Thread TUN interface with the given NAT64 CIDR. */
+ // TODO: manage the NAT64 CIDR in the TunInterfaceController
+ public LinkProperties getLinkPropertiesWithNat64Cidr(@Nullable LinkAddress nat64Cidr) {
+ final LinkProperties lp = getLinkProperties();
+ if (nat64Cidr != null) {
+ lp.addLinkAddress(nat64Cidr);
+ lp.addRoute(getRouteForAddress(nat64Cidr));
+ }
+ return lp;
+ }
+
/**
* Creates the tunnel interface.
*
@@ -148,6 +159,9 @@
/** Adds a new address to the interface. */
public void addAddress(LinkAddress address) {
+ if (!(address.getAddress() instanceof Inet6Address)) {
+ return;
+ }
LOG.v("Adding address " + address + " with flags: " + address.getFlags());
long preferredLifetimeSeconds;
@@ -172,7 +186,7 @@
(address.getExpirationTime() - SystemClock.elapsedRealtime()) / 1000L,
0L);
}
-
+ // Only apply to Ipv6 address
if (!NetlinkUtils.sendRtmNewAddressRequest(
Os.if_nametoindex(mIfName),
address.getAddress(),
@@ -190,6 +204,9 @@
/** Removes an address from the interface. */
public void removeAddress(LinkAddress address) {
+ if (!(address.getAddress() instanceof Inet6Address)) {
+ return;
+ }
LOG.v("Removing address " + address);
// Intentionally update the mLinkProperties before send netlink message because the
@@ -197,6 +214,7 @@
// when the netlink request below fails
mLinkProperties.removeLinkAddress(address);
mLinkProperties.removeRoute(getRouteForAddress(address));
+ // Only apply to Ipv6 address
if (!NetlinkUtils.sendRtmDelAddressRequest(
Os.if_nametoindex(mIfName),
(Inet6Address) address.getAddress(),
diff --git a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
index f8e92f0..f6dd6b9 100644
--- a/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
+++ b/thread/tests/integration/src/android/net/thread/BorderRoutingTest.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
+import static android.net.thread.utils.IntegrationTestUtils.buildIcmpv4EchoReply;
import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv4Packet;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
@@ -77,9 +78,11 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
+import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@@ -101,7 +104,6 @@
(Inet6Address) parseNumericAddress("ff03::1234");
private static final Inet4Address IPV4_SERVER_ADDR =
(Inet4Address) parseNumericAddress("8.8.8.8");
- private static final String NAT64_CIDR = "192.168.255.0/24";
private static final IpPrefix DHCP6_PD_PREFIX = new IpPrefix("2001:db8::/64");
private static final IpPrefix AIL_NAT64_PREFIX = new IpPrefix("2001:db8:1234::/96");
private static final Inet6Address AIL_NAT64_SYNTHESIZED_SERVER_ADDR =
@@ -647,16 +649,27 @@
}
@Test
- public void nat64_threadDevicePingIpv4InfraDevice_outboundPacketIsForwarded() throws Exception {
+ public void nat64_threadDevicePingIpv4InfraDevice_outboundPacketIsForwardedAndReplyIsReceived()
+ throws Exception {
FullThreadDevice ftd = mFtds.get(0);
joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
- mOtCtl.setNat64Cidr(NAT64_CIDR);
mController.setNat64EnabledAndWait(true);
waitFor(() -> mOtCtl.hasNat64PrefixInNetdata(), UPDATE_NAT64_PREFIX_TIMEOUT);
+ Thread echoReplyThread = new Thread(() -> respondToEchoRequestOnce(IPV4_SERVER_ADDR));
+ echoReplyThread.start();
- ftd.ping(IPV4_SERVER_ADDR);
+ assertThat(ftd.ping(IPV4_SERVER_ADDR, 1 /* count */)).isEqualTo(1);
- assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMP_ECHO, null, IPV4_SERVER_ADDR));
+ echoReplyThread.join();
+ }
+
+ private void respondToEchoRequestOnce(Inet4Address dstAddress) {
+ byte[] echoRequest = pollForIcmpPacketOnInfraNetwork(ICMP_ECHO, null, dstAddress);
+ assertNotNull(echoRequest);
+ try {
+ mInfraNetworkReader.sendResponse(buildIcmpv4EchoReply(ByteBuffer.wrap(echoRequest)));
+ } catch (IOException ignored) {
+ }
}
@Test
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index d903636..dc2a9c9 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -38,9 +38,15 @@
import android.os.Handler
import android.os.SystemClock
import android.system.OsConstants
+import android.system.OsConstants.IPPROTO_ICMP
import androidx.test.core.app.ApplicationProvider
import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
+import com.android.net.module.util.IpUtils
import com.android.net.module.util.NetworkStackConstants
+import com.android.net.module.util.NetworkStackConstants.ICMP_CHECKSUM_OFFSET
+import com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET
+import com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV4_LENGTH_OFFSET
import com.android.net.module.util.Struct
import com.android.net.module.util.structs.Icmpv4Header
import com.android.net.module.util.structs.Icmpv6Header
@@ -307,6 +313,73 @@
return null
}
+ /** Builds an ICMPv4 Echo Reply packet to respond to the given ICMPv4 Echo Request packet. */
+ @JvmStatic
+ fun buildIcmpv4EchoReply(request: ByteBuffer): ByteBuffer? {
+ val requestIpv4Header = Struct.parse(Ipv4Header::class.java, request) ?: return null
+ val requestIcmpv4Header = Struct.parse(Icmpv4Header::class.java, request) ?: return null
+
+ val id = request.getShort()
+ val seq = request.getShort()
+
+ val payload = ByteBuffer.allocate(4 + request.limit() - request.position())
+ payload.putShort(id)
+ payload.putShort(seq)
+ payload.put(request)
+ payload.rewind()
+
+ val ipv4HeaderLen = Struct.getSize(Ipv4Header::class.java)
+ val Icmpv4HeaderLen = Struct.getSize(Icmpv4Header::class.java)
+ val payloadLen = payload.limit();
+
+ val reply = ByteBuffer.allocate(ipv4HeaderLen + Icmpv4HeaderLen + payloadLen)
+
+ // IPv4 header
+ val replyIpv4Header = Ipv4Header(
+ 0 /* TYPE OF SERVICE */,
+ 0.toShort().toInt()/* totalLength, calculate later */,
+ requestIpv4Header.id,
+ requestIpv4Header.flagsAndFragmentOffset,
+ 0x40 /* ttl */,
+ IPPROTO_ICMP.toByte(),
+ 0.toShort()/* checksum, calculate later */,
+ requestIpv4Header.dstIp /* srcIp */,
+ requestIpv4Header.srcIp /* dstIp */
+ )
+ replyIpv4Header.writeToByteBuffer(reply)
+
+ // ICMPv4 header
+ val replyIcmpv4Header = Icmpv4Header(
+ 0 /* type, ICMP_ECHOREPLY */,
+ requestIcmpv4Header.code,
+ 0.toShort() /* checksum, calculate later */
+ )
+ replyIcmpv4Header.writeToByteBuffer(reply)
+
+ // Payload
+ reply.put(payload)
+ reply.flip()
+
+ // Populate the IPv4 totalLength field.
+ reply.putShort(
+ IPV4_LENGTH_OFFSET, (ipv4HeaderLen + Icmpv4HeaderLen + payloadLen).toShort()
+ )
+
+ // Populate the IPv4 header checksum field.
+ reply.putShort(
+ IPV4_CHECKSUM_OFFSET, IpUtils.ipChecksum(reply, 0 /* headerOffset */)
+ )
+
+ // Populate the ICMP checksum field.
+ reply.putShort(
+ IPV4_HEADER_MIN_LEN + ICMP_CHECKSUM_OFFSET, IpUtils.icmpChecksum(
+ reply, IPV4_HEADER_MIN_LEN, Icmpv4HeaderLen + payloadLen
+ )
+ )
+
+ return reply
+ }
+
/** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */
@JvmStatic
fun getRaPios(raMsg: ByteArray?): List<PrefixInformationOption> {
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 7ac404f..e188491 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -44,6 +44,8 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNotNull;
+import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
@@ -64,6 +66,7 @@
import android.content.Intent;
import android.content.res.Resources;
import android.net.ConnectivityManager;
+import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkAgent;
@@ -91,9 +94,12 @@
import com.android.connectivity.resources.R;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.net.module.util.RoutingCoordinatorManager;
import com.android.server.connectivity.ConnectivityResources;
import com.android.server.thread.openthread.DnsTxtAttribute;
+import com.android.server.thread.openthread.IOtStatusReceiver;
import com.android.server.thread.openthread.MeshcopTxtAttributes;
+import com.android.server.thread.openthread.OtDaemonConfiguration;
import com.android.server.thread.openthread.testing.FakeOtDaemon;
import org.junit.Before;
@@ -164,8 +170,10 @@
private static final byte[] TEST_VENDOR_OUI_BYTES = new byte[] {(byte) 0xAC, (byte) 0xDE, 0x48};
private static final String TEST_VENDOR_NAME = "test vendor";
private static final String TEST_MODEL_NAME = "test model";
+ private static final LinkAddress TEST_NAT64_CIDR = new LinkAddress("192.168.255.0/24");
@Mock private ConnectivityManager mMockConnectivityManager;
+ @Mock private RoutingCoordinatorManager mMockRoutingCoordinatorManager;
@Mock private NetworkAgent mMockNetworkAgent;
@Mock private TunInterfaceController mMockTunIfController;
@Mock private ParcelFileDescriptor mMockTunFd;
@@ -208,7 +216,10 @@
NetworkProvider networkProvider =
new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider");
- mFakeOtDaemon = new FakeOtDaemon(handler);
+ when(mMockRoutingCoordinatorManager.requestDownstreamAddress(any()))
+ .thenReturn(TEST_NAT64_CIDR);
+
+ mFakeOtDaemon = spy(new FakeOtDaemon(handler));
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
@@ -235,6 +246,7 @@
networkProvider,
() -> mFakeOtDaemon,
mMockConnectivityManager,
+ mMockRoutingCoordinatorManager,
mMockTunIfController,
mMockInfraIfController,
mPersistentSettings,
@@ -281,6 +293,37 @@
}
@Test
+ public void initialize_nat64Disabled_doesNotRequestNat64CidrAndConfiguresOtDaemon()
+ throws Exception {
+ ThreadConfiguration config =
+ new ThreadConfiguration.Builder().setNat64Enabled(false).build();
+ mPersistentSettings.putConfiguration(config);
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ verify(mMockRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
+ verify(mFakeOtDaemon, times(1)).setNat64Cidr(isNull(), any());
+ verify(mFakeOtDaemon, never()).setNat64Cidr(isNotNull(), any());
+ }
+
+ @Test
+ public void initialize_nat64Enabled_requestsNat64CidrAndConfiguresAtOtDaemon()
+ throws Exception {
+ ThreadConfiguration config =
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+ mPersistentSettings.putConfiguration(config);
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ verify(mMockRoutingCoordinatorManager, times(1)).requestDownstreamAddress(any());
+ verify(mFakeOtDaemon, times(1))
+ .setConfiguration(
+ new OtDaemonConfiguration.Builder().setNat64Enabled(true).build(),
+ null /* receiver */);
+ verify(mFakeOtDaemon, times(1)).setNat64Cidr(eq(TEST_NAT64_CIDR.toString()), any());
+ }
+
+ @Test
public void getMeshcopTxtAttributes_emptyVendorName_accepted() {
when(mResources.getString(eq(R.string.config_thread_vendor_name))).thenReturn("");
@@ -758,6 +801,71 @@
}
@Test
+ public void setConfiguration_enablesNat64_requestsNat64CidrAndConfiguresOtdaemon()
+ throws Exception {
+ mService.initialize();
+ mTestLooper.dispatchAll();
+ clearInvocations(mMockRoutingCoordinatorManager, mFakeOtDaemon);
+
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+ mService.setConfiguration(
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build(), mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ verify(mMockRoutingCoordinatorManager, times(1)).requestDownstreamAddress(any());
+ verify(mFakeOtDaemon, times(1))
+ .setConfiguration(
+ eq(new OtDaemonConfiguration.Builder().setNat64Enabled(true).build()),
+ any(IOtStatusReceiver.class));
+ verify(mFakeOtDaemon, times(1))
+ .setNat64Cidr(eq(TEST_NAT64_CIDR.toString()), any(IOtStatusReceiver.class));
+ }
+
+ @Test
+ public void setConfiguration_enablesNat64_otDaemonRemoteFailure_serviceDoesNotCrash()
+ throws Exception {
+ mService.initialize();
+ mTestLooper.dispatchAll();
+ clearInvocations(mMockRoutingCoordinatorManager, mFakeOtDaemon);
+ mFakeOtDaemon.setSetNat64CidrException(
+ new RemoteException("ot-daemon setNat64Cidr() throws"));
+
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+ mService.setConfiguration(
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build(), mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mFakeOtDaemon, times(1))
+ .setNat64Cidr(eq(TEST_NAT64_CIDR.toString()), any(IOtStatusReceiver.class));
+ }
+
+ @Test
+ public void setConfiguration_disablesNat64_releasesNat64CidrAndConfiguresOtdaemon()
+ throws Exception {
+ mPersistentSettings.putConfiguration(
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build());
+ mService.initialize();
+ mTestLooper.dispatchAll();
+ clearInvocations(mMockRoutingCoordinatorManager, mFakeOtDaemon);
+
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+ mService.setConfiguration(
+ new ThreadConfiguration.Builder().setNat64Enabled(false).build(), mockReceiver);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ verify(mMockRoutingCoordinatorManager, times(1)).releaseDownstream(any());
+ verify(mMockRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
+ verify(mFakeOtDaemon, times(1))
+ .setConfiguration(
+ eq(new OtDaemonConfiguration.Builder().setNat64Enabled(false).build()),
+ any(IOtStatusReceiver.class));
+ verify(mFakeOtDaemon, times(1)).setNat64Cidr(isNull(), any(IOtStatusReceiver.class));
+ verify(mFakeOtDaemon, never()).setNat64Cidr(isNotNull(), any(IOtStatusReceiver.class));
+ }
+
+ @Test
public void initialize_upstreamNetworkRequestHasCertainTransportTypesAndCapabilities() {
mService.initialize();
mTestLooper.dispatchAll();