Merge changes from topic "nat64-integration-test-case" into main

* changes:
  [Thread] add an integration test case for NAT64
  [Thread] support polling IPv4 packets in integration test
This commit is contained in:
Treehugger Robot 2024-08-07 02:23:23 +00:00 • committed by Gerrit Code Review
commit bf52fa708e
4 changed files with 187 additions and 61 deletions

View file

@ -17,17 +17,20 @@
package android.net.thread;
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.getIpv6LinkAddresses;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv4Packet;
import static android.net.thread.utils.IntegrationTestUtils.isExpectedIcmpv6Packet;
import static android.net.thread.utils.IntegrationTestUtils.isFromIpv6Source;
import static android.net.thread.utils.IntegrationTestUtils.isFrom;
import static android.net.thread.utils.IntegrationTestUtils.isInMulticastGroup;
import static android.net.thread.utils.IntegrationTestUtils.isToIpv6Destination;
import static android.net.thread.utils.IntegrationTestUtils.isTo;
import static android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr;
import static android.net.thread.utils.IntegrationTestUtils.newPacketReader;
import static android.net.thread.utils.IntegrationTestUtils.pollForPacket;
import static android.net.thread.utils.IntegrationTestUtils.sendUdpMessage;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
import static android.system.OsConstants.ICMP_ECHO;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REPLY_TYPE;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
@ -44,11 +47,11 @@ import static org.junit.Assert.assertNull;
import static java.util.Objects.requireNonNull;
import android.content.Context;
import android.net.InetAddresses;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.RouteInfo;
import android.net.thread.utils.FullThreadDevice;
import android.net.thread.utils.InfraNetworkDevice;
import android.net.thread.utils.OtDaemonController;
@ -74,7 +77,9 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@ -89,11 +94,14 @@ public class BorderRoutingTest {
private static final String TAG = BorderRoutingTest.class.getSimpleName();
private static final int NUM_FTD = 2;
private static final Inet6Address GROUP_ADDR_SCOPE_5 =
(Inet6Address) InetAddresses.parseNumericAddress("ff05::1234");
(Inet6Address) parseNumericAddress("ff05::1234");
private static final Inet6Address GROUP_ADDR_SCOPE_4 =
(Inet6Address) InetAddresses.parseNumericAddress("ff04::1234");
(Inet6Address) parseNumericAddress("ff04::1234");
private static final Inet6Address GROUP_ADDR_SCOPE_3 =
(Inet6Address) InetAddresses.parseNumericAddress("ff03::1234");
(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";
@Rule public final ThreadFeatureCheckerRule mThreadRule = new ThreadFeatureCheckerRule();
@ -165,7 +173,7 @@ public class BorderRoutingTest {
mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
// Infra device receives an echo reply sent by FTD.
assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@ -186,7 +194,7 @@ public class BorderRoutingTest {
mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@ -213,7 +221,7 @@ public class BorderRoutingTest {
mInfraDevice.sendEchoRequest(ftd.getOmrAddress());
assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
} finally {
runAsShell(MANAGE_TEST_NETWORKS, () -> oldInfraNetworkTracker.teardown());
}
@ -322,7 +330,7 @@ public class BorderRoutingTest {
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@ -354,7 +362,7 @@ public class BorderRoutingTest {
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_3);
assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
assertNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@ -375,7 +383,7 @@ public class BorderRoutingTest {
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
assertNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
assertNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd.getOmrAddress()));
}
@Test
@ -405,13 +413,15 @@ public class BorderRoutingTest {
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
assertNotNull(
pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
// Verify ping reply from ftd1 and ftd2 separately as the order of replies can't be
// predicted.
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_4);
assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
assertNotNull(
pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
}
@Test
@ -441,12 +451,14 @@ public class BorderRoutingTest {
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
assertNotNull(
pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd1.getOmrAddress()));
// Send the request twice as the order of replies from ftd1 and ftd2 are not guaranteed
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
assertNotNull(
pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftd2.getOmrAddress()));
}
@Test
@ -469,9 +481,11 @@ public class BorderRoutingTest {
ftd.ping(GROUP_ADDR_SCOPE_4);
assertNotNull(
pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
pollForIcmpPacketOnInfraNetwork(
ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_5));
assertNotNull(
pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
pollForIcmpPacketOnInfraNetwork(
ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
}
@Test
@ -493,7 +507,7 @@ public class BorderRoutingTest {
ftd.ping(GROUP_ADDR_SCOPE_3);
assertNull(
pollForPacketOnInfraNetwork(
pollForIcmpPacketOnInfraNetwork(
ICMPV6_ECHO_REQUEST_TYPE, ftd.getOmrAddress(), GROUP_ADDR_SCOPE_3));
}
@ -517,7 +531,8 @@ public class BorderRoutingTest {
ftd.ping(GROUP_ADDR_SCOPE_4, ftdLla);
assertNull(
pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
pollForIcmpPacketOnInfraNetwork(
ICMPV6_ECHO_REQUEST_TYPE, ftdLla, GROUP_ADDR_SCOPE_4));
}
@Test
@ -541,7 +556,7 @@ public class BorderRoutingTest {
ftd.ping(GROUP_ADDR_SCOPE_4, ftdMla);
assertNull(
pollForPacketOnInfraNetwork(
pollForIcmpPacketOnInfraNetwork(
ICMPV6_ECHO_REQUEST_TYPE, ftdMla, GROUP_ADDR_SCOPE_4));
}
}
@ -572,7 +587,7 @@ public class BorderRoutingTest {
mInfraDevice.sendEchoRequest(GROUP_ADDR_SCOPE_5);
assertNotNull(pollForPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMPV6_ECHO_REPLY_TYPE, ftdOmr));
}
@Test
@ -600,18 +615,40 @@ public class BorderRoutingTest {
ftd.ping(GROUP_ADDR_SCOPE_4);
assertNotNull(
pollForPacketOnInfraNetwork(ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
pollForIcmpPacketOnInfraNetwork(
ICMPV6_ECHO_REQUEST_TYPE, ftdOmr, GROUP_ADDR_SCOPE_4));
}
@Test
public void nat64_threadDevicePingIpv4InfraDevice_outboundPacketIsForwarded() throws Exception {
FullThreadDevice ftd = mFtds.get(0);
joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET);
// TODO: enable NAT64 via ThreadNetworkController API instead of ot-ctl
mOtCtl.setNat64Cidr(NAT64_CIDR);
mOtCtl.setNat64Enabled(true);
waitFor(() -> mOtCtl.hasNat64PrefixInNetdata(), Duration.ofSeconds(10));
ftd.ping(IPV4_SERVER_ADDR);
assertNotNull(pollForIcmpPacketOnInfraNetwork(ICMP_ECHO, null, IPV4_SERVER_ADDR));
}
private void setUpInfraNetwork() throws Exception {
LinkProperties lp = new LinkProperties();
// NAT64 feature requires the infra network to have an IPv4 default route.
lp.addRoute(
new RouteInfo(
new IpPrefix("0.0.0.0/0") /* destination */,
null /* gateway */,
null,
RouteInfo.RTN_UNICAST,
1500 /* mtu */));
mInfraNetworkTracker =
runAsShell(
MANAGE_TEST_NETWORKS,
() ->
initTestNetwork(
mContext, new LinkProperties(), 5000 /* timeoutMs */));
mController.setTestNetworkAsUpstreamAndWait(
mInfraNetworkTracker.getTestIface().getInterfaceName());
() -> initTestNetwork(mContext, lp, 5000 /* timeoutMs */));
String infraNetworkName = mInfraNetworkTracker.getTestIface().getInterfaceName();
mController.setTestNetworkAsUpstreamAndWait(infraNetworkName);
}
private void tearDownInfraNetwork() {
@ -648,20 +685,28 @@ public class BorderRoutingTest {
assertInfraLinkMemberOfGroup(address);
}
private byte[] pollForPacketOnInfraNetwork(int type, Inet6Address srcAddress) {
return pollForPacketOnInfraNetwork(type, srcAddress, null);
private byte[] pollForIcmpPacketOnInfraNetwork(int type, InetAddress srcAddress) {
return pollForIcmpPacketOnInfraNetwork(type, srcAddress, null /* destAddress */);
}
private byte[] pollForPacketOnInfraNetwork(
int type, Inet6Address srcAddress, Inet6Address destAddress) {
Predicate<byte[]> filter;
filter =
private byte[] pollForIcmpPacketOnInfraNetwork(
int type, InetAddress srcAddress, InetAddress destAddress) {
if (srcAddress == null && destAddress == null) {
throw new IllegalArgumentException("srcAddress and destAddress cannot be both null");
}
if (srcAddress != null && destAddress != null) {
if ((srcAddress instanceof Inet4Address) != (destAddress instanceof Inet4Address)) {
throw new IllegalArgumentException(
"srcAddress and destAddress must be both IPv4 or both IPv6");
}
}
boolean isIpv4 =
(srcAddress instanceof Inet4Address) || (destAddress instanceof Inet4Address);
final Predicate<byte[]> filter =
p ->
(isExpectedIcmpv6Packet(p, type)
&& (srcAddress == null ? true : isFromIpv6Source(p, srcAddress))
&& (destAddress == null
? true
: isToIpv6Destination(p, destAddress)));
(isIpv4 ? isExpectedIcmpv4Packet(p, type) : isExpectedIcmpv6Packet(p, type))
&& (srcAddress == null || isFrom(p, srcAddress))
&& (destAddress == null || isTo(p, destAddress));
return pollForPacket(mInfraNetworkReader, filter);
}
}

View file

@ -417,7 +417,7 @@ public final class FullThreadDevice {
executeCommand("ipmaddr add " + address.getHostAddress());
}
public void ping(Inet6Address address, Inet6Address source) {
public void ping(InetAddress address, Inet6Address source) {
ping(
address,
source,
@ -428,7 +428,7 @@ public final class FullThreadDevice {
PING_TIMEOUT_0_1_SECOND);
}
public void ping(Inet6Address address) {
public void ping(InetAddress address) {
ping(
address,
null,
@ -440,7 +440,7 @@ public final class FullThreadDevice {
}
/** Returns the number of ping reply packets received. */
public int ping(Inet6Address address, int count) {
public int ping(InetAddress address, int count) {
List<String> output =
ping(
address,
@ -454,7 +454,7 @@ public final class FullThreadDevice {
}
private List<String> ping(
Inet6Address address,
InetAddress address,
Inet6Address source,
int size,
int count,

View file

@ -16,6 +16,7 @@
package android.net.thread.utils;
import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.system.OsConstants.IPPROTO_ICMP;
import static android.system.OsConstants.IPPROTO_ICMPV6;
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
@ -49,7 +50,9 @@ import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import com.android.net.module.util.Struct;
import com.android.net.module.util.structs.Icmpv4Header;
import com.android.net.module.util.structs.Icmpv6Header;
import com.android.net.module.util.structs.Ipv4Header;
import com.android.net.module.util.structs.Ipv6Header;
import com.android.net.module.util.structs.PrefixInformationOption;
import com.android.net.module.util.structs.RaHeader;
@ -62,6 +65,7 @@ import java.io.FileDescriptor;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@ -192,16 +196,36 @@ public final class IntegrationTestUtils {
return null;
}
/** Returns {@code true} if {@code packet} is an ICMPv4 packet of given {@code type}. */
public static boolean isExpectedIcmpv4Packet(byte[] packet, int type) {
ByteBuffer buf = makeByteBuffer(packet);
Ipv4Header header = extractIpv4Header(buf);
if (header == null) {
return false;
}
if (header.protocol != (byte) IPPROTO_ICMP) {
return false;
}
try {
return Struct.parse(Icmpv4Header.class, buf).type == (short) type;
} catch (IllegalArgumentException ignored) {
// It's fine that the passed in packet is malformed because it's could be sent
// by anybody.
}
return false;
}
/** Returns {@code true} if {@code packet} is an ICMPv6 packet of given {@code type}. */
public static boolean isExpectedIcmpv6Packet(byte[] packet, int type) {
if (packet == null) {
ByteBuffer buf = makeByteBuffer(packet);
Ipv6Header header = extractIpv6Header(buf);
if (header == null) {
return false;
}
if (header.nextHeader != (byte) IPPROTO_ICMPV6) {
return false;
}
ByteBuffer buf = ByteBuffer.wrap(packet);
try {
if (Struct.parse(Ipv6Header.class, buf).nextHeader != (byte) IPPROTO_ICMPV6) {
return false;
}
return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
} catch (IllegalArgumentException ignored) {
// It's fine that the passed in packet is malformed because it's could be sent
@ -210,32 +234,66 @@ public final class IntegrationTestUtils {
return false;
}
public static boolean isFromIpv6Source(byte[] packet, Inet6Address src) {
if (packet == null) {
return false;
}
ByteBuffer buf = ByteBuffer.wrap(packet);
try {
return Struct.parse(Ipv6Header.class, buf).srcIp.equals(src);
} catch (IllegalArgumentException ignored) {
// It's fine that the passed in packet is malformed because it's could be sent
// by anybody.
public static boolean isFrom(byte[] packet, InetAddress src) {
if (src instanceof Inet4Address) {
return isFromIpv4Source(packet, (Inet4Address) src);
} else if (src instanceof Inet6Address) {
return isFromIpv6Source(packet, (Inet6Address) src);
}
return false;
}
public static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
if (packet == null) {
return false;
public static boolean isTo(byte[] packet, InetAddress dest) {
if (dest instanceof Inet4Address) {
return isToIpv4Destination(packet, (Inet4Address) dest);
} else if (dest instanceof Inet6Address) {
return isToIpv6Destination(packet, (Inet6Address) dest);
}
ByteBuffer buf = ByteBuffer.wrap(packet);
return false;
}
private static boolean isFromIpv4Source(byte[] packet, Inet4Address src) {
Ipv4Header header = extractIpv4Header(makeByteBuffer(packet));
return header != null && header.srcIp.equals(src);
}
private static boolean isFromIpv6Source(byte[] packet, Inet6Address src) {
Ipv6Header header = extractIpv6Header(makeByteBuffer(packet));
return header != null && header.srcIp.equals(src);
}
private static boolean isToIpv4Destination(byte[] packet, Inet4Address dest) {
Ipv4Header header = extractIpv4Header(makeByteBuffer(packet));
return header != null && header.dstIp.equals(dest);
}
private static boolean isToIpv6Destination(byte[] packet, Inet6Address dest) {
Ipv6Header header = extractIpv6Header(makeByteBuffer(packet));
return header != null && header.dstIp.equals(dest);
}
private static ByteBuffer makeByteBuffer(byte[] packet) {
return packet == null ? null : ByteBuffer.wrap(packet);
}
private static Ipv4Header extractIpv4Header(ByteBuffer buf) {
try {
return Struct.parse(Ipv6Header.class, buf).dstIp.equals(dest);
return Struct.parse(Ipv4Header.class, buf);
} catch (IllegalArgumentException ignored) {
// It's fine that the passed in packet is malformed because it's could be sent
// by anybody.
}
return false;
return null;
}
private static Ipv6Header extractIpv6Header(ByteBuffer buf) {
try {
return Struct.parse(Ipv6Header.class, buf);
} catch (IllegalArgumentException ignored) {
// It's fine that the passed in packet is malformed because it's could be sent
// by anybody.
}
return null;
}
/** Returns the Prefix Information Options (PIO) extracted from an ICMPv6 RA message. */

View file

@ -105,6 +105,29 @@ public final class OtDaemonController {
return prefixes.isEmpty() ? null : prefixes.get(0);
}
/** Enables/Disables NAT64 feature. */
public void setNat64Enabled(boolean enabled) {
executeCommand("nat64 " + (enabled ? "enable" : "disable"));
}
/** Sets the NAT64 CIDR. */
public void setNat64Cidr(String cidr) {
executeCommand("nat64 cidr " + cidr);
}
/** Returns whether there's a NAT64 prefix in network data */
public boolean hasNat64PrefixInNetdata() {
// Example (in the 'Routes' section):
// fdb2:bae3:5b59:2:0:0::/96 sn low c000
List<String> outputLines = executeCommandAndParse("netdata show");
for (String line : outputLines) {
if (line.contains(" sn")) {
return true;
}
}
return false;
}
public String executeCommand(String cmd) {
return SystemUtil.runShellCommand(OT_CTL + " " + cmd);
}