Merge "Add test for IPv4 UDP forwarding rules in BPF map"

This commit is contained in:
Nucca Chen
2022-02-17 02:59:07 +00:00
committed by Gerrit Code Review
2 changed files with 146 additions and 6 deletions

View File

@@ -30,6 +30,7 @@ java_defaults {
"androidx.test.rules", "androidx.test.rules",
"mockito-target-extended-minus-junit4", "mockito-target-extended-minus-junit4",
"net-tests-utils", "net-tests-utils",
"net-utils-device-common-bpf",
"testables", "testables",
], ],
libs: [ libs: [

View File

@@ -18,6 +18,7 @@ package android.net;
import static android.Manifest.permission.ACCESS_NETWORK_STATE; import static android.Manifest.permission.ACCESS_NETWORK_STATE;
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS; import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.DUMP;
import static android.Manifest.permission.MANAGE_TEST_NETWORKS; import static android.Manifest.permission.MANAGE_TEST_NETWORKS;
import static android.Manifest.permission.NETWORK_SETTINGS; import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.Manifest.permission.TETHER_PRIVILEGED; import static android.Manifest.permission.TETHER_PRIVILEGED;
@@ -53,11 +54,15 @@ import android.net.TetheringManager.StartTetheringCallback;
import android.net.TetheringManager.TetheringEventCallback; import android.net.TetheringManager.TetheringEventCallback;
import android.net.TetheringManager.TetheringRequest; import android.net.TetheringManager.TetheringRequest;
import android.net.TetheringTester.TetheredDevice; import android.net.TetheringTester.TetheredDevice;
import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.SystemClock; import android.os.SystemClock;
import android.os.SystemProperties; import android.os.SystemProperties;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -67,17 +72,24 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.net.module.util.PacketBuilder; import com.android.net.module.util.PacketBuilder;
import com.android.net.module.util.Struct; import com.android.net.module.util.Struct;
import com.android.net.module.util.bpf.Tether4Key;
import com.android.net.module.util.bpf.Tether4Value;
import com.android.net.module.util.structs.EthernetHeader; import com.android.net.module.util.structs.EthernetHeader;
import com.android.net.module.util.structs.Icmpv6Header; import com.android.net.module.util.structs.Icmpv6Header;
import com.android.net.module.util.structs.Ipv4Header; import com.android.net.module.util.structs.Ipv4Header;
import com.android.net.module.util.structs.Ipv6Header; import com.android.net.module.util.structs.Ipv6Header;
import com.android.net.module.util.structs.UdpHeader; import com.android.net.module.util.structs.UdpHeader;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DumpTestUtils;
import com.android.testutils.HandlerUtils; import com.android.testutils.HandlerUtils;
import com.android.testutils.TapPacketReader; import com.android.testutils.TapPacketReader;
import com.android.testutils.TestNetworkTracker; import com.android.testutils.TestNetworkTracker;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -88,9 +100,12 @@ import java.net.InterfaceAddress;
import java.net.NetworkInterface; import java.net.NetworkInterface;
import java.net.SocketException; import java.net.SocketException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -101,10 +116,17 @@ import java.util.concurrent.TimeoutException;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@MediumTest @MediumTest
public class EthernetTetheringTest { public class EthernetTetheringTest {
@Rule
public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
private static final String TAG = EthernetTetheringTest.class.getSimpleName(); private static final String TAG = EthernetTetheringTest.class.getSimpleName();
private static final int TIMEOUT_MS = 5000; private static final int TIMEOUT_MS = 5000;
private static final int TETHER_REACHABILITY_ATTEMPTS = 20; private static final int TETHER_REACHABILITY_ATTEMPTS = 20;
private static final int DUMP_POLLING_MAX_RETRY = 100;
private static final int DUMP_POLLING_INTERVAL_MS = 50;
// Kernel treats a confirmed UDP connection which active after two seconds as stream mode.
// See upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5.
private static final int UDP_STREAM_TS_MS = 2000;
private static final LinkAddress TEST_IP4_ADDR = new LinkAddress("10.0.0.1/8"); private static final LinkAddress TEST_IP4_ADDR = new LinkAddress("10.0.0.1/8");
private static final LinkAddress TEST_IP6_ADDR = new LinkAddress("2001:db8:1::101/64"); private static final LinkAddress TEST_IP6_ADDR = new LinkAddress("2001:db8:1::101/64");
private static final InetAddress TEST_IP4_DNS = parseNumericAddress("8.8.8.8"); private static final InetAddress TEST_IP4_DNS = parseNumericAddress("8.8.8.8");
@@ -112,6 +134,10 @@ public class EthernetTetheringTest {
private static final ByteBuffer TEST_REACHABILITY_PAYLOAD = private static final ByteBuffer TEST_REACHABILITY_PAYLOAD =
ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa }); ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa });
private static final String DUMPSYS_TETHERING_RAWMAP_ARG = "bpfRawMap";
private static final String BASE64_DELIMITER = ",";
private static final String LINE_DELIMITER = "\\n";
private final Context mContext = InstrumentationRegistry.getContext(); private final Context mContext = InstrumentationRegistry.getContext();
private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class); private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class); private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
@@ -136,10 +162,11 @@ public class EthernetTetheringTest {
// Needed to create a TestNetworkInterface, to call requestTetheredInterface, and to receive // Needed to create a TestNetworkInterface, to call requestTetheredInterface, and to receive
// tethered client callbacks. The restricted networks permission is needed to ensure that // tethered client callbacks. The restricted networks permission is needed to ensure that
// EthernetManager#isAvailable will correctly return true on devices where Ethernet is // EthernetManager#isAvailable will correctly return true on devices where Ethernet is
// marked restricted, like cuttlefish. // marked restricted, like cuttlefish. The dump permission is needed to verify bpf related
// functions via dumpsys output.
mUiAutomation.adoptShellPermissionIdentity( mUiAutomation.adoptShellPermissionIdentity(
MANAGE_TEST_NETWORKS, NETWORK_SETTINGS, TETHER_PRIVILEGED, ACCESS_NETWORK_STATE, MANAGE_TEST_NETWORKS, NETWORK_SETTINGS, TETHER_PRIVILEGED, ACCESS_NETWORK_STATE,
CONNECTIVITY_USE_RESTRICTED_NETWORKS); CONNECTIVITY_USE_RESTRICTED_NETWORKS, DUMP);
mRunTests = mTm.isTetheringSupported() && mEm != null; mRunTests = mTm.isTetheringSupported() && mEm != null;
assumeTrue(mRunTests); assumeTrue(mRunTests);
@@ -747,12 +774,15 @@ public class EthernetTetheringTest {
private static final byte TYPE_OF_SERVICE = 0; private static final byte TYPE_OF_SERVICE = 0;
private static final short ID = 27149; private static final short ID = 27149;
private static final short ID2 = 27150; private static final short ID2 = 27150;
private static final short ID3 = 27151;
private static final short FLAGS_AND_FRAGMENT_OFFSET = (short) 0x4000; // flags=DF, offset=0 private static final short FLAGS_AND_FRAGMENT_OFFSET = (short) 0x4000; // flags=DF, offset=0
private static final byte TIME_TO_LIVE = (byte) 0x40; private static final byte TIME_TO_LIVE = (byte) 0x40;
private static final ByteBuffer PAYLOAD = private static final ByteBuffer PAYLOAD =
ByteBuffer.wrap(new byte[] { (byte) 0x12, (byte) 0x34 }); ByteBuffer.wrap(new byte[] { (byte) 0x12, (byte) 0x34 });
private static final ByteBuffer PAYLOAD2 = private static final ByteBuffer PAYLOAD2 =
ByteBuffer.wrap(new byte[] { (byte) 0x56, (byte) 0x78 }); ByteBuffer.wrap(new byte[] { (byte) 0x56, (byte) 0x78 });
private static final ByteBuffer PAYLOAD3 =
ByteBuffer.wrap(new byte[] { (byte) 0x9a, (byte) 0xbc });
private boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEther, private boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEther,
@NonNull final ByteBuffer payload) { @NonNull final ByteBuffer payload) {
@@ -830,7 +860,8 @@ public class EthernetTetheringTest {
return false; return false;
} }
private void runUdp4Test(TetheringTester tester, RemoteResponder remote) throws Exception { private void runUdp4Test(TetheringTester tester, RemoteResponder remote, boolean usingBpf)
throws Exception {
final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString( final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
"1:2:3:4:5:6")); "1:2:3:4:5:6"));
@@ -861,10 +892,51 @@ public class EthernetTetheringTest {
Log.d(TAG, "Packet in downstream: " + dumpHexString(p)); Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
return isExpectedUdpPacket(p, true/* hasEther */, PAYLOAD2); return isExpectedUdpPacket(p, true/* hasEther */, PAYLOAD2);
}); });
if (usingBpf) {
// Send second UDP packet in original direction.
// The BPF coordinator only offloads the ASSURED conntrack entry. The "request + reply"
// packets can make status IPS_SEEN_REPLY to be set. Need one more packet to make
// conntrack status IPS_ASSURED_BIT to be set. Note the third packet needs to delay
// 2 seconds because kernel monitors a UDP connection which still alive after 2 seconds
// and apply ASSURED flag.
// See kernel upstream commit b7b1d02fc43925a4d569ec221715db2dfa1ce4f5 and
// nf_conntrack_udp_packet in net/netfilter/nf_conntrack_proto_udp.c
Thread.sleep(UDP_STREAM_TS_MS);
final ByteBuffer originalPacket2 = buildUdpv4Packet(tethered.macAddr,
tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */,
REMOTE_PORT /*dstPort */, PAYLOAD3 /* payload */);
tester.verifyUpload(remote, originalPacket2, p -> {
Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD3);
});
final HashMap<Tether4Key, Tether4Value> upstreamMap = pollIpv4UpstreamMapFromDump();
assertNotNull(upstreamMap);
assertEquals(1, upstreamMap.size());
final Map.Entry<Tether4Key, Tether4Value> rule =
upstreamMap.entrySet().iterator().next();
final Tether4Key key = rule.getKey();
assertEquals(IPPROTO_UDP, key.l4proto);
assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), key.src4));
assertEquals(LOCAL_PORT, key.srcPort);
assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), key.dst4));
assertEquals(REMOTE_PORT, key.dstPort);
final Tether4Value value = rule.getValue();
assertTrue(Arrays.equals(publicIp4Addr.getAddress(),
InetAddress.getByAddress(value.src46).getAddress()));
assertEquals(LOCAL_PORT, value.srcPort);
assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(),
InetAddress.getByAddress(value.dst46).getAddress()));
assertEquals(REMOTE_PORT, value.dstPort);
}
} }
@Test void initializeTethering() throws Exception {
public void testUdpV4() throws Exception {
assumeFalse(mEm.isAvailable()); assumeFalse(mEm.isAvailable());
// MyTetheringEventCallback currently only support await first available upstream. Tethering // MyTetheringEventCallback currently only support await first available upstream. Tethering
@@ -885,8 +957,75 @@ public class EthernetTetheringTest {
mDownstreamReader = makePacketReader(mDownstreamIface); mDownstreamReader = makePacketReader(mDownstreamIface);
mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface()); mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
}
runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader)); @Test
@IgnoreAfter(Build.VERSION_CODES.Q)
public void testTetherUdpV4WithoutBpf() throws Exception {
initializeTethering();
runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
false /* usingBpf */);
}
@Test
@IgnoreUpTo(Build.VERSION_CODES.R)
public void testTetherUdpV4WithBpf() throws Exception {
initializeTethering();
runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
true /* usingBpf */);
}
@Nullable
private Pair<Tether4Key, Tether4Value> parseTether4KeyValue(@NonNull String dumpStr) {
Log.w(TAG, "Parsing string: " + dumpStr);
String[] keyValueStrs = dumpStr.split(BASE64_DELIMITER);
if (keyValueStrs.length != 2 /* key + value */) {
fail("The length is " + keyValueStrs.length + " but expect 2. "
+ "Split string(s): " + TextUtils.join(",", keyValueStrs));
}
final byte[] keyBytes = Base64.decode(keyValueStrs[0], Base64.DEFAULT);
Log.d(TAG, "keyBytes: " + dumpHexString(keyBytes));
final ByteBuffer keyByteBuffer = ByteBuffer.wrap(keyBytes);
keyByteBuffer.order(ByteOrder.nativeOrder());
final Tether4Key tether4Key = Struct.parse(Tether4Key.class, keyByteBuffer);
Log.w(TAG, "tether4Key: " + tether4Key);
final byte[] valueBytes = Base64.decode(keyValueStrs[1], Base64.DEFAULT);
Log.d(TAG, "valueBytes: " + dumpHexString(valueBytes));
final ByteBuffer valueByteBuffer = ByteBuffer.wrap(valueBytes);
valueByteBuffer.order(ByteOrder.nativeOrder());
final Tether4Value tether4Value = Struct.parse(Tether4Value.class, valueByteBuffer);
Log.w(TAG, "tether4Value: " + tether4Value);
return new Pair<>(tether4Key, tether4Value);
}
@NonNull
private HashMap<Tether4Key, Tether4Value> dumpIpv4UpstreamMap() throws Exception {
final String rawMapStr = DumpTestUtils.dumpService(Context.TETHERING_SERVICE,
DUMPSYS_TETHERING_RAWMAP_ARG);
final HashMap<Tether4Key, Tether4Value> map = new HashMap<>();
for (final String line : rawMapStr.split(LINE_DELIMITER)) {
final Pair<Tether4Key, Tether4Value> rule = parseTether4KeyValue(line.trim());
map.put(rule.first, rule.second);
}
return map;
}
@Nullable
private HashMap<Tether4Key, Tether4Value> pollIpv4UpstreamMapFromDump() throws Exception {
for (int retryCount = 0; retryCount < DUMP_POLLING_MAX_RETRY; retryCount++) {
final HashMap<Tether4Key, Tether4Value> map = dumpIpv4UpstreamMap();
if (!map.isEmpty()) return map;
Thread.sleep(DUMP_POLLING_INTERVAL_MS);
}
fail("Cannot get rules after " + DUMP_POLLING_MAX_RETRY * DUMP_POLLING_INTERVAL_MS + "ms");
return null;
} }
private <T> List<T> toList(T... array) { private <T> List<T> toList(T... array) {