[Thread] add vendor & model name support for net diag

1. allow OEMs to default the vendor and model name to system properties.
2. use the vendor & model name for net diag TLVs

Change-Id: I30e988ba3e9cb9f9c5dc3a096f162d5dbcdfbc11
This commit is contained in:
Kangping Dong 2025-03-20 16:26:37 +08:00
commit 6303c84ef5
7 changed files with 166 additions and 64 deletions

View file

@ -46,11 +46,12 @@
<bool name="config_thread_location_use_for_country_code_enabled">true</bool>
<!-- Specifies the UTF-8 vendor name of this device. If this value is not an empty string, it
will be included in TXT value (key is 'vn') of the "_meshcop._udp" mDNS service which is
published by the Thread service. A non-empty string value must not exceed length of 24 UTF-8
bytes.
will be included in TXT value (key is 'vn') of the "_meshcop._udp" mDNS service as well as the
Vendor Name TLV for network diagnostic. A non-empty string value must not exceed length of 24
UTF-8 bytes. A special value "ro.product.manufacturer" indicates this value should be derived
from the `ro.product.manufacturer` system property.
-->
<string translatable="false" name="config_thread_vendor_name">Android</string>
<string translatable="false" name="config_thread_vendor_name">ro.product.manufacturer</string>
<!-- Specifies the 24 bits vendor OUI of this device. If this value is not an empty string, it
will be included in TXT (key is 'vo') value of the "_meshcop._udp" mDNS service which is
@ -61,11 +62,12 @@
<string translatable="false" name="config_thread_vendor_oui"></string>
<!-- Specifies the UTF-8 product model name of this device. If this value is not an empty
string, it will be included in TXT (key is 'mn') value of the "_meshcop._udp" mDNS service
which is published by the Thread service. A non-empty string value must not exceed length of 24
UTF-8 bytes.
string, it will be included in TXT (key is 'mn') value of the "_meshcop._udp" mDNS service as
well as the Vendor Model TLV for network diagnostic. A non-empty string value must not exceed
length of 24 UTF-8 bytes. A special value "ro.product.model" indicates this value should be
derived from the `ro.product.model` system property.
-->
<string translatable="false" name="config_thread_model_name">Thread Border Router</string>
<string translatable="false" name="config_thread_model_name">ro.product.model</string>
<!-- Specifies vendor-specific mDNS TXT entries which will be included in the "_meshcop._udp"
service. The TXT entries list MUST conform to the format requirement in RFC 6763 section 6. For

View file

@ -128,6 +128,7 @@ import com.android.net.module.util.RoutingCoordinatorManager;
import com.android.net.module.util.SharedLog;
import com.android.server.ServiceManagerWrapper;
import com.android.server.connectivity.ConnectivityResources;
import com.android.server.connectivity.MockableSystemProperties;
import com.android.server.thread.openthread.BackboneRouterState;
import com.android.server.thread.openthread.DnsTxtAttribute;
import com.android.server.thread.openthread.IChannelMasksReceiver;
@ -191,6 +192,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private final Context mContext;
private final Handler mHandler;
private final MockableSystemProperties mSystemProperties;
// Below member fields can only be accessed from the handler thread (`mHandler`). In
// particular, the constructor does not run on the handler thread, so it must not touch any of
@ -235,6 +237,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
ThreadNetworkControllerService(
Context context,
Handler handler,
MockableSystemProperties systemProperties,
NetworkProvider networkProvider,
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
@ -249,6 +252,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
Map<Network, LinkProperties> networkToLinkProperties) {
mContext = context;
mHandler = handler;
mSystemProperties = systemProperties;
mNetworkProvider = networkProvider;
mOtDaemonSupplier = otDaemonSupplier;
mConnectivityManager = connectivityManager;
@ -286,6 +290,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
return new ThreadNetworkControllerService(
context,
handler,
new MockableSystemProperties(),
networkProvider,
() -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
connectivityManager,
@ -355,7 +360,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
newOtDaemonConfig(mPersistentSettings.getConfiguration()),
mTunIfController.getTunFd(),
mNsdPublisher,
getMeshcopTxtAttributes(mResources.get()),
getMeshcopTxtAttributes(mResources.get(), mSystemProperties),
mCountryCodeSupplier.get(),
FeatureFlags.isTrelEnabled(),
mOtDaemonCallbackProxy);
@ -365,10 +370,37 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
return mOtDaemon;
}
static String getVendorName(Resources resources, MockableSystemProperties systemProperties) {
final String PROP_MANUFACTURER = "ro.product.manufacturer";
String vendorName = resources.getString(R.string.config_thread_vendor_name);
if (vendorName.equalsIgnoreCase(PROP_MANUFACTURER)) {
vendorName = systemProperties.get(PROP_MANUFACTURER);
// Assume it's always ASCII chars in ro.product.manufacturer
if (vendorName.length() > MAX_VENDOR_NAME_UTF8_BYTES) {
vendorName = vendorName.substring(0, MAX_VENDOR_NAME_UTF8_BYTES);
}
}
return vendorName;
}
static String getModelName(Resources resources, MockableSystemProperties systemProperties) {
final String PROP_MODEL = "ro.product.model";
String modelName = resources.getString(R.string.config_thread_model_name);
if (modelName.equalsIgnoreCase(PROP_MODEL)) {
modelName = systemProperties.get(PROP_MODEL);
// Assume it's always ASCII chars in ro.product.model
if (modelName.length() > MAX_MODEL_NAME_UTF8_BYTES) {
modelName = modelName.substring(0, MAX_MODEL_NAME_UTF8_BYTES);
}
}
return modelName;
}
@VisibleForTesting
static MeshcopTxtAttributes getMeshcopTxtAttributes(Resources resources) {
final String modelName = resources.getString(R.string.config_thread_model_name);
final String vendorName = resources.getString(R.string.config_thread_vendor_name);
static MeshcopTxtAttributes getMeshcopTxtAttributes(
Resources resources, MockableSystemProperties systemProperties) {
final String vendorName = getVendorName(resources, systemProperties);
final String modelName = getModelName(resources, systemProperties);
final String vendorOui = resources.getString(R.string.config_thread_vendor_oui);
final String[] vendorSpecificTxts =
resources.getStringArray(R.array.config_thread_mdns_vendor_specific_txts);
@ -637,6 +669,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
.setSrpServerWaitForBorderRoutingEnabled(srpServerWaitEnabled)
.setBorderRouterAutoJoinEnabled(autoJoinEnabled)
.setCountryCodeEnabled(countryCodeEnabled)
.setVendorName(getVendorName(mResources.get(), mSystemProperties))
.setModelName(getModelName(mResources.get(), mSystemProperties))
.build();
}

View file

@ -48,6 +48,7 @@ import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThrea
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature;
import android.net.thread.utils.ThreadNetworkControllerWrapper;
import android.os.HandlerThread;
import android.os.SystemProperties;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
@ -454,14 +455,22 @@ public class ServiceDiscoveryTest {
}
@Test
public void meshcopOverlay_vendorAndModelNameAreSetToOverlayValue() throws Exception {
public void meshcopOverlay_vendorAndModelNameAreSetToSystemProperties() throws Exception {
NsdServiceInfo discoveredService = discoverService(mNsdManager, "_meshcop._udp");
assertThat(discoveredService).isNotNull();
NsdServiceInfo meshcopService = resolveService(mNsdManager, discoveredService);
String expectedVendorName = SystemProperties.get("ro.product.manufacturer");
if (expectedVendorName.length() > 24) {
expectedVendorName = expectedVendorName.substring(0, 24);
}
String expectedModelName = SystemProperties.get("ro.product.model");
if (expectedModelName.length() > 24) {
expectedModelName = expectedModelName.substring(0, 24);
}
Map<String, byte[]> txtMap = meshcopService.getAttributes();
assertThat(txtMap.get("vn")).isEqualTo("Android".getBytes(UTF_8));
assertThat(txtMap.get("mn")).isEqualTo("Thread Border Router".getBytes(UTF_8));
assertThat(txtMap.get("vn")).isEqualTo(expectedVendorName.getBytes(UTF_8));
assertThat(txtMap.get("mn")).isEqualTo(expectedModelName.getBytes(UTF_8));
}
@Test

View file

@ -24,6 +24,8 @@ import static android.net.thread.utils.IntegrationTestUtils.CALLBACK_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.getIpv6Addresses;
import static android.net.thread.utils.IntegrationTestUtils.getIpv6LinkAddresses;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
import static android.net.thread.utils.OtDaemonController.DIAG_VENDOR_MODEL_TLV_TYPE;
import static android.net.thread.utils.OtDaemonController.DIAG_VENDOR_NAME_TLV_TYPE;
import static android.net.thread.utils.ThreadNetworkControllerWrapper.JOIN_TIMEOUT;
import static android.os.SystemClock.elapsedRealtime;
@ -290,6 +292,17 @@ public class ThreadIntegrationTest {
// TODO: b/376217403 - enables / disables Border Agent at runtime
}
@Test
public void networkDiagnostic_vendorAndModelNameAreSet() throws Exception {
mController.joinAndWait(DEFAULT_DATASET);
var tlvTypes = List.of(DIAG_VENDOR_NAME_TLV_TYPE, DIAG_VENDOR_MODEL_TLV_TYPE);
var result = mOtCtl.netDiagGet(mOtCtl.getMlEid(), tlvTypes);
assertThat(result.get("Vendor Name")).isNotEmpty();
assertThat(result.get("Vendor Model")).isNotEmpty();
}
private NetworkCapabilities registerNetworkCallbackAndWait(NetworkRequest request)
throws Exception {
CompletableFuture<Network> networkFuture = new CompletableFuture<>();

View file

@ -34,6 +34,7 @@ import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_TESTING;
import static com.android.server.thread.ThreadNetworkControllerService.getMeshcopTxtAttributes;
import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
import static com.android.server.thread.ThreadPersistentSettings.KEY_THREAD_ENABLED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
@ -49,8 +50,6 @@ import static org.mockito.ArgumentMatchers.anyInt;
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;
@ -101,10 +100,10 @@ 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.connectivity.MockableSystemProperties;
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.After;
@ -181,6 +180,7 @@ public final class ThreadNetworkControllerServiceTest {
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 MockableSystemProperties mMockSystemProperties;
@Mock private ConnectivityManager mMockConnectivityManager;
@Mock private RoutingCoordinatorManager mMockRoutingCoordinatorManager;
@Mock private NetworkAgent mMockNetworkAgent;
@ -271,6 +271,7 @@ public final class ThreadNetworkControllerServiceTest {
new ThreadNetworkControllerService(
mContext,
handler,
mMockSystemProperties,
networkProvider,
() -> mFakeOtDaemon,
mMockConnectivityManager,
@ -335,6 +336,23 @@ public final class ThreadNetworkControllerServiceTest {
.containsExactly(new DnsTxtAttribute("vt", "test".getBytes(UTF_8)));
}
@Test
public void initialize_vendorAndModelNameSetToProperty_propertiesAreSetToOtDaemon()
throws Exception {
when(mMockSystemProperties.get(eq("ro.product.manufacturer"))).thenReturn("Banana");
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn("ro.product.manufacturer");
when(mMockSystemProperties.get(eq("ro.product.model"))).thenReturn("Orange");
when(mResources.getString(eq(R.string.config_thread_model_name)))
.thenReturn("ro.product.model");
mService.initialize();
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getConfiguration().vendorName).isEqualTo("Banana");
assertThat(mFakeOtDaemon.getConfiguration().modelName).isEqualTo("Orange");
}
@Test
public void initialize_nat64Disabled_doesNotRequestNat64CidrAndConfiguresOtDaemon()
throws Exception {
@ -345,8 +363,7 @@ public final class ThreadNetworkControllerServiceTest {
mTestLooper.dispatchAll();
verify(mMockRoutingCoordinatorManager, never()).requestDownstreamAddress(any());
verify(mFakeOtDaemon, times(1)).setNat64Cidr(isNull(), any());
verify(mFakeOtDaemon, never()).setNat64Cidr(isNotNull(), any());
assertThat(mFakeOtDaemon.getNat64Cidr()).isNull();
}
@Test
@ -359,11 +376,8 @@ public final class ThreadNetworkControllerServiceTest {
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());
assertThat(mFakeOtDaemon.getConfiguration().nat64Enabled).isTrue();
assertThat(mFakeOtDaemon.getNat64Cidr()).isEqualTo(TEST_NAT64_CIDR.toString());
}
@Test
@ -400,7 +414,7 @@ public final class ThreadNetworkControllerServiceTest {
when(mResources.getString(eq(R.string.config_thread_vendor_name))).thenReturn("");
MeshcopTxtAttributes meshcopTxts =
ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
getMeshcopTxtAttributes(mResources, mMockSystemProperties);
assertThat(meshcopTxts.vendorName).isEqualTo("");
}
@ -412,7 +426,31 @@ public final class ThreadNetworkControllerServiceTest {
assertThrows(
IllegalStateException.class,
() -> ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources));
() -> getMeshcopTxtAttributes(mResources, mMockSystemProperties));
}
@Test
public void getMeshcopTxtAttributes_VendorNameSetToManufacturer_manufacturerPropertyIsUsed() {
when(mMockSystemProperties.get(eq("ro.product.manufacturer"))).thenReturn("Banana");
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn("ro.product.manufacturer");
MeshcopTxtAttributes meshcopTxts =
getMeshcopTxtAttributes(mResources, mMockSystemProperties);
assertThat(meshcopTxts.vendorName).isEqualTo("Banana");
}
@Test
public void getMeshcopTxtAttributes_ModelNameSetToModelProperty_modelPropertyIsUsed() {
when(mMockSystemProperties.get(eq("ro.product.model"))).thenReturn("Orange");
when(mResources.getString(eq(R.string.config_thread_model_name)))
.thenReturn("ro.product.model");
MeshcopTxtAttributes meshcopTxts =
getMeshcopTxtAttributes(mResources, mMockSystemProperties);
assertThat(meshcopTxts.modelName).isEqualTo("Orange");
}
@Test
@ -422,14 +460,14 @@ public final class ThreadNetworkControllerServiceTest {
assertThrows(
IllegalStateException.class,
() -> ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources));
() -> getMeshcopTxtAttributes(mResources, mMockSystemProperties));
}
@Test
public void getMeshcopTxtAttributes_emptyModelName_accepted() {
when(mResources.getString(eq(R.string.config_thread_model_name))).thenReturn("");
var meshcopTxts = ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources);
var meshcopTxts = getMeshcopTxtAttributes(mResources, mMockSystemProperties);
assertThat(meshcopTxts.modelName).isEqualTo("");
}
@ -461,7 +499,7 @@ public final class ThreadNetworkControllerServiceTest {
private byte[] getMeshcopTxtAttributesWithVendorOui(String vendorOui) {
when(mResources.getString(eq(R.string.config_thread_vendor_oui))).thenReturn(vendorOui);
return ThreadNetworkControllerService.getMeshcopTxtAttributes(mResources).vendorOui;
return getMeshcopTxtAttributes(mResources, mMockSystemProperties).vendorOui;
}
@Test
@ -886,16 +924,12 @@ public final class ThreadNetworkControllerServiceTest {
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));
assertThat(mFakeOtDaemon.getConfiguration().nat64Enabled).isTrue();
assertThat(mFakeOtDaemon.getNat64Cidr()).isEqualTo(TEST_NAT64_CIDR.toString());
}
@Test
public void setConfiguration_enablesNat64_otDaemonRemoteFailure_serviceDoesNotCrash()
public void setConfiguration_enablesNat64AndOtDaemonRemoteFailure_serviceDoesNotCrash()
throws Exception {
mService.initialize();
mTestLooper.dispatchAll();
@ -929,12 +963,8 @@ public final class ThreadNetworkControllerServiceTest {
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));
assertThat(mFakeOtDaemon.getConfiguration().nat64Enabled).isFalse();
assertThat(mFakeOtDaemon.getNat64Cidr()).isNull();
}
@Test

View file

@ -24,10 +24,13 @@ import android.os.SystemClock;
import com.android.compatibility.common.util.SystemUtil;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
@ -37,6 +40,9 @@ import java.util.stream.Collectors;
* <p>Note that this class takes root privileged to run.
*/
public final class OtDaemonController {
public static final int DIAG_VENDOR_NAME_TLV_TYPE = 25;
public static final int DIAG_VENDOR_MODEL_TLV_TYPE = 26;
private static final String OT_CTL = "/system/bin/ot-ctl";
/**
@ -187,6 +193,35 @@ public final class OtDaemonController {
return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
}
/**
* Sends DIAG_GET request to the given peer device and returns the parsed result as a dict of
* the requested TLV values.
*
* <p>For example, a request {@code netDiagGet("fdad:3d13:7b11:4049:ed1a:7e87:4770:a345",
* [DIAG_VENDOR_NAME_TLV_TYPE, DIAG_VENDOR_MODEL_TLV_TYPE])} can return a dict of {@code
* {"Vendor Name" : "ABC", "Vendor Model" : "Cuttlefish"}}
*/
public Map<String, String> netDiagGet(InetAddress peerAddr, List<Integer> tlvTypes) {
String tlvTypeList =
tlvTypes.stream().map(Object::toString).collect(Collectors.joining(" "));
List<String> outputs =
executeCommandAndParse(
"networkdiagnostic get " + peerAddr.getHostAddress() + " " + tlvTypeList);
Map<String, String> result = new HashMap<>();
for (String line : outputs) {
if (line.startsWith("DIAG_GET")) {
continue;
}
String[] keyValue = line.split(":");
if (keyValue.length != 2) {
throw new IllegalStateException("Unexpected OT output: " + line);
}
result.put(keyValue[0].strip(), keyValue[1].strip());
}
return result;
}
/**
* Executes a ot-ctl command and parse the output to a list of strings.
*

View file

@ -16,44 +16,23 @@
package android.net.thread.utils;
import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.IPPROTO_UDP;
import static android.system.OsConstants.SOCK_DGRAM;
import static com.android.testutils.RecorderCallback.CallbackEntry.LINK_PROPERTIES_CHANGED;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.TestNetworkInterface;
import android.net.TestNetworkManager;
import android.net.TestNetworkSpecifier;
import android.os.Looper;
import android.system.ErrnoException;
import android.system.Os;
import com.android.compatibility.common.util.PollingCheck;
import com.android.testutils.TestableNetworkAgent;
import com.android.testutils.TestableNetworkCallback;
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** A class that can create/destroy a test network based on TAP interface. */
public final class TapTestNetworkTracker {