Merge "Add SoftApConfiguration to TetheringRequest" into main
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 3944a8a..1eb6255 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -26,7 +26,9 @@
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringTester.buildTcpPacket;
import static android.net.TetheringTester.buildUdpPacket;
+import static android.net.TetheringTester.buildUdpPackets;
import static android.net.TetheringTester.isAddressIpv4;
+import static android.net.TetheringTester.isExpectedFragmentIpPacket;
import static android.net.TetheringTester.isExpectedIcmpPacket;
import static android.net.TetheringTester.isExpectedTcpPacket;
import static android.net.TetheringTester.isExpectedUdpPacket;
@@ -58,12 +60,14 @@
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
+import android.util.ArrayMap;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.FragmentHeader;
import com.android.net.module.util.structs.Ipv6Header;
import com.android.testutils.HandlerUtils;
import com.android.testutils.TapPacketReader;
@@ -678,6 +682,57 @@
});
}
+ protected void sendDownloadFragmentedUdpPackets(@NonNull final Inet6Address srcIp,
+ @NonNull final Inet6Address dstIp, @NonNull final TetheringTester tester,
+ @NonNull final ByteBuffer payload, int l2mtu) throws Exception {
+ final List<ByteBuffer> testPackets = buildUdpPackets(null /* srcMac */, null /* dstMac */,
+ srcIp, dstIp, REMOTE_PORT, LOCAL_PORT, payload, l2mtu);
+ assertTrue("No packet fragmentation occurs", testPackets.size() > 1);
+
+ short id = 0;
+ final ArrayMap<Short, ByteBuffer> fragmentPayloads = new ArrayMap<>();
+ for (ByteBuffer testPacket : testPackets) {
+ Struct.parse(Ipv6Header.class, testPacket);
+ final FragmentHeader fragmentHeader = Struct.parse(FragmentHeader.class, testPacket);
+ // Conversion of IPv6's fragmentOffset field to IPv4's flagsAndFragmentOffset field.
+ // IPv6 Fragment Header:
+ // '13 bits of offset in multiples of 8' + 2 zero bits + more fragment bit
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | Next Header | Reserved | Fragment Offset |Res|M|
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | Identification |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // IPv4 Header:
+ // zero bit + don't frag bit + more frag bit + '13 bits of offset in multiples of 8'
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // |Version| IHL |Type of Service| Total Length |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | Identification |Flags| Fragment Offset |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // + . . . +
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ short offset = (short) (((fragmentHeader.fragmentOffset & 0x1) << 13)
+ | (fragmentHeader.fragmentOffset >> 3));
+ // RFC6145: for fragment id, copied from the low-order 16 bits in the identification
+ // field in the Fragment Header.
+ id = (short) (fragmentHeader.identification & 0xffff);
+ final byte[] fragmentPayload = new byte[testPacket.remaining()];
+ testPacket.get(fragmentPayload);
+ testPacket.flip();
+ fragmentPayloads.put(offset, ByteBuffer.wrap(fragmentPayload));
+ }
+
+ final short fragId = id;
+ tester.verifyDownloadBatch(testPackets, p -> {
+ Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
+ return isExpectedFragmentIpPacket(p, fragId, fragmentPayloads);
+ });
+ }
+
protected void sendDownloadPacketTcp(@NonNull final InetAddress srcIp,
@NonNull final InetAddress dstIp, short seq, short ack, byte tcpFlags,
@NonNull final ByteBuffer payload, @NonNull final TetheringTester tester,
diff --git a/Tethering/tests/integration/base/android/net/TetheringTester.java b/Tethering/tests/integration/base/android/net/TetheringTester.java
index ae4ae55..b152b4c 100644
--- a/Tethering/tests/integration/base/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/base/android/net/TetheringTester.java
@@ -27,6 +27,7 @@
import static android.system.OsConstants.IPPROTO_IPV6;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
+
import static com.android.net.module.util.DnsPacket.ANSECTION;
import static com.android.net.module.util.DnsPacket.DnsHeader;
import static com.android.net.module.util.DnsPacket.DnsRecord;
@@ -53,6 +54,7 @@
import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
+
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
@@ -91,6 +93,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
@@ -497,6 +500,51 @@
});
}
+ /**
+ * Checks if the given raw packet data represents an expected fragmented IP packet.
+ *
+ * @param rawPacket the raw packet data to check.
+ * @param id the identification field of the fragmented IP packet.
+ * @param expectedPayloads a map of fragment offsets to their corresponding payload data.
+ * @return true if the packet is a valid fragmented IP packet with matching payload fragments;
+ * false otherwise.
+ */
+ public static boolean isExpectedFragmentIpPacket(@NonNull final byte[] rawPacket, int id,
+ @NonNull final Map<Short, ByteBuffer> expectedPayloads) {
+ final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
+ try {
+ // Validate Ethernet header and IPv4 header.
+ if (!hasExpectedEtherHeader(buf, true /* isIpv4 */)) return false;
+ final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, buf);
+ if (ipv4Header.protocol != (byte) IPPROTO_UDP) return false;
+ if (ipv4Header.id != id) return false;
+ // Validate payload data which expected at a specific fragment offset.
+ final ByteBuffer expectedPayload =
+ expectedPayloads.get(ipv4Header.flagsAndFragmentOffset);
+ if (expectedPayload == null) return false;
+ if (buf.remaining() != expectedPayload.limit()) return false;
+ // Validate UDP header (which located in the 1st fragment).
+ // TODO: Validate the checksum field in UDP header. Currently, it'll be altered by NAT.
+ if ((ipv4Header.flagsAndFragmentOffset & 0x1FFF) == 0) {
+ final UdpHeader receivedUdpHeader = Struct.parse(UdpHeader.class, buf);
+ final UdpHeader expectedUdpHeader = Struct.parse(UdpHeader.class, expectedPayload);
+ if (receivedUdpHeader == null || expectedUdpHeader == null) return false;
+ if (receivedUdpHeader.srcPort != expectedUdpHeader.srcPort
+ || receivedUdpHeader.dstPort != expectedUdpHeader.dstPort
+ || receivedUdpHeader.length != expectedUdpHeader.length) {
+ return false;
+ }
+ return true;
+ }
+ // Check the contents of the remaining payload.
+ return Arrays.equals(getRemaining(buf),
+ getRemaining(expectedPayload.asReadOnlyBuffer()));
+ } catch (Exception e) {
+ // A failed packet parsing indicates that the packet is not a fragmented IPv4 packet.
+ return false;
+ }
+ }
+
// |expectedPayload| is copied as read-only because the caller may reuse it.
// See hasExpectedDnsMessage.
public static boolean isExpectedUdpDnsPacket(@NonNull final byte[] rawPacket, boolean hasEth,
@@ -683,10 +731,10 @@
}
@NonNull
- public static ByteBuffer buildUdpPacket(
+ public static List<ByteBuffer> buildUdpPackets(
@Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
@NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
- short srcPort, short dstPort, @Nullable final ByteBuffer payload)
+ short srcPort, short dstPort, @Nullable final ByteBuffer payload, int l2mtu)
throws Exception {
final int ipProto = getIpProto(srcIp, dstIp);
final boolean hasEther = (srcMac != null && dstMac != null);
@@ -720,7 +768,30 @@
payload.clear();
}
- return packetBuilder.finalizePacket();
+ return l2mtu == 0
+ ? Arrays.asList(packetBuilder.finalizePacket())
+ : packetBuilder.finalizePacket(l2mtu);
+ }
+
+ /**
+ * Builds a UDP packet.
+ *
+ * @param srcMac the source MAC address.
+ * @param dstMac the destination MAC address.
+ * @param srcIp the source IP address.
+ * @param dstIp the destination IP address.
+ * @param srcPort the source port number.
+ * @param dstPort the destination port number.
+ * @param payload the optional payload data to be included in the packet.
+ * @return a ByteBuffer containing the constructed UDP packet.
+ */
+ @NonNull
+ public static ByteBuffer buildUdpPacket(
+ @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
+ @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp,
+ short srcPort, short dstPort, @Nullable final ByteBuffer payload)
+ throws Exception {
+ return buildUdpPackets(srcMac, dstMac, srcIp, dstIp, srcPort, dstPort, payload, 0).get(0);
}
@NonNull
@@ -994,6 +1065,28 @@
return verifyPacketNotNull("Download fail", getDownloadPacket(filter));
}
+ /**
+ * Sends a batch of download packets and verifies against a specified filtering condition.
+ *
+ * This method is designed for testing fragmented packets. All packets are sent before
+ * verification because the kernel buffers fragments until the last one is received.
+ * Captured packets are then verified against the provided filter.
+ *
+ * @param packets the list of ByteBuffers containing the packets to send.
+ * @param filter a Predicate that defines the filtering condition to apply to each received
+ * packet. If the filter returns true for a packet's data, it is considered to
+ * meet the verification criteria.
+ */
+ public void verifyDownloadBatch(final List<ByteBuffer> packets, final Predicate<byte[]> filter)
+ throws Exception {
+ for (ByteBuffer packet : packets) {
+ sendDownloadPacket(packet);
+ }
+ for (int i = 0; i < packets.size(); ++i) {
+ verifyPacketNotNull("Download fail", getDownloadPacket(filter));
+ }
+ }
+
// Send DHCPDISCOVER to DHCP server to see if DHCP server is still alive to handle
// the upcoming DHCP packets. This method should be only used when we know the DHCP
// server has been created successfully before.
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 9cdba2f..049f5f0 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -33,6 +33,9 @@
import static com.android.net.module.util.HexDump.dumpHexString;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
import static com.android.testutils.DeviceInfoUtils.KVersion;
import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -62,6 +65,10 @@
import com.android.net.module.util.BpfDump;
import com.android.net.module.util.Ipv6Utils;
import com.android.net.module.util.Struct;
+import com.android.net.module.util.bpf.ClatEgress4Key;
+import com.android.net.module.util.bpf.ClatEgress4Value;
+import com.android.net.module.util.bpf.ClatIngress6Key;
+import com.android.net.module.util.bpf.ClatIngress6Value;
import com.android.net.module.util.bpf.Tether4Key;
import com.android.net.module.util.bpf.Tether4Value;
import com.android.net.module.util.bpf.TetherStatsKey;
@@ -122,6 +129,8 @@
private static final int TX_UDP_PACKET_SIZE = 30;
private static final int TX_UDP_PACKET_COUNT = 123;
+ private static final String DUMPSYS_CLAT_RAWMAP_EGRESS4_ARG = "clatEgress4RawBpfMap";
+ private static final String DUMPSYS_CLAT_RAWMAP_INGRESS6_ARG = "clatIngress6RawBpfMap";
private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
private static final String DUMPSYS_RAWMAP_ARG_STATS = "--stats";
private static final String DUMPSYS_RAWMAP_ARG_UPSTREAM4 = "--upstream4";
@@ -901,11 +910,10 @@
@NonNull
private <K extends Struct, V extends Struct> HashMap<K, V> dumpAndParseRawMap(
- Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+ Class<K> keyClass, Class<V> valueClass, @NonNull String service, @NonNull String[] args)
throws Exception {
- final String[] args = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, mapArg};
final String rawMapStr = runAsShell(DUMP, () ->
- DumpTestUtils.dumpService(Context.TETHERING_SERVICE, args));
+ DumpTestUtils.dumpService(service, args));
final HashMap<K, V> map = new HashMap<>();
for (final String line : rawMapStr.split(LINE_DELIMITER)) {
@@ -918,10 +926,10 @@
@Nullable
private <K extends Struct, V extends Struct> HashMap<K, V> pollRawMapFromDump(
- Class<K> keyClass, Class<V> valueClass, @NonNull String mapArg)
+ Class<K> keyClass, Class<V> valueClass, @NonNull String service, @NonNull String[] args)
throws Exception {
for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
- final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, mapArg);
+ final HashMap<K, V> map = dumpAndParseRawMap(keyClass, valueClass, service, args);
if (!map.isEmpty()) return map;
Thread.sleep(DUMP_POLLING_INTERVAL_MS);
@@ -977,8 +985,10 @@
Thread.sleep(UDP_STREAM_SLACK_MS);
// [1] Verify IPv4 upstream rule map.
+ final String[] upstreamArgs = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG,
+ DUMPSYS_RAWMAP_ARG_UPSTREAM4};
final HashMap<Tether4Key, Tether4Value> upstreamMap = pollRawMapFromDump(
- Tether4Key.class, Tether4Value.class, DUMPSYS_RAWMAP_ARG_UPSTREAM4);
+ Tether4Key.class, Tether4Value.class, Context.TETHERING_SERVICE, upstreamArgs);
assertNotNull(upstreamMap);
assertEquals(1, upstreamMap.size());
@@ -1017,8 +1027,10 @@
}
// Dump stats map to verify.
+ final String[] statsArgs = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG,
+ DUMPSYS_RAWMAP_ARG_STATS};
final HashMap<TetherStatsKey, TetherStatsValue> statsMap = pollRawMapFromDump(
- TetherStatsKey.class, TetherStatsValue.class, DUMPSYS_RAWMAP_ARG_STATS);
+ TetherStatsKey.class, TetherStatsValue.class, Context.TETHERING_SERVICE, statsArgs);
assertNotNull(statsMap);
assertEquals(1, statsMap.size());
@@ -1053,4 +1065,102 @@
runUdp4Test();
}
+
+ private ClatEgress4Value getClatEgress4Value() throws Exception {
+ // Command: dumpsys connectivity clatEgress4RawBpfMap
+ final String[] args = new String[] {DUMPSYS_CLAT_RAWMAP_EGRESS4_ARG};
+ final HashMap<ClatEgress4Key, ClatEgress4Value> egress4Map = pollRawMapFromDump(
+ ClatEgress4Key.class, ClatEgress4Value.class, Context.CONNECTIVITY_SERVICE, args);
+ assertNotNull(egress4Map);
+ assertEquals(1, egress4Map.size());
+ return egress4Map.entrySet().iterator().next().getValue();
+ }
+
+ private ClatIngress6Value getClatIngress6Value() throws Exception {
+ // Command: dumpsys connectivity clatIngress6RawBpfMap
+ final String[] args = new String[] {DUMPSYS_CLAT_RAWMAP_INGRESS6_ARG};
+ final HashMap<ClatIngress6Key, ClatIngress6Value> ingress6Map = pollRawMapFromDump(
+ ClatIngress6Key.class, ClatIngress6Value.class, Context.CONNECTIVITY_SERVICE, args);
+ assertNotNull(ingress6Map);
+ assertEquals(1, ingress6Map.size());
+ return ingress6Map.entrySet().iterator().next().getValue();
+ }
+
+ /**
+ * Test network topology:
+ *
+ * public network (rawip) private network
+ * | UE (CLAT support) |
+ * +---------------+ V +------------+------------+ V +------------+
+ * | NAT64 Gateway +---------+ Upstream | Downstream +---------+ Client |
+ * +---------------+ +------------+------------+ +------------+
+ * remote ip public ip private ip
+ * [64:ff9b::808:808]:443 [clat ipv6]:9876 [TetheredDevice ipv4]:9876
+ *
+ * Note that CLAT IPv6 address is generated by ClatCoordinator. Get the CLAT IPv6 address by
+ * sending out an IPv4 packet and extracting the source address from CLAT translated IPv6
+ * packet.
+ */
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testTetherClatBpfOffloadUdp() throws Exception {
+ assumeKernelSupportBpfOffloadUdpV4();
+
+ // CLAT only starts on IPv6 only network.
+ final TetheringTester tester = initTetheringTester(toList(TEST_IP6_ADDR),
+ toList(TEST_IP6_DNS));
+ final TetheredDevice tethered = tester.createTetheredDevice(TEST_MAC, true /* hasIpv6 */);
+
+ // Get CLAT IPv6 address.
+ final Inet6Address clatIp6 = getClatIpv6Address(tester, tethered);
+
+ // Get current values before sending packets.
+ final ClatEgress4Value oldEgress4 = getClatEgress4Value();
+ final ClatIngress6Value oldIngress6 = getClatIngress6Value();
+
+ // Send an IPv4 UDP packet in original direction.
+ // IPv4 packet -- CLAT translation --> IPv6 packet
+ for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
+ sendUploadPacketUdp(tethered.macAddr, tethered.routerMacAddr, tethered.ipv4Addr,
+ REMOTE_IP4_ADDR, tester, true /* is4To6 */);
+ }
+
+ // Send an IPv6 UDP packet in reply direction.
+ // IPv6 packet -- CLAT translation --> IPv4 packet
+ for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
+ sendDownloadPacketUdp(REMOTE_NAT64_ADDR, clatIp6, tester, true /* is6To4 */);
+ }
+
+ // Send fragmented IPv6 UDP packets in the reply direction.
+ // IPv6 frament packet -- CLAT translation --> IPv4 fragment packet
+ final int payloadLen = 1500;
+ final int l2mtu = 1000;
+ final int fragPktCnt = 2; // 1500 bytes of UDP payload were fragmented into two packets.
+ final long fragRxBytes = payloadLen + UDP_HEADER_LEN + fragPktCnt * IPV4_HEADER_MIN_LEN;
+ final byte[] payload = new byte[payloadLen];
+ // Initialize the payload with random bytes.
+ Random random = new Random();
+ random.nextBytes(payload);
+ sendDownloadFragmentedUdpPackets(REMOTE_NAT64_ADDR, clatIp6, tester,
+ ByteBuffer.wrap(payload), l2mtu);
+
+ // After sending test packets, get stats again to verify their differences.
+ final ClatEgress4Value newEgress4 = getClatEgress4Value();
+ final ClatIngress6Value newIngress6 = getClatIngress6Value();
+
+ assertEquals(RX_UDP_PACKET_COUNT + fragPktCnt, newIngress6.packets - oldIngress6.packets);
+ assertEquals(RX_UDP_PACKET_COUNT * RX_UDP_PACKET_SIZE + fragRxBytes,
+ newIngress6.bytes - oldIngress6.bytes);
+ assertEquals(TX_UDP_PACKET_COUNT, newEgress4.packets - oldEgress4.packets);
+ // The increase in egress traffic equals the expected size of the translated UDP packets.
+ // Calculation:
+ // - Original UDP packet was TX_UDP_PACKET_SIZE bytes (IPv4 header + UDP header + payload).
+ // - After CLAT translation, each packet is now:
+ // IPv6 header + unchanged UDP header + unchanged payload
+ // Therefore, the total size of the translated UDP packet should be:
+ // TX_UDP_PACKET_SIZE + IPV6_HEADER_LEN - IPV4_HEADER_MIN_LEN
+ assertEquals(
+ TX_UDP_PACKET_COUNT * (TX_UDP_PACKET_SIZE + IPV6_HEADER_LEN - IPV4_HEADER_MIN_LEN),
+ newEgress4.bytes - oldEgress4.bytes);
+ }
}
diff --git a/common/flags.aconfig b/common/flags.aconfig
index 1b0da4e..45cbb78 100644
--- a/common/flags.aconfig
+++ b/common/flags.aconfig
@@ -10,6 +10,7 @@
namespace: "android_core_networking"
description: "Set data saver through ConnectivityManager API"
bug: "297836825"
+ is_fixed_read_only: true
}
flag {
@@ -18,6 +19,7 @@
namespace: "android_core_networking"
description: "This flag controls whether isUidNetworkingBlocked is supported"
bug: "297836825"
+ is_fixed_read_only: true
}
flag {
@@ -26,6 +28,7 @@
namespace: "android_core_networking"
description: "Block network access for apps in a low importance background state"
bug: "304347838"
+ is_fixed_read_only: true
}
flag {
@@ -34,6 +37,7 @@
namespace: "android_core_networking_ipsec"
description: "The flag controls the access for getIpSecTransformState and IpSecTransformState"
bug: "308011229"
+ is_fixed_read_only: true
}
flag {
@@ -42,6 +46,7 @@
namespace: "android_core_networking"
description: "The flag controls the access for the parcelable TetheringRequest with getSoftApConfiguration/setSoftApConfiguration API"
bug: "216524590"
+ is_fixed_read_only: true
}
flag {
@@ -50,6 +55,7 @@
namespace: "android_core_networking"
description: "Flag for API to support requesting restricted wifi"
bug: "315835605"
+ is_fixed_read_only: true
}
flag {
@@ -58,6 +64,7 @@
namespace: "android_core_networking"
description: "Flag for local network capability API"
bug: "313000440"
+ is_fixed_read_only: true
}
flag {
@@ -66,6 +73,7 @@
namespace: "android_core_networking"
description: "Flag for satellite transport API"
bug: "320514105"
+ is_fixed_read_only: true
}
flag {
@@ -74,6 +82,7 @@
namespace: "android_core_networking"
description: "Flag for API to support nsd subtypes"
bug: "265095929"
+ is_fixed_read_only: true
}
flag {
@@ -82,6 +91,7 @@
namespace: "android_core_networking"
description: "Flag for API to register nsd offload engine"
bug: "301713539"
+ is_fixed_read_only: true
}
flag {
@@ -90,6 +100,7 @@
namespace: "android_core_networking"
description: "Flag for metered network firewall chain API"
bug: "332628891"
+ is_fixed_read_only: true
}
flag {
@@ -98,6 +109,7 @@
namespace: "android_core_networking"
description: "Flag for oem deny chains blocked reasons API"
bug: "328732146"
+ is_fixed_read_only: true
}
flag {
@@ -106,6 +118,7 @@
namespace: "android_core_networking"
description: "Flag for BLOCKED_REASON_NETWORK_RESTRICTED API"
bug: "339559837"
+ is_fixed_read_only: true
}
flag {
@@ -114,6 +127,7 @@
namespace: "android_core_networking"
description: "Flag for NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED API"
bug: "343823469"
+ is_fixed_read_only: true
}
flag {
@@ -122,6 +136,7 @@
namespace: "android_core_networking"
description: "Flag for introducing TETHERING_VIRTUAL type"
bug: "340376953"
+ is_fixed_read_only: true
}
flag {
@@ -130,4 +145,5 @@
namespace: "android_core_networking"
description: "Flag for NetworkStats#addEntries API"
bug: "335680025"
+ is_fixed_read_only: true
}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0fe24a2..53b1eb2 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -6002,7 +6002,7 @@
// 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) {
+ if (null != satisfier && !satisfier.isDestroyed()) {
try {
mNetd.networkRemoveUidRangesParcel(new NativeUidRangeConfig(
satisfier.network.getNetId(),
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index a014834..d00ae52 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -61,8 +61,10 @@
private val shouldThreadLeakFailTest = klass.isAnnotationPresent(MonitorThreadLeak::class.java)
private val restoreDefaultNetworkDesc =
Description.createTestDescription(klass, "RestoreDefaultNetwork")
- private val restoreDefaultNetwork = klass.isAnnotationPresent(RestoreDefaultNetwork::class.java)
val ctx = ApplicationProvider.getApplicationContext<Context>()
+ private val restoreDefaultNetwork =
+ klass.isAnnotationPresent(RestoreDefaultNetwork::class.java) &&
+ !ctx.applicationInfo.isInstantApp()
// Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the
// Java bytecode doesn't have a way to express this. Give this type a name by wrapping it.
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
index 88c2738..5ca7fcc 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSSatelliteNetworkTest.kt
@@ -54,6 +54,7 @@
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@@ -162,6 +163,43 @@
doTestSatelliteNeverBecomeDefaultNetwork(restricted = false)
}
+ private fun doTestUnregisterAfterReplacementSatisfier(destroyed: Boolean) {
+ val satelliteAgent = createSatelliteAgent("satellite0")
+ satelliteAgent.connect()
+
+ val uids = setOf(TEST_PACKAGE_UID)
+ updateSatelliteNetworkFallbackUids(uids)
+
+ if (destroyed) {
+ satelliteAgent.unregisterAfterReplacement(timeoutMs = 5000)
+ }
+
+ updateSatelliteNetworkFallbackUids(setOf())
+ if (destroyed) {
+ // If the network is already destroyed, networkRemoveUidRangesParcel should not be
+ // called.
+ verify(netd, never()).networkRemoveUidRangesParcel(any())
+ } else {
+ verify(netd).networkRemoveUidRangesParcel(
+ NativeUidRangeConfig(
+ satelliteAgent.network.netId,
+ toUidRangeStableParcels(uidRangesForUids(uids)),
+ PREFERENCE_ORDER_SATELLITE_FALLBACK
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testUnregisterAfterReplacementSatisfier_destroyed() {
+ doTestUnregisterAfterReplacementSatisfier(destroyed = true)
+ }
+
+ @Test
+ fun testUnregisterAfterReplacementSatisfier_notDestroyed() {
+ doTestUnregisterAfterReplacementSatisfier(destroyed = false)
+ }
+
private fun assertCreateMultiLayerNrisFromSatelliteNetworkPreferredUids(uids: Set<Int>) {
val nris: Set<ConnectivityService.NetworkRequestInfo> =
service.createMultiLayerNrisFromSatelliteNetworkFallbackUids(uids)
diff --git a/thread/README.md b/thread/README.md
index 41b73ac..f2bd3b2 100644
--- a/thread/README.md
+++ b/thread/README.md
@@ -16,3 +16,7 @@
Open `https://localhost:8443/` in your web browser, you can find the Thread
demoapp (with the Thread logo) in the cuttlefish instance. Open it and have fun with Thread!
+
+## More docs
+
+- [Make your Android Border Router](./docs/make-your-android-border-router.md)
diff --git a/thread/docs/android-thread-arch.png b/thread/docs/android-thread-arch.png
new file mode 100644
index 0000000..ea408fa
--- /dev/null
+++ b/thread/docs/android-thread-arch.png
Binary files differ
diff --git a/thread/docs/build-an-android-border-router.md b/thread/docs/build-an-android-border-router.md
new file mode 100644
index 0000000..257999b
--- /dev/null
+++ b/thread/docs/build-an-android-border-router.md
@@ -0,0 +1,526 @@
+# Build an Android Border Router
+
+If you are not an Android device or Thread chip vendor, you can stop reading
+now.
+
+This document walks you through the steps to build a new Android-based Thread
+Border Router device with the latest AOSP source code. By following this
+document, you will learn:
+
+1. [the overall architecture and status of Thread support in Android](#architecture)
+2. [how to create your own Thread HAL service](#build-your-thread-hal-service)
+3. [how to make your device compatible with Google Home](#be-compatible-with-google-home)
+4. [how to test your Thread Border Router](#testing)
+
+If you need support, file an issue in
+[GitHub](https://github.com/openthread/ot-br-posix/issues) or open a
+[Dicussion](https://github.com/orgs/openthread/discussions) if you have any
+questions.
+
+Note: Before creating an issue or discussion, search to see if it has already
+been reported or asked.
+
+## Overview
+
+The Android Thread stack is based on OpenThread and `ot-br-posix` which are
+open-sourced by Google in GitHub. The same way OpenThread is developed in a
+public GitHub repository, so the Android Thread stack is developed in the
+public [AOSP codebase](https://cs.android.com/). All features and bug fixes
+are submitted first in AOSP. This allows vendors to start adopting the latest
+Thread versions without waiting for regular Android releases.
+
+### Architecture
+
+The whole Android Thread stack consists of two major components: the core
+Thread stack in a generic system partition and the Thread HAL service in a
+vendor partition. Device vendors typically need only to take care and build the
+HAL service.
+
+
+
+Here is a brief summary of how the Android Thread stack works:
+- There is a Java Thread system service in the system server which manages the
+ whole stack - provides the Thread system API, creates the `thread-wpan`
+ tunnel interface, registers the Thread network to the
+ [Connectivity service](https://developer.android.com/reference/android/content/Context#CONNECTIVITY_SERVICE)
+ and implements the Border Routing and Advertising Proxy functionalities.
+- The core Thread / OpenThread stack is hosted in a non-privileged standalone
+ native process which is named `ot-daemon`. `ot-daemon` is directly managed
+ by the Java system service via private AIDL APIs and it accesses the Thread
+ hardware radio through the Thread HAL API.
+- A vendor-provided Thread HAL service MUST implement the Thread HAL API. It
+ typically works as an
+ [RCP](https://openthread.io/platforms/co-processor#radio_co-processor_rcp)
+ and implements the
+ [spinel](https://openthread.io/platforms/co-processor#spinel_protocol)
+ protocol.
+
+Note: Both the Java system service and ot-daemon are delivered in a
+[Tethering](https://source.android.com/docs/core/ota/modular-system/tethering#overview)
+mainline module. For mobile devices, the binary is managed by Google and
+delivered to devices via Google Play monthly. For non-mobile devices such as
+TV, vendors are free to build from source or use a prebuilt.
+
+### Where is the code?
+
+- The Android Thread framework / API and service: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Connectivity/thread/
+- The Thread HAL API and default service implementation: https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/threadnetwork/
+- Imported OpenThread repo: https://cs.android.com/android/platform/superproject/main/+/main:external/openthread/
+- Imported ot-br-posix repo: https://cs.android.com/android/platform/superproject/main/+/main:external/ot-br-posix/
+
+## Set up development environment
+
+Android device vendors who have already established an Android development
+environment for the device can skip this section.
+
+If you are new to Android ecosystem or you are a silicon vendor who wants to
+make your Thread chip compatible with Android and provide support for device
+vendors, keep reading.
+
+### Follow the Android developer codelab
+
+To set up your Android development environment for the first time, use the
+following codelab: https://source.android.com/docs/setup/start. At the end of
+this codelab, you will be able to build and run a simulated Cuttlefish device
+from source code.
+
+## Build your Thread HAL service
+
+### Try Thread in Cuttlefish
+
+[Cuttlefish](https://source.android.com/docs/devices/Cuttlefish) is the virtual
+Android device. Before starting building your own HAL service, it's better to
+try Thread in Cuttlefish to understand how HAL works.
+
+A default Thread HAL service is provided in Cuttlefish and it's implemented
+with the [simulated RCP](https://github.com/openthread/openthread/tree/main/examples/platforms/simulation)
+which transceives packets via UDP socket to and from a simulated
+Thread (802.15.4) radio.
+
+In the Cuttlefish instance, a "ThreadNetworkDemoApp" is pre-installed. Open
+that app to join the Cuttlefish device into a default Thread network.
+
+
+
+Note: The Border Router functionalities will be started (and OMR address will
+be created) only when the Cuttlefish device is connected to a virtual Wi-Fi
+network. You can connect the Wi-Fi network in Settings.
+
+There are also the `ot-ctl` and `ot-cli-ftd` command line tools provided to
+configure your Thread network for testing. Those tools support all the
+OpenThread CLI commands that you may be familiar with already.
+
+You can grep for logs of the Cuttlefish Thread HAL service by:
+
+```
+$ adb logcat | egrep -i threadnetwork-service
+
+07-21 10:43:05.048 0 0 I init : Parsing file /apex/com.android.hardware.threadnetwork/etc/threadnetwork-service.rc...
+07-21 10:59:27.233 580 580 W android.hardware.threadnetwork-service: ThreadChip binder is unlinked
+07-21 10:59:27.233 580 580 I android.hardware.threadnetwork-service: Close IThreadChip successfully
+07-21 10:59:27.385 580 580 I android.hardware.threadnetwork-service: Open IThreadChip successfully
+```
+
+Or grep for ot-daemon logs by:
+
+```
+$ adb logcat | egrep -i ot-daemon
+07-21 10:43:48.741 0 0 I init : starting service 'ot-daemon'...
+07-21 10:43:48.742 0 0 I init : Created socket '/dev/socket/ot-daemon/thread-wpan.sock', mode 660, user 1084, group 1084
+07-21 10:43:48.762 0 0 I init : ... started service 'ot-daemon' has pid 2473
+07-21 10:46:26.320 2473 2473 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-21 10:46:30.290 2473 2473 W ot-daemon: [W] P-Daemon------: Daemon read: Connection reset by peer
+07-21 10:48:07.264 2473 2473 I ot-daemon: [INFO]-BINDER--: Start joining...
+07-21 10:48:07.267 2473 2473 I ot-daemon: [I] Settings------: Saved ActiveDataset
+07-21 10:48:07.267 2473 2473 I ot-daemon: [I] DatasetManager: Active dataset set
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] DnssdServer---: Started
+07-21 10:48:07.273 2473 2473 I ot-daemon: [N] Mle-----------: Role disabled -> detached
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] Mle-----------: AttachState Idle -> Start
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] Notifier------: StateChanged (0x111fd11d) [Ip6+ Role LLAddr MLAddr KeySeqCntr Ip6Mult+ Channel PanId NetName ExtPanId ...
+07-21 10:48:07.273 2473 2473 I ot-daemon: [I] Notifier------: StateChanged (0x111fd11d) ... NetworkKey PSKc SecPolicy NetifState ActDset]
+```
+
+Note: You can also capture Thread system server log with the tag
+"ThreadNetworkService".
+
+The Cuttlefish Thread HAL service uses the default Thread HAL service plus the
+OpenThread simulated RCP binary, see the next section for how it works.
+
+### The default HAL service
+
+A [default HAL service](https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/threadnetwork/aidl/default/)
+is included along with the Thread HAL API. The default HAL service supports
+both simulated and real RCP devices. It receives an optional RCP device URL and
+if the URL is not provided, it defaults to the simulated RCP device.
+
+In file `hardware/interfaces/threadnetwork/aidl/default/threadnetwork-service.rc`:
+
+```
+service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service
+ class hal
+ user thread_network
+```
+
+This is equivalent to:
+
+```
+service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service spinel+hdlc+forkpty:///apex/com.android.hardware.threadnetwork/bin/ot-rcp?forkpty-arg=1
+ class hal
+ user thread_network
+```
+
+For real RCP devices, it supports both SPI and UART interace and you can
+specify the device with the schema `spinel+spi://`, `spinel+hdlc+uart://` and
+`spinel+socket://` respectively.
+
+Note: `spinel+socket://` is a new spinel interface added in the default Thread
+HAL, it supports transmitting spinel frame via an Unix socket. A full
+socket-based radio URL may be like
+`spinel+socket:///data/vendor/threadnetwork/thread_spinel_socket`.
+
+#### Understand the vendor APEX
+
+Similar to the Thread stack in the Tethering mainline module, the default Thread
+HAL service in Cuttlefish is packaged in an APEX module as well. But it's a
+vendor APEX module which will be installed to `/vendor/apex/` (The artifacts in
+the module will be unzipped to `/apex/com.android.hardware.threadnetwork/`).
+
+```aidl
+apex {
+ name: "com.android.hardware.threadnetwork",
+ manifest: "manifest.json",
+ file_contexts: "file_contexts",
+ key: "com.android.hardware.key",
+ certificate: ":com.android.hardware.certificate",
+ updatable: false,
+ vendor: true,
+
+ binaries: [
+ "android.hardware.threadnetwork-service",
+ "ot-rcp",
+ ],
+
+ prebuilts: [
+ "threadnetwork-default.xml", // vintf_fragment
+ "threadnetwork-service.rc", // init_rc
+ "android.hardware.thread_network.prebuilt.xml", // permission
+ ],
+}
+```
+
+There are a few important configurations that you will need to pay attention or
+make changes to when building your own HAL APEX module:
+
+- `file_contexts`: This describes the binary / data files delivered in this
+ APEX module or files the HAL service need to access (for example, the RCP
+ device). This allows you to specify specific sepolicy rules for your HAL
+ service to access the hardware RCP device.
+- `binaries`: The binary file delivered in this APEX module
+- `threadnetwork-service.rc`: How the HAL service will be started. You need to
+ specify the RCP device path here.
+- `android.hardware.thread_network.prebuilt.xml`: Defines the
+ `android.hardware.thread_network` hardware feature. This is required for the
+ Android system to know that your device does have Thread hardware support.
+ Otherwise, the Android Thread stack won't be enabled.
+
+### Create your HAL service
+
+Whether you are an Android device developer or a silicon vendor, you should be
+familiar with building OT RCP firmware for your Thread chip. The following
+instructions assume that the hardware chip is correctly wired and
+validated.
+
+The simplest way to build your HAL APEX is to create a new APEX with the
+binaries and prebuilts of the default HAL APEX. For example, if your company is
+Banana and the RCP device on your device is `/dev/ttyACM0`, your Thread HAL
+APEX will look like this:
+
+- `Android.bp`:
+ ```
+ prebuilt_etc {
+ name: "banana-threadnetwork-service.rc",
+ src: "banana-threadnetwork-service.rc",
+ installable: false,
+ }
+
+ apex {
+ name: "com.banana.android.hardware.threadnetwork",
+ manifest: "manifest.json",
+ file_contexts: "file_contexts",
+ key: "com.android.hardware.key",
+ certificate: ":com.android.hardware.certificate",
+ updatable: false,
+ vendor: true,
+
+ binaries: [
+ "android.hardware.threadnetwork-service",
+ ],
+
+ prebuilts: [
+ "banana-threadnetwork-service.rc",
+ "threadnetwork-default.xml",
+ "android.hardware.thread_network.prebuilt.xml",
+ ],
+ }
+ ```
+- `file_contexts`:
+ ```
+ (/.*)? u:object_r:vendor_file:s0
+ /etc(/.*)? u:object_r:vendor_configs_file:s0
+ /bin/hw/android\.hardware\.threadnetwork-service u:object_r:hal_threadnetwork_default_exec:s0
+ /dev/ttyACM0 u:object_r:threadnetwork_rcp_device:s0
+ ```
+ The file paths in the first column are related to `/apex/com.android.hardware.threadnetwork/`.
+- `threadnetwork-service.rc`:
+ ```
+ service vendor.threadnetwork_hal /apex/com.android.hardware.threadnetwork/bin/hw/android.hardware.threadnetwork-service spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=115200
+ class hal
+ user root
+ ```
+- `manifest.json`:
+ ```
+ {
+ "name": "com.android.hardware.threadnetwork",
+ "version": 1
+ }
+ ```
+
+Note: The default Thread HAL service is not designed to be a generic system
+component which works for all Android devices. If the default implementation
+can't support your device, you are free to make a copy and change it for your
+needs. In this case, it's just simpler to create a new APEX module without
+overriding the default one.
+
+Assuming you are making a new device named Orange, your device specific
+configuration directory will be like:
+
+```
+device/banana/orange/threadnetwork/
+ sepolicy/
+ Android.bp
+ file_contexts
+ manifest.json
+ threadnetwork-default.xml
+ threadnetwork-service.rc
+```
+
+See the next section for what sepolicy rules should be added in the `sepolicy/`
+sub-directory.
+
+#### Sepolicy rules for RCP device
+
+By default, your Thread HAL service doesn't have access to the RCP device (for
+example `/dev/ttyACM0`), custom sepolicy rules need to be added to the
+`sepolicy/` directory.
+
+Create a new `sepolicy/threadnetwork_hal.te` file with below content:
+
+```
+type threadnetwork_rcp_device, dev_type;
+
+# Allows the Thread HAL service to read / write the Thread RCP device
+allow hal_threadnetwork_default threadnetwork_rcp_device:chr_file rw_file_perms;
+```
+
+#### Put together
+
+Now you have finished almost all the code needs for adding Thread, the last
+step is to add the Thread HAL APEX and sepolicy rules to your device's image.
+
+You can do this by adding below code to your device's `Makefile` (for example,
+`device.mk`):
+
+```
+PRODUCT_PACKAGES += com.banana.hardware.threadnetwork
+BOARD_SEPOLICY_DIRS += device/banana/orange/threadnetwork/sepolicy
+```
+
+Note: Unfortunately, APEX module doesn't support sepolicy rules, so you need
+to explicitly specify the sepolicy directory separately.
+
+If everything works, now you will be able to see the Thread HAL service log similar to:
+
+```
+$ adb logcat | egrep -i threadnetwork-service
+08-13 13:26:41.751 477 477 I android.hardware.threadnetwork-service: ServiceName: android.hardware.threadnetwork.IThreadChip/chip0, Url: spinel+spi
+08-13 13:26:41.751 477 477 I android.hardware.threadnetwork-service: Thread Network HAL is running
+08-13 13:26:55.165 477 477 I android.hardware.threadnetwork-service: Open IThreadChip successfully
+```
+
+And the `ot-daemon` log will be like:
+
+```
+$ adb logcat -s ot-daemon
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Running OTBR_AGENT/Unknown
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Thread version: 1.3.0
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Thread interface: thread-wpan
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Backbone interface is not specified
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-AGENT---: Radio URL: threadnetwork_hal://binder?none
+08-13 13:26:55.157 1019 1019 I ot-daemon: [NOTE]-ILS-----: Infra link selected:
+08-13 13:26:55.160 1019 1019 I ot-daemon: [I] Platform------: [HAL] Wait for getting the service android.hardware.threadnetwork.IThreadChip/chip0 ...
+08-13 13:26:55.165 1019 1019 I ot-daemon: [I] Platform------: [HAL] Successfully got the service android.hardware.threadnetwork.IThreadChip/chip0
+08-13 13:26:55.275 1019 1019 I ot-daemon: [I] P-RadioSpinel-: RCP reset: RESET_UNKNOWN
+08-13 13:26:55.276 1019 1019 I ot-daemon: [I] P-RadioSpinel-: Software reset RCP successfully
+08-13 13:26:55.277 1019 1019 I ot-daemon: [I] P-RadioSpinel-: RCP reset: RESET_POWER_ON
+08-13 13:26:55.322 1019 1019 I ot-daemon: [I] ChildSupervsn-: Timeout: 0 -> 190
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] RoutingManager: Initializing - InfraIfIndex:0
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] InfraIf-------: Init infra netif 0
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] Settings------: Read BrUlaPrefix fd7b:cc45:ff06::/48
+08-13 13:26:55.324 1019 1019 I ot-daemon: [N] RoutingManager: BR ULA prefix: fd7b:cc45:ff06::/48 (loaded)
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] RoutingManager: Generated local OMR prefix: fd7b:cc45:ff06:1::/64
+08-13 13:26:55.324 1019 1019 I ot-daemon: [N] RoutingManager: Local on-link prefix: fdde:ad00:beef:cafe::/64
+08-13 13:26:55.324 1019 1019 I ot-daemon: [I] RoutingManager: Enabling
+```
+
+## Customization
+
+The Thread mainline module (it's actually a part of the "Tethering" module)
+provides a few [overlayable configurations](https://source.android.com/docs/core/runtime/rros)
+which can be specified by vendors to customize the stack behavior. See
+[config_thread.xml](https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Connectivity/service/ServiceConnectivityResources/res/values/config_thread.xml)
+for the full list.
+
+Typically, you must change the `config_thread_vendor_name`,
+`config_thread_vendor_oui` and `config_thread_model_name` to your vendor or
+product values. Those values will be included in the `_meshcop._udp` mDNS
+service which is always advertised by a Thread Border Router.
+
+To add the overlay, you need to create a new `ConnectivityOverlayOrange`
+runtime_resource_overlay target for your Orange device. Create a new
+`ConnectivityOverlay/` directory under `device/banana/orange/rro_overlays` and
+create below contents in it:
+
+```
+device/banana/orange/rro_overlays/ConnectivityOverlay/
+ res
+ values
+ config_thread.xml
+ Android.bp
+ AndroidManifest.xml
+```
+
+- `Android.bp`:
+ ```
+ package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+ }
+
+ runtime_resource_overlay {
+ name: "ConnectivityOverlayOrange",
+ manifest: "AndroidManifest.xml",
+ resource_dirs: ["res"],
+ certificate: "platform",
+ product_specific: true,
+ sdk_version: "current",
+ }
+ ```
+- `AndroidManifest.xml`:
+ ```
+ <!-- Orange overlays for the Connectivity module -->
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.banana.android.connectivity.resources.orange"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <application android:hasCode="false" />
+
+ <!-- If your device uses google-signed mainline modules, the targetPackage
+ needs to be "com.google.android.connectivity.resources", otherise, it
+ should be "com.android.connectivity.resources"
+ -->
+ <overlay
+ android:targetPackage="com.google.android.connectivity.resources"
+ android:targetName="ServiceConnectivityResourcesConfig"
+ android:isStatic="true"
+ android:priority="1"/>
+ </manifest>
+ ```
+- `config_thread.xml`:
+ ```
+ <string translatable="false" name="config_thread_vendor_name">Banana Inc.</string>
+ <string translatable="false" name="config_thread_vendor_oui">AC:DE:48</string>
+ <string translatable="false" name="config_thread_model_name">Orange</string>
+ ```
+
+Similar to the HAL APEX, you need to add the overlay app to your `device.mk`
+file:
+
+```
+PRODUCT_PACKAGES += \
+ ConnectivityOverlayOrange
+```
+
+If everything works, you will see that `ot-daemon` logs the vendor and model name
+at the very beginning of the log:
+```
+$ adb logcat -s ot-daemon
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] Cli-----------: Input: state
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] Cli-----------: Output: disabled
+07-22 15:31:37.693 1472 1472 I ot-daemon: [I] Cli-----------: Output: Done
+07-22 15:31:37.693 1472 1472 W ot-daemon: [W] P-Daemon------: Daemon read: Connection reset by peer
+07-22 15:31:50.091 1472 1472 I ot-daemon: [I] P-Daemon------: Session socket is ready
+07-22 15:31:50.091 1472 1472 I ot-daemon: [I] Cli-----------: Input: factoryreset
+07-22 15:31:50.092 1472 1472 I ot-daemon: [I] Settings------: Wiped all info
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-ADPROXY-: Stopped
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-DPROXY--: Stopped
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-BA------: Stop Thread Border Agent
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-BA------: Unpublish meshcop service Banana Inc. Orange #4833._meshcop._udp.local
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-MDNS----: Removing service Banana Inc. Orange #4833._meshcop._udp
+07-22 15:31:50.092 1472 1472 I ot-daemon: [INFO]-MDNS----: Unpublishing service Banana Inc. Orange #4833._meshcop._udp listener ID = 0
+```
+
+Note: In case the overlay doesn't work, check https://source.android.com/docs/core/runtime/rro-troubleshoot
+for troubleshooting instructions.
+
+### Be compatible with Google Home
+
+Additionally, if you want to make your Border Router be used by the Google Home
+ecosystem, you can specify this configuration in `config_thread.xml`:
+
+```
+<string-array name="config_thread_mdns_vendor_specific_txts">
+ <item>vgh=1</item>
+</string-array>
+```
+
+## Testing
+
+Your device should be compatible with the Thread 1.3+ Border Router
+specification now. Before sending it to the Thread certification program, there
+are a few Android xTS tests should be exercised to ensure the compatibility.
+
+- The VTS test makes sure Thread HAL service work as expected on your device.
+ You can run the tests with command
+ ```
+ atest VtsHalThreadNetworkTargetTest
+ ```
+- The CTS test makes sure Thread APIs work as expected on your device. You can
+ run the tests with command
+ ```
+ atest CtsThreadNetworkTestCases
+ ```
+- The integration test provides more quality guarantee of how the Thread
+ mainline code works on your device. You can run the tests with command
+ ```
+ atest ThreadNetworkIntegrationTests
+ ```
+
+You can also find more instructions of how to run VTS/CTS/MTS tests with those
+released test suites:
+
+- https://source.android.com/docs/core/tests/vts
+- https://source.android.com/docs/compatibility/cts/run
+- https://docs.partner.android.com/mainline/test/mts (you need to be a partner to access this link)
+
+### Test with the Thread demo app
+
+Similar to the Cuttlefish device, you can add the Thread demo app to your system image:
+
+```
+# ThreadNetworkDemoApp for testing
+PRODUCT_PACKAGES_DEBUG += ThreadNetworkDemoApp
+```
+
+Note that you should add it to only the debug / eng variant (for example,
+`PRODUCT_PACKAGES_DEBUG`) given this is not supposed to be included in user
+build for end consumers.
diff --git a/thread/docs/demoapp-screenshot.png b/thread/docs/demoapp-screenshot.png
new file mode 100644
index 0000000..fa7f079
--- /dev/null
+++ b/thread/docs/demoapp-screenshot.png
Binary files differ