From 47641d75d991a26e8616a239b717021810dee798 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Mon, 4 Apr 2022 14:46:25 +0200 Subject: [PATCH] Move ipconfig file inside apex data directory In order to support rollbacks, the ethernet ipconfig file needs to be written to the data apex's data directory. We still need to support reading from the legacy location to be backwards compatible. If the apex file path already exists, then read the IP configuration from there directly, otherwise, check if the legacy file path exists, if yes, read the legacy IP configuration and copy it to the new apex file path. Support for default IpConfiguration is also being removed in this CL. The default IpConfiguration (configuration entry for interface "0") was a way to transition from supporting a single interface to supporting multiple interfaces and was introduced in P. Every device that has used such a configuration and has connected to an ethernet network since upgrading to P+ has already removed the "0" config and has assigned it to the first interface (probably "eth0"). There has never been an API to set this, but if someone wanted to install a default IpConfiguration, they could just configure "eth0" instead. Bug: 224919694 Test: atest EthernetConfigStoreTest --iterations Test: manually test - adb push /data/misc/ethernet/ - adb reboot - read the IP config from legacy path and the same config file is created in the apex file path - adb reboot - read the IP config from the apex file path directly Change-Id: Ifb82a5d7fa13d17ea8fd3a42a19cd020cb95e26a --- .../server/ethernet/EthernetConfigStore.java | 95 +++++++++--- .../ethernet/EthernetConfigStoreTest.java | 143 ++++++++++++++++++ 2 files changed, 220 insertions(+), 18 deletions(-) create mode 100644 tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java diff --git a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java index 6b623f48ff..6006539600 100644 --- a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java +++ b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java @@ -16,23 +16,37 @@ package com.android.server.ethernet; +import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME; + import android.annotation.Nullable; +import android.content.ApexEnvironment; import android.net.IpConfiguration; import android.os.Environment; import android.util.ArrayMap; +import android.util.AtomicFile; +import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.net.IpConfigStore; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; /** * This class provides an API to store and manage Ethernet network configuration. */ public class EthernetConfigStore { - private static final String ipConfigFile = Environment.getDataDirectory() + - "/misc/ethernet/ipconfig.txt"; + private static final String TAG = EthernetConfigStore.class.getSimpleName(); + private static final String CONFIG_FILE = "ipconfig.txt"; + private static final String FILE_PATH = "/misc/ethernet/"; + private static final String LEGACY_IP_CONFIG_FILE_PATH = Environment.getDataDirectory() + + FILE_PATH; + private static final String APEX_IP_CONFIG_FILE_PATH = ApexEnvironment.getApexEnvironment( + TETHERING_MODULE_NAME).getDeviceProtectedDataDir() + FILE_PATH; private IpConfigStore mStore = new IpConfigStore(); - private ArrayMap mIpConfigurations; + private final ArrayMap mIpConfigurations; private IpConfiguration mIpConfigurationForDefaultInterface; private final Object mSync = new Object(); @@ -40,22 +54,70 @@ public class EthernetConfigStore { mIpConfigurations = new ArrayMap<>(0); } - public void read() { - synchronized (mSync) { - ArrayMap configs = - IpConfigStore.readIpConfigurations(ipConfigFile); + private static boolean doesConfigFileExist(final String filepath) { + return new File(filepath).exists(); + } - // This configuration may exist in old file versions when there was only a single active - // Ethernet interface. - if (configs.containsKey("0")) { - mIpConfigurationForDefaultInterface = configs.remove("0"); + private void writeLegacyIpConfigToApexPath(final String newFilePath, final String oldFilePath, + final String filename) { + final File directory = new File(newFilePath); + if (!directory.exists()) { + directory.mkdirs(); + } + + // Write the legacy IP config to the apex file path. + FileOutputStream fos = null; + final AtomicFile dst = new AtomicFile(new File(newFilePath + filename)); + final AtomicFile src = new AtomicFile(new File(oldFilePath + filename)); + try { + final byte[] raw = src.readFully(); + if (raw.length > 0) { + fos = dst.startWrite(); + fos.write(raw); + fos.flush(); + dst.finishWrite(fos); } - - mIpConfigurations = configs; + } catch (IOException e) { + Log.e(TAG, "Fail to sync the legacy IP config to the apex file path."); + dst.failWrite(fos); } } + public void read() { + read(APEX_IP_CONFIG_FILE_PATH, LEGACY_IP_CONFIG_FILE_PATH, CONFIG_FILE); + } + + @VisibleForTesting + void read(final String newFilePath, final String oldFilePath, final String filename) { + synchronized (mSync) { + // Attempt to read the IP configuration from apex file path first. + if (doesConfigFileExist(newFilePath + filename)) { + loadConfigFileLocked(newFilePath + filename); + return; + } + + // If the config file doesn't exist in the apex file path, attempt to read it from + // the legacy file path, if config file exists, write the legacy IP configuration to + // apex config file path, this should just happen on the first boot. New or updated + // config entries are only written to the apex config file later. + if (!doesConfigFileExist(oldFilePath + filename)) return; + loadConfigFileLocked(oldFilePath + filename); + writeLegacyIpConfigToApexPath(newFilePath, oldFilePath, filename); + } + } + + private void loadConfigFileLocked(final String filepath) { + final ArrayMap configs = + IpConfigStore.readIpConfigurations(filepath); + mIpConfigurations.putAll(configs); + } + public void write(String iface, IpConfiguration config) { + write(iface, config, APEX_IP_CONFIG_FILE_PATH + CONFIG_FILE); + } + + @VisibleForTesting + void write(String iface, IpConfiguration config, String filepath) { boolean modified; synchronized (mSync) { @@ -67,7 +129,7 @@ public class EthernetConfigStore { } if (modified) { - mStore.writeIpConfigurations(ipConfigFile, mIpConfigurations); + mStore.writeIpConfigurations(filepath, mIpConfigurations); } } } @@ -80,9 +142,6 @@ public class EthernetConfigStore { @Nullable public IpConfiguration getIpConfigurationForDefaultInterface() { - synchronized (mSync) { - return mIpConfigurationForDefaultInterface == null - ? null : new IpConfiguration(mIpConfigurationForDefaultInterface); - } + return null; } } diff --git a/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java b/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java new file mode 100644 index 0000000000..a9f80ea436 --- /dev/null +++ b/tests/unit/java/com/android/server/ethernet/EthernetConfigStoreTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2022 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.ethernet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.net.InetAddresses; +import android.net.IpConfiguration; +import android.net.IpConfiguration.IpAssignment; +import android.net.IpConfiguration.ProxySettings; +import android.net.LinkAddress; +import android.net.ProxyInfo; +import android.net.StaticIpConfiguration; +import android.util.ArrayMap; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class EthernetConfigStoreTest { + private static final LinkAddress LINKADDR = new LinkAddress("192.168.1.100/25"); + private static final InetAddress GATEWAY = InetAddresses.parseNumericAddress("192.168.1.1"); + private static final InetAddress DNS1 = InetAddresses.parseNumericAddress("8.8.8.8"); + private static final InetAddress DNS2 = InetAddresses.parseNumericAddress("8.8.4.4"); + private static final StaticIpConfiguration STATIC_IP_CONFIG = + new StaticIpConfiguration.Builder() + .setIpAddress(LINKADDR) + .setGateway(GATEWAY) + .setDnsServers(new ArrayList( + List.of(DNS1, DNS2))) + .build(); + private static final ProxyInfo PROXY_INFO = ProxyInfo.buildDirectProxy("test", 8888); + private static final IpConfiguration APEX_IP_CONFIG = + new IpConfiguration(IpAssignment.DHCP, ProxySettings.NONE, null, null); + private static final IpConfiguration LEGACY_IP_CONFIG = + new IpConfiguration(IpAssignment.STATIC, ProxySettings.STATIC, STATIC_IP_CONFIG, + PROXY_INFO); + + private EthernetConfigStore mEthernetConfigStore; + private File mApexTestDir; + private File mLegacyTestDir; + private File mApexConfigFile; + private File mLegacyConfigFile; + + private void createTestDir() { + final Context context = InstrumentationRegistry.getContext(); + final File baseDir = context.getFilesDir(); + mApexTestDir = new File(baseDir.getPath() + "/apex"); + mApexTestDir.mkdirs(); + + mLegacyTestDir = new File(baseDir.getPath() + "/legacy"); + mLegacyTestDir.mkdirs(); + } + + @Before + public void setUp() { + createTestDir(); + mEthernetConfigStore = new EthernetConfigStore(); + } + + @After + public void tearDown() { + mApexTestDir.delete(); + mLegacyTestDir.delete(); + } + + private void assertConfigFileExist(final String filepath) { + assertTrue(new File(filepath).exists()); + } + + /** Wait for the delayed write operation completes. */ + private void waitForMs(long ms) { + try { + Thread.sleep(ms); + } catch (final InterruptedException e) { + fail("Thread was interrupted"); + } + } + + @Test + public void testWriteIpConfigToApexFilePathAndRead() throws Exception { + // Write the config file to the apex file path, pretend the config file exits and + // check if IP config should be read from apex file path. + mApexConfigFile = new File(mApexTestDir.getPath(), "test.txt"); + mEthernetConfigStore.write("eth0", APEX_IP_CONFIG, mApexConfigFile.getPath()); + waitForMs(50); + + mEthernetConfigStore.read(mApexTestDir.getPath(), mLegacyTestDir.getPath(), "/test.txt"); + final ArrayMap ipConfigurations = + mEthernetConfigStore.getIpConfigurations(); + assertEquals(APEX_IP_CONFIG, ipConfigurations.get("eth0")); + + mApexConfigFile.delete(); + } + + @Test + public void testWriteIpConfigToLegacyFilePathAndRead() throws Exception { + // Write the config file to the legacy file path, pretend the config file exits and + // check if IP config should be read from legacy file path. + mLegacyConfigFile = new File(mLegacyTestDir, "test.txt"); + mEthernetConfigStore.write("0", LEGACY_IP_CONFIG, mLegacyConfigFile.getPath()); + waitForMs(50); + + mEthernetConfigStore.read(mApexTestDir.getPath(), mLegacyTestDir.getPath(), "/test.txt"); + final ArrayMap ipConfigurations = + mEthernetConfigStore.getIpConfigurations(); + assertEquals(LEGACY_IP_CONFIG, ipConfigurations.get("0")); + + // Check the same config file in apex file path is created. + assertConfigFileExist(mApexTestDir.getPath() + "/test.txt"); + + final File apexConfigFile = new File(mApexTestDir.getPath() + "/test.txt"); + apexConfigFile.delete(); + mLegacyConfigFile.delete(); + } +}