| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.cts.net.hostside; |
| |
| import static android.Manifest.permission.NETWORK_SETTINGS; |
| import static android.net.NetworkCapabilities.TRANSPORT_VPN; |
| import static android.os.Process.INVALID_UID; |
| import static android.system.OsConstants.AF_INET; |
| import static android.system.OsConstants.AF_INET6; |
| import static android.system.OsConstants.ECONNABORTED; |
| import static android.system.OsConstants.IPPROTO_ICMP; |
| import static android.system.OsConstants.IPPROTO_ICMPV6; |
| import static android.system.OsConstants.IPPROTO_TCP; |
| import static android.system.OsConstants.POLLIN; |
| import static android.system.OsConstants.SOCK_DGRAM; |
| import static android.test.MoreAsserts.assertNotEqual; |
| |
| import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; |
| |
| import android.annotation.Nullable; |
| import android.app.DownloadManager; |
| import android.app.DownloadManager.Query; |
| import android.app.DownloadManager.Request; |
| import android.content.BroadcastReceiver; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.database.Cursor; |
| import android.net.ConnectivityManager; |
| import android.net.ConnectivityManager.NetworkCallback; |
| import android.net.LinkProperties; |
| import android.net.Network; |
| import android.net.NetworkCapabilities; |
| import android.net.NetworkRequest; |
| import android.net.Proxy; |
| import android.net.ProxyInfo; |
| import android.net.TransportInfo; |
| import android.net.Uri; |
| import android.net.VpnManager; |
| import android.net.VpnService; |
| import android.net.VpnTransportInfo; |
| import android.net.cts.util.CtsNetUtils.TestNetworkCallback; |
| import android.net.wifi.WifiManager; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.ParcelFileDescriptor; |
| import android.os.Process; |
| import android.os.SystemProperties; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.support.test.uiautomator.UiDevice; |
| import android.support.test.uiautomator.UiObject; |
| import android.support.test.uiautomator.UiSelector; |
| import android.system.ErrnoException; |
| import android.system.Os; |
| import android.system.OsConstants; |
| import android.system.StructPollfd; |
| import android.test.InstrumentationTestCase; |
| import android.test.MoreAsserts; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import com.android.compatibility.common.util.BlockingBroadcastReceiver; |
| import com.android.modules.utils.build.SdkLevel; |
| |
| import java.io.Closeable; |
| import java.io.FileDescriptor; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.DatagramPacket; |
| import java.net.DatagramSocket; |
| import java.net.Inet6Address; |
| import java.net.InetAddress; |
| import java.net.InetSocketAddress; |
| import java.net.ServerSocket; |
| import java.net.Socket; |
| import java.net.UnknownHostException; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Objects; |
| import java.util.Random; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Tests for the VpnService API. |
| * |
| * These tests establish a VPN via the VpnService API, and have the service reflect the packets back |
| * to the device without causing any network traffic. This allows testing the local VPN data path |
| * without a network connection or a VPN server. |
| * |
| * Note: in Lollipop, VPN functionality relies on kernel support for UID-based routing. If these |
| * tests fail, it may be due to the lack of kernel support. The necessary patches can be |
| * cherry-picked from the Android common kernel trees: |
| * |
| * android-3.10: |
| * https://android-review.googlesource.com/#/c/99220/ |
| * https://android-review.googlesource.com/#/c/100545/ |
| * |
| * android-3.4: |
| * https://android-review.googlesource.com/#/c/99225/ |
| * https://android-review.googlesource.com/#/c/100557/ |
| * |
| * To ensure that the kernel has the required commits, run the kernel unit |
| * tests described at: |
| * |
| * https://source.android.com/devices/tech/config/kernel_network_tests.html |
| * |
| */ |
| public class VpnTest extends InstrumentationTestCase { |
| |
| // These are neither public nor @TestApi. |
| // TODO: add them to @TestApi. |
| private static final String PRIVATE_DNS_MODE_SETTING = "private_dns_mode"; |
| private static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = "hostname"; |
| private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic"; |
| private static final String PRIVATE_DNS_SPECIFIER_SETTING = "private_dns_specifier"; |
| |
| public static String TAG = "VpnTest"; |
| public static int TIMEOUT_MS = 3 * 1000; |
| public static int SOCKET_TIMEOUT_MS = 100; |
| public static String TEST_HOST = "connectivitycheck.gstatic.com"; |
| |
| private UiDevice mDevice; |
| private MyActivity mActivity; |
| private String mPackageName; |
| private ConnectivityManager mCM; |
| private WifiManager mWifiManager; |
| private RemoteSocketFactoryClient mRemoteSocketFactoryClient; |
| |
| Network mNetwork; |
| NetworkCallback mCallback; |
| final Object mLock = new Object(); |
| final Object mLockShutdown = new Object(); |
| |
| private String mOldPrivateDnsMode; |
| private String mOldPrivateDnsSpecifier; |
| |
| private boolean supportedHardware() { |
| final PackageManager pm = getInstrumentation().getContext().getPackageManager(); |
| return !pm.hasSystemFeature("android.hardware.type.watch"); |
| } |
| |
| @Override |
| public void setUp() throws Exception { |
| super.setUp(); |
| |
| mNetwork = null; |
| mCallback = null; |
| storePrivateDnsSetting(); |
| |
| mDevice = UiDevice.getInstance(getInstrumentation()); |
| mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(), |
| MyActivity.class, null); |
| mPackageName = mActivity.getPackageName(); |
| mCM = (ConnectivityManager) mActivity.getSystemService(Context.CONNECTIVITY_SERVICE); |
| mWifiManager = (WifiManager) mActivity.getSystemService(Context.WIFI_SERVICE); |
| mRemoteSocketFactoryClient = new RemoteSocketFactoryClient(mActivity); |
| mRemoteSocketFactoryClient.bind(); |
| mDevice.waitForIdle(); |
| } |
| |
| @Override |
| public void tearDown() throws Exception { |
| restorePrivateDnsSetting(); |
| mRemoteSocketFactoryClient.unbind(); |
| if (mCallback != null) { |
| mCM.unregisterNetworkCallback(mCallback); |
| } |
| Log.i(TAG, "Stopping VPN"); |
| stopVpn(); |
| mActivity.finish(); |
| super.tearDown(); |
| } |
| |
| private void prepareVpn() throws Exception { |
| final int REQUEST_ID = 42; |
| |
| // Attempt to prepare. |
| Log.i(TAG, "Preparing VPN"); |
| Intent intent = VpnService.prepare(mActivity); |
| |
| if (intent != null) { |
| // Start the confirmation dialog and click OK. |
| mActivity.startActivityForResult(intent, REQUEST_ID); |
| mDevice.waitForIdle(); |
| |
| String packageName = intent.getComponent().getPackageName(); |
| String resourceIdRegex = "android:id/button1$|button_start_vpn"; |
| final UiObject okButton = new UiObject(new UiSelector() |
| .className("android.widget.Button") |
| .packageName(packageName) |
| .resourceIdMatches(resourceIdRegex)); |
| if (okButton.waitForExists(TIMEOUT_MS) == false) { |
| mActivity.finishActivity(REQUEST_ID); |
| fail("VpnService.prepare returned an Intent for '" + intent.getComponent() + "' " + |
| "to display the VPN confirmation dialog, but this test could not find the " + |
| "button to allow the VPN application to connect. Please ensure that the " + |
| "component displays a button with a resource ID matching the regexp: '" + |
| resourceIdRegex + "'."); |
| } |
| |
| // Click the button and wait for RESULT_OK. |
| okButton.click(); |
| try { |
| int result = mActivity.getResult(TIMEOUT_MS); |
| if (result != MyActivity.RESULT_OK) { |
| fail("The VPN confirmation dialog did not return RESULT_OK when clicking on " + |
| "the button matching the regular expression '" + resourceIdRegex + |
| "' of " + intent.getComponent() + "'. Please ensure that clicking on " + |
| "that button allows the VPN application to connect. " + |
| "Return value: " + result); |
| } |
| } catch (InterruptedException e) { |
| fail("VPN confirmation dialog did not return after " + TIMEOUT_MS + "ms"); |
| } |
| |
| // Now we should be prepared. |
| intent = VpnService.prepare(mActivity); |
| if (intent != null) { |
| fail("VpnService.prepare returned non-null even after the VPN dialog " + |
| intent.getComponent() + "returned RESULT_OK."); |
| } |
| } |
| } |
| |
| // TODO: Consider replacing arguments with a Builder. |
| private void startVpn( |
| String[] addresses, String[] routes, String allowedApplications, |
| String disallowedApplications, @Nullable ProxyInfo proxyInfo, |
| @Nullable ArrayList<Network> underlyingNetworks, boolean isAlwaysMetered) throws Exception { |
| prepareVpn(); |
| |
| // Register a callback so we will be notified when our VPN comes up. |
| final NetworkRequest request = new NetworkRequest.Builder() |
| .addTransportType(NetworkCapabilities.TRANSPORT_VPN) |
| .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) |
| .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) |
| .build(); |
| mCallback = new NetworkCallback() { |
| public void onAvailable(Network network) { |
| synchronized (mLock) { |
| Log.i(TAG, "Got available callback for network=" + network); |
| mNetwork = network; |
| mLock.notify(); |
| } |
| } |
| }; |
| mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown. |
| |
| // Start the service and wait up for TIMEOUT_MS ms for the VPN to come up. |
| Intent intent = new Intent(mActivity, MyVpnService.class) |
| .putExtra(mPackageName + ".cmd", "connect") |
| .putExtra(mPackageName + ".addresses", TextUtils.join(",", addresses)) |
| .putExtra(mPackageName + ".routes", TextUtils.join(",", routes)) |
| .putExtra(mPackageName + ".allowedapplications", allowedApplications) |
| .putExtra(mPackageName + ".disallowedapplications", disallowedApplications) |
| .putExtra(mPackageName + ".httpProxy", proxyInfo) |
| .putParcelableArrayListExtra( |
| mPackageName + ".underlyingNetworks", underlyingNetworks) |
| .putExtra(mPackageName + ".isAlwaysMetered", isAlwaysMetered); |
| |
| mActivity.startService(intent); |
| synchronized (mLock) { |
| if (mNetwork == null) { |
| Log.i(TAG, "bf mLock"); |
| mLock.wait(TIMEOUT_MS); |
| Log.i(TAG, "af mLock"); |
| } |
| } |
| |
| if (mNetwork == null) { |
| fail("VPN did not become available after " + TIMEOUT_MS + "ms"); |
| } |
| |
| // Unfortunately, when the available callback fires, the VPN UID ranges are not yet |
| // configured. Give the system some time to do so. http://b/18436087 . |
| try { Thread.sleep(3000); } catch(InterruptedException e) {} |
| } |
| |
| private void stopVpn() { |
| // Register a callback so we will be notified when our VPN comes up. |
| final NetworkRequest request = new NetworkRequest.Builder() |
| .addTransportType(NetworkCapabilities.TRANSPORT_VPN) |
| .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) |
| .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) |
| .build(); |
| mCallback = new NetworkCallback() { |
| public void onLost(Network network) { |
| synchronized (mLockShutdown) { |
| Log.i(TAG, "Got lost callback for network=" + network |
| + ",mNetwork = " + mNetwork); |
| if( mNetwork == network){ |
| mLockShutdown.notify(); |
| } |
| } |
| } |
| }; |
| mCM.registerNetworkCallback(request, mCallback); // Unregistered in tearDown. |
| // Simply calling mActivity.stopService() won't stop the service, because the system binds |
| // to the service for the purpose of sending it a revoke command if another VPN comes up, |
| // and stopping a bound service has no effect. Instead, "start" the service again with an |
| // Intent that tells it to disconnect. |
| Intent intent = new Intent(mActivity, MyVpnService.class) |
| .putExtra(mPackageName + ".cmd", "disconnect"); |
| mActivity.startService(intent); |
| synchronized (mLockShutdown) { |
| try { |
| Log.i(TAG, "bf mLockShutdown"); |
| mLockShutdown.wait(TIMEOUT_MS); |
| Log.i(TAG, "af mLockShutdown"); |
| } catch(InterruptedException e) {} |
| } |
| } |
| |
| private static void closeQuietly(Closeable c) { |
| if (c != null) { |
| try { |
| c.close(); |
| } catch (IOException e) { |
| } |
| } |
| } |
| |
| private static void checkPing(String to) throws IOException, ErrnoException { |
| InetAddress address = InetAddress.getByName(to); |
| FileDescriptor s; |
| final int LENGTH = 64; |
| byte[] packet = new byte[LENGTH]; |
| byte[] header; |
| |
| // Construct a ping packet. |
| Random random = new Random(); |
| random.nextBytes(packet); |
| if (address instanceof Inet6Address) { |
| s = Os.socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6); |
| header = new byte[] { (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; |
| } else { |
| // Note that this doesn't actually work due to http://b/18558481 . |
| s = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); |
| header = new byte[] { (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; |
| } |
| System.arraycopy(header, 0, packet, 0, header.length); |
| |
| // Send the packet. |
| int port = random.nextInt(65534) + 1; |
| Os.connect(s, address, port); |
| Os.write(s, packet, 0, packet.length); |
| |
| // Expect a reply. |
| StructPollfd pollfd = new StructPollfd(); |
| pollfd.events = (short) POLLIN; // "error: possible loss of precision" |
| pollfd.fd = s; |
| int ret = Os.poll(new StructPollfd[] { pollfd }, SOCKET_TIMEOUT_MS); |
| assertEquals("Expected reply after sending ping", 1, ret); |
| |
| byte[] reply = new byte[LENGTH]; |
| int read = Os.read(s, reply, 0, LENGTH); |
| assertEquals(LENGTH, read); |
| |
| // Find out what the kernel set the ICMP ID to. |
| InetSocketAddress local = (InetSocketAddress) Os.getsockname(s); |
| port = local.getPort(); |
| packet[4] = (byte) ((port >> 8) & 0xff); |
| packet[5] = (byte) (port & 0xff); |
| |
| // Check the contents. |
| if (packet[0] == (byte) 0x80) { |
| packet[0] = (byte) 0x81; |
| } else { |
| packet[0] = 0; |
| } |
| // Zero out the checksum in the reply so it matches the uninitialized checksum in packet. |
| reply[2] = reply[3] = 0; |
| MoreAsserts.assertEquals(packet, reply); |
| } |
| |
| // Writes data to out and checks that it appears identically on in. |
| private static void writeAndCheckData( |
| OutputStream out, InputStream in, byte[] data) throws IOException { |
| out.write(data, 0, data.length); |
| out.flush(); |
| |
| byte[] read = new byte[data.length]; |
| int bytesRead = 0, totalRead = 0; |
| do { |
| bytesRead = in.read(read, totalRead, read.length - totalRead); |
| totalRead += bytesRead; |
| } while (bytesRead >= 0 && totalRead < data.length); |
| assertEquals(totalRead, data.length); |
| MoreAsserts.assertEquals(data, read); |
| } |
| |
| private void checkTcpReflection(String to, String expectedFrom) throws IOException { |
| // Exercise TCP over the VPN by "connecting to ourselves". We open a server socket and a |
| // client socket, and connect the client socket to a remote host, with the port of the |
| // server socket. The PacketReflector reflects the packets, changing the source addresses |
| // but not the ports, so our client socket is connected to our server socket, though both |
| // sockets think their peers are on the "remote" IP address. |
| |
| // Open a listening socket. |
| ServerSocket listen = new ServerSocket(0, 10, InetAddress.getByName("::")); |
| |
| // Connect the client socket to it. |
| InetAddress toAddr = InetAddress.getByName(to); |
| Socket client = new Socket(); |
| try { |
| client.connect(new InetSocketAddress(toAddr, listen.getLocalPort()), SOCKET_TIMEOUT_MS); |
| if (expectedFrom == null) { |
| closeQuietly(listen); |
| closeQuietly(client); |
| fail("Expected connection to fail, but it succeeded."); |
| } |
| } catch (IOException e) { |
| if (expectedFrom != null) { |
| closeQuietly(listen); |
| fail("Expected connection to succeed, but it failed."); |
| } else { |
| // We expected the connection to fail, and it did, so there's nothing more to test. |
| return; |
| } |
| } |
| |
| // The connection succeeded, and we expected it to succeed. Send some data; if things are |
| // working, the data will be sent to the VPN, reflected by the PacketReflector, and arrive |
| // at our server socket. For good measure, send some data in the other direction. |
| Socket server = null; |
| try { |
| // Accept the connection on the server side. |
| listen.setSoTimeout(SOCKET_TIMEOUT_MS); |
| server = listen.accept(); |
| checkConnectionOwnerUidTcp(client); |
| checkConnectionOwnerUidTcp(server); |
| // Check that the source and peer addresses are as expected. |
| assertEquals(expectedFrom, client.getLocalAddress().getHostAddress()); |
| assertEquals(expectedFrom, server.getLocalAddress().getHostAddress()); |
| assertEquals( |
| new InetSocketAddress(toAddr, client.getLocalPort()), |
| server.getRemoteSocketAddress()); |
| assertEquals( |
| new InetSocketAddress(toAddr, server.getLocalPort()), |
| client.getRemoteSocketAddress()); |
| |
| // Now write some data. |
| final int LENGTH = 32768; |
| byte[] data = new byte[LENGTH]; |
| new Random().nextBytes(data); |
| |
| // Make sure our writes don't block or time out, because we're single-threaded and can't |
| // read and write at the same time. |
| server.setReceiveBufferSize(LENGTH * 2); |
| client.setSendBufferSize(LENGTH * 2); |
| client.setSoTimeout(SOCKET_TIMEOUT_MS); |
| server.setSoTimeout(SOCKET_TIMEOUT_MS); |
| |
| // Send some data from client to server, then from server to client. |
| writeAndCheckData(client.getOutputStream(), server.getInputStream(), data); |
| writeAndCheckData(server.getOutputStream(), client.getInputStream(), data); |
| } finally { |
| closeQuietly(listen); |
| closeQuietly(client); |
| closeQuietly(server); |
| } |
| } |
| |
| private void checkConnectionOwnerUidUdp(DatagramSocket s, boolean expectSuccess) { |
| final int expectedUid = expectSuccess ? Process.myUid() : INVALID_UID; |
| InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort()); |
| InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort()); |
| int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_UDP, loc, rem); |
| assertEquals(expectedUid, uid); |
| } |
| |
| private void checkConnectionOwnerUidTcp(Socket s) { |
| final int expectedUid = Process.myUid(); |
| InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort()); |
| InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort()); |
| int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_TCP, loc, rem); |
| assertEquals(expectedUid, uid); |
| } |
| |
| private void checkUdpEcho(String to, String expectedFrom) throws IOException { |
| DatagramSocket s; |
| InetAddress address = InetAddress.getByName(to); |
| if (address instanceof Inet6Address) { // http://b/18094870 |
| s = new DatagramSocket(0, InetAddress.getByName("::")); |
| } else { |
| s = new DatagramSocket(); |
| } |
| s.setSoTimeout(SOCKET_TIMEOUT_MS); |
| |
| Random random = new Random(); |
| byte[] data = new byte[random.nextInt(1650)]; |
| random.nextBytes(data); |
| DatagramPacket p = new DatagramPacket(data, data.length); |
| s.connect(address, 7); |
| |
| if (expectedFrom != null) { |
| assertEquals("Unexpected source address: ", |
| expectedFrom, s.getLocalAddress().getHostAddress()); |
| } |
| |
| try { |
| if (expectedFrom != null) { |
| s.send(p); |
| checkConnectionOwnerUidUdp(s, true); |
| s.receive(p); |
| MoreAsserts.assertEquals(data, p.getData()); |
| } else { |
| try { |
| s.send(p); |
| s.receive(p); |
| fail("Received unexpected reply"); |
| } catch (IOException expected) { |
| checkConnectionOwnerUidUdp(s, false); |
| } |
| } |
| } finally { |
| s.close(); |
| } |
| } |
| |
| private void checkTrafficOnVpn() throws Exception { |
| checkUdpEcho("192.0.2.251", "192.0.2.2"); |
| checkUdpEcho("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe"); |
| checkPing("2001:db8:dead:beef::f00"); |
| checkTcpReflection("192.0.2.252", "192.0.2.2"); |
| checkTcpReflection("2001:db8:dead:beef::f00", "2001:db8:1:2::ffe"); |
| } |
| |
| private void checkNoTrafficOnVpn() throws Exception { |
| checkUdpEcho("192.0.2.251", null); |
| checkUdpEcho("2001:db8:dead:beef::f00", null); |
| checkTcpReflection("192.0.2.252", null); |
| checkTcpReflection("2001:db8:dead:beef::f00", null); |
| } |
| |
| private FileDescriptor openSocketFd(String host, int port, int timeoutMs) throws Exception { |
| Socket s = new Socket(host, port); |
| s.setSoTimeout(timeoutMs); |
| // Dup the filedescriptor so ParcelFileDescriptor's finalizer doesn't garbage collect it |
| // and cause our fd to become invalid. http://b/35927643 . |
| FileDescriptor fd = Os.dup(ParcelFileDescriptor.fromSocket(s).getFileDescriptor()); |
| s.close(); |
| return fd; |
| } |
| |
| private FileDescriptor openSocketFdInOtherApp( |
| String host, int port, int timeoutMs) throws Exception { |
| Log.d(TAG, String.format("Creating test socket in UID=%d, my UID=%d", |
| mRemoteSocketFactoryClient.getUid(), Os.getuid())); |
| FileDescriptor fd = mRemoteSocketFactoryClient.openSocketFd(host, port, TIMEOUT_MS); |
| return fd; |
| } |
| |
| private void sendRequest(FileDescriptor fd, String host) throws Exception { |
| String request = "GET /generate_204 HTTP/1.1\r\n" + |
| "Host: " + host + "\r\n" + |
| "Connection: keep-alive\r\n\r\n"; |
| byte[] requestBytes = request.getBytes(StandardCharsets.UTF_8); |
| int ret = Os.write(fd, requestBytes, 0, requestBytes.length); |
| Log.d(TAG, "Wrote " + ret + "bytes"); |
| |
| String expected = "HTTP/1.1 204 No Content\r\n"; |
| byte[] response = new byte[expected.length()]; |
| Os.read(fd, response, 0, response.length); |
| |
| String actual = new String(response, StandardCharsets.UTF_8); |
| assertEquals(expected, actual); |
| Log.d(TAG, "Got response: " + actual); |
| } |
| |
| private void assertSocketStillOpen(FileDescriptor fd, String host) throws Exception { |
| try { |
| assertTrue(fd.valid()); |
| sendRequest(fd, host); |
| assertTrue(fd.valid()); |
| } finally { |
| Os.close(fd); |
| } |
| } |
| |
| private void assertSocketClosed(FileDescriptor fd, String host) throws Exception { |
| try { |
| assertTrue(fd.valid()); |
| sendRequest(fd, host); |
| fail("Socket opened before VPN connects should be closed when VPN connects"); |
| } catch (ErrnoException expected) { |
| assertEquals(ECONNABORTED, expected.errno); |
| assertTrue(fd.valid()); |
| } finally { |
| Os.close(fd); |
| } |
| } |
| |
| private ContentResolver getContentResolver() { |
| return getInstrumentation().getContext().getContentResolver(); |
| } |
| |
| private boolean isPrivateDnsInStrictMode() { |
| return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME.equals( |
| Settings.Global.getString(getContentResolver(), PRIVATE_DNS_MODE_SETTING)); |
| } |
| |
| private void storePrivateDnsSetting() { |
| mOldPrivateDnsMode = Settings.Global.getString(getContentResolver(), |
| PRIVATE_DNS_MODE_SETTING); |
| mOldPrivateDnsSpecifier = Settings.Global.getString(getContentResolver(), |
| PRIVATE_DNS_SPECIFIER_SETTING); |
| } |
| |
| private void restorePrivateDnsSetting() { |
| Settings.Global.putString(getContentResolver(), PRIVATE_DNS_MODE_SETTING, |
| mOldPrivateDnsMode); |
| Settings.Global.putString(getContentResolver(), PRIVATE_DNS_SPECIFIER_SETTING, |
| mOldPrivateDnsSpecifier); |
| } |
| |
| // TODO: replace with CtsNetUtils.awaitPrivateDnsSetting in Q or above. |
| private void expectPrivateDnsHostname(final String hostname) throws Exception { |
| final NetworkRequest request = new NetworkRequest.Builder() |
| .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) |
| .build(); |
| final CountDownLatch latch = new CountDownLatch(1); |
| final NetworkCallback callback = new NetworkCallback() { |
| @Override |
| public void onLinkPropertiesChanged(Network network, LinkProperties lp) { |
| if (network.equals(mNetwork) && |
| Objects.equals(lp.getPrivateDnsServerName(), hostname)) { |
| latch.countDown(); |
| } |
| } |
| }; |
| |
| mCM.registerNetworkCallback(request, callback); |
| |
| try { |
| assertTrue("Private DNS hostname was not " + hostname + " after " + TIMEOUT_MS + "ms", |
| latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| } finally { |
| mCM.unregisterNetworkCallback(callback); |
| } |
| } |
| |
| private void setAndVerifyPrivateDns(boolean strictMode) throws Exception { |
| final ContentResolver cr = getInstrumentation().getContext().getContentResolver(); |
| String privateDnsHostname; |
| |
| if (strictMode) { |
| privateDnsHostname = "vpncts-nx.metric.gstatic.com"; |
| Settings.Global.putString(cr, PRIVATE_DNS_SPECIFIER_SETTING, privateDnsHostname); |
| Settings.Global.putString(cr, PRIVATE_DNS_MODE_SETTING, |
| PRIVATE_DNS_MODE_PROVIDER_HOSTNAME); |
| } else { |
| Settings.Global.putString(cr, PRIVATE_DNS_MODE_SETTING, PRIVATE_DNS_MODE_OPPORTUNISTIC); |
| privateDnsHostname = null; |
| } |
| |
| expectPrivateDnsHostname(privateDnsHostname); |
| |
| String randomName = "vpncts-" + new Random().nextInt(1000000000) + "-ds.metric.gstatic.com"; |
| if (strictMode) { |
| // Strict mode private DNS is enabled. DNS lookups should fail, because the private DNS |
| // server name is invalid. |
| try { |
| InetAddress.getByName(randomName); |
| fail("VPN DNS lookup should fail with private DNS enabled"); |
| } catch (UnknownHostException expected) { |
| } |
| } else { |
| // Strict mode private DNS is disabled. DNS lookup should succeed, because the VPN |
| // provides no DNS servers, and thus DNS falls through to the default network. |
| assertNotNull("VPN DNS lookup should succeed with private DNS disabled", |
| InetAddress.getByName(randomName)); |
| } |
| } |
| |
| // Tests that strict mode private DNS is used on VPNs. |
| private void checkStrictModePrivateDns() throws Exception { |
| final boolean initialMode = isPrivateDnsInStrictMode(); |
| setAndVerifyPrivateDns(!initialMode); |
| setAndVerifyPrivateDns(initialMode); |
| } |
| |
| private class NeverChangeNetworkCallback extends NetworkCallback { |
| private CountDownLatch mLatch = new CountDownLatch(1); |
| private volatile Network mFirstNetwork; |
| private volatile Network mOtherNetwork; |
| |
| public void onAvailable(Network n) { |
| // Don't assert here, as it crashes the test with a hard to debug message. |
| if (mFirstNetwork == null) { |
| mFirstNetwork = n; |
| mLatch.countDown(); |
| } else if (mOtherNetwork == null) { |
| mOtherNetwork = n; |
| } |
| } |
| |
| public Network getFirstNetwork() throws Exception { |
| assertTrue( |
| "System default callback got no network after " + TIMEOUT_MS + "ms. " |
| + "Please ensure the device has a working Internet connection.", |
| mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| return mFirstNetwork; |
| } |
| |
| public void assertNeverChanged() { |
| assertNull(mOtherNetwork); |
| } |
| } |
| |
| public void testDefault() throws Exception { |
| if (!supportedHardware()) return; |
| // If adb TCP port opened, this test may running by adb over network. |
| // All of socket would be destroyed in this test. So this test don't |
| // support adb over network, see b/119382723. |
| if (SystemProperties.getInt("persist.adb.tcp.port", -1) > -1 |
| || SystemProperties.getInt("service.adb.tcp.port", -1) > -1) { |
| Log.i(TAG, "adb is running over the network, so skip this test"); |
| return; |
| } |
| |
| final BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver( |
| getInstrumentation().getTargetContext(), MyVpnService.ACTION_ESTABLISHED); |
| receiver.register(); |
| |
| // Test the behaviour of a variety of types of network callbacks. |
| final Network defaultNetwork = mCM.getActiveNetwork(); |
| final NeverChangeNetworkCallback systemDefaultCallback = new NeverChangeNetworkCallback(); |
| final NeverChangeNetworkCallback otherUidCallback = new NeverChangeNetworkCallback(); |
| final TestNetworkCallback myUidCallback = new TestNetworkCallback(); |
| if (SdkLevel.isAtLeastS()) { |
| final int otherUid = UserHandle.getUid(UserHandle.of(5), Process.FIRST_APPLICATION_UID); |
| final Handler h = new Handler(Looper.getMainLooper()); |
| runWithShellPermissionIdentity(() -> { |
| mCM.registerSystemDefaultNetworkCallback(systemDefaultCallback, h); |
| mCM.registerDefaultNetworkCallbackAsUid(otherUid, otherUidCallback, h); |
| mCM.registerDefaultNetworkCallbackAsUid(Process.myUid(), myUidCallback, h); |
| }, NETWORK_SETTINGS); |
| assertEquals(defaultNetwork, myUidCallback.waitForAvailable()); |
| } |
| |
| FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS); |
| |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"0.0.0.0/0", "::/0"}, |
| "", "", null, null /* underlyingNetworks */, false /* isAlwaysMetered */); |
| |
| final Intent intent = receiver.awaitForBroadcast(TimeUnit.MINUTES.toMillis(1)); |
| assertNotNull("Failed to receive broadcast from VPN service", intent); |
| assertFalse("Wrong VpnService#isAlwaysOn", |
| intent.getBooleanExtra(MyVpnService.EXTRA_ALWAYS_ON, true)); |
| assertFalse("Wrong VpnService#isLockdownEnabled", |
| intent.getBooleanExtra(MyVpnService.EXTRA_LOCKDOWN_ENABLED, true)); |
| |
| assertSocketClosed(fd, TEST_HOST); |
| |
| checkTrafficOnVpn(); |
| |
| final Network vpnNetwork = myUidCallback.waitForAvailable(); |
| assertEquals(vpnNetwork, mCM.getActiveNetwork()); |
| assertNotEqual(defaultNetwork, vpnNetwork); |
| maybeExpectVpnTransportInfo(vpnNetwork); |
| |
| if (SdkLevel.isAtLeastS()) { |
| // Check that system default network callback has not seen any network changes, even |
| // though the app's default network changed. Also check that otherUidCallback saw no |
| // network changes, because otherUid is in a different user and not subject to the VPN. |
| // This needs to be done before testing private DNS because checkStrictModePrivateDns |
| // will set the private DNS server to a nonexistent name, which will cause validation to |
| // fail and could cause the default network to switch (e.g., from wifi to cellular). |
| assertEquals(defaultNetwork, systemDefaultCallback.getFirstNetwork()); |
| systemDefaultCallback.assertNeverChanged(); |
| assertEquals(defaultNetwork, otherUidCallback.getFirstNetwork()); |
| otherUidCallback.assertNeverChanged(); |
| runWithShellPermissionIdentity(() -> { |
| mCM.unregisterNetworkCallback(systemDefaultCallback); |
| mCM.unregisterNetworkCallback(otherUidCallback); |
| mCM.unregisterNetworkCallback(myUidCallback); |
| }, NETWORK_SETTINGS); |
| } |
| |
| checkStrictModePrivateDns(); |
| |
| receiver.unregisterQuietly(); |
| } |
| |
| public void testAppAllowed() throws Exception { |
| if (!supportedHardware()) return; |
| |
| FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS); |
| |
| // Shell app must not be put in here or it would kill the ADB-over-network use case |
| String allowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName; |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"192.0.2.0/24", "2001:db8::/32"}, |
| allowedApps, "", null, null /* underlyingNetworks */, false /* isAlwaysMetered */); |
| |
| assertSocketClosed(fd, TEST_HOST); |
| |
| checkTrafficOnVpn(); |
| |
| maybeExpectVpnTransportInfo(mCM.getActiveNetwork()); |
| |
| checkStrictModePrivateDns(); |
| } |
| |
| public void testAppDisallowed() throws Exception { |
| if (!supportedHardware()) return; |
| |
| FileDescriptor localFd = openSocketFd(TEST_HOST, 80, TIMEOUT_MS); |
| FileDescriptor remoteFd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS); |
| |
| String disallowedApps = mRemoteSocketFactoryClient.getPackageName() + "," + mPackageName; |
| // If adb TCP port opened, this test may running by adb over TCP. |
| // Add com.android.shell appllication into blacklist to exclude adb socket for VPN test, |
| // see b/119382723. |
| // Note: The test don't support running adb over network for root device |
| disallowedApps = disallowedApps + ",com.android.shell"; |
| Log.i(TAG, "Append shell app to disallowedApps: " + disallowedApps); |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"192.0.2.0/24", "2001:db8::/32"}, |
| "", disallowedApps, null, null /* underlyingNetworks */, |
| false /* isAlwaysMetered */); |
| |
| assertSocketStillOpen(localFd, TEST_HOST); |
| assertSocketStillOpen(remoteFd, TEST_HOST); |
| |
| checkNoTrafficOnVpn(); |
| |
| final Network network = mCM.getActiveNetwork(); |
| final NetworkCapabilities nc = mCM.getNetworkCapabilities(network); |
| assertFalse(nc.hasTransport(TRANSPORT_VPN)); |
| } |
| |
| public void testGetConnectionOwnerUidSecurity() throws Exception { |
| if (!supportedHardware()) return; |
| |
| DatagramSocket s; |
| InetAddress address = InetAddress.getByName("localhost"); |
| s = new DatagramSocket(); |
| s.setSoTimeout(SOCKET_TIMEOUT_MS); |
| s.connect(address, 7); |
| InetSocketAddress loc = new InetSocketAddress(s.getLocalAddress(), s.getLocalPort()); |
| InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort()); |
| try { |
| int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_TCP, loc, rem); |
| assertEquals("Only an active VPN app should see connection information", |
| INVALID_UID, uid); |
| } catch (SecurityException acceptable) { |
| // R and below throw SecurityException if a non-active VPN calls this method. |
| // As long as we can't actually get socket information, either behaviour is fine. |
| return; |
| } |
| } |
| |
| public void testSetProxy() throws Exception { |
| if (!supportedHardware()) return; |
| ProxyInfo initialProxy = mCM.getDefaultProxy(); |
| // Receiver for the proxy change broadcast. |
| BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver(); |
| proxyBroadcastReceiver.register(); |
| |
| String allowedApps = mPackageName; |
| ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888); |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", |
| testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */); |
| |
| // Check that the proxy change broadcast is received |
| try { |
| assertNotNull("No proxy change was broadcast when VPN is connected.", |
| proxyBroadcastReceiver.awaitForBroadcast()); |
| } finally { |
| proxyBroadcastReceiver.unregisterQuietly(); |
| } |
| |
| // Proxy is set correctly in network and in link properties. |
| assertNetworkHasExpectedProxy(testProxyInfo, mNetwork); |
| assertDefaultProxy(testProxyInfo); |
| |
| proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver(); |
| proxyBroadcastReceiver.register(); |
| stopVpn(); |
| try { |
| assertNotNull("No proxy change was broadcast when VPN was disconnected.", |
| proxyBroadcastReceiver.awaitForBroadcast()); |
| } finally { |
| proxyBroadcastReceiver.unregisterQuietly(); |
| } |
| |
| // After disconnecting from VPN, the proxy settings are the ones of the initial network. |
| assertDefaultProxy(initialProxy); |
| } |
| |
| public void testSetProxyDisallowedApps() throws Exception { |
| if (!supportedHardware()) return; |
| ProxyInfo initialProxy = mCM.getDefaultProxy(); |
| |
| // If adb TCP port opened, this test may running by adb over TCP. |
| // Add com.android.shell appllication into blacklist to exclude adb socket for VPN test, |
| // see b/119382723. |
| // Note: The test don't support running adb over network for root device |
| String disallowedApps = mPackageName + ",com.android.shell"; |
| ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888); |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"0.0.0.0/0", "::/0"}, "", disallowedApps, |
| testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */); |
| |
| // The disallowed app does has the proxy configs of the default network. |
| assertNetworkHasExpectedProxy(initialProxy, mCM.getActiveNetwork()); |
| assertDefaultProxy(initialProxy); |
| } |
| |
| public void testNoProxy() throws Exception { |
| if (!supportedHardware()) return; |
| ProxyInfo initialProxy = mCM.getDefaultProxy(); |
| BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver(); |
| proxyBroadcastReceiver.register(); |
| String allowedApps = mPackageName; |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null, |
| null /* underlyingNetworks */, false /* isAlwaysMetered */); |
| |
| try { |
| assertNotNull("No proxy change was broadcast.", |
| proxyBroadcastReceiver.awaitForBroadcast()); |
| } finally { |
| proxyBroadcastReceiver.unregisterQuietly(); |
| } |
| |
| // The VPN network has no proxy set. |
| assertNetworkHasExpectedProxy(null, mNetwork); |
| |
| proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver(); |
| proxyBroadcastReceiver.register(); |
| stopVpn(); |
| try { |
| assertNotNull("No proxy change was broadcast.", |
| proxyBroadcastReceiver.awaitForBroadcast()); |
| } finally { |
| proxyBroadcastReceiver.unregisterQuietly(); |
| } |
| // After disconnecting from VPN, the proxy settings are the ones of the initial network. |
| assertDefaultProxy(initialProxy); |
| assertNetworkHasExpectedProxy(initialProxy, mCM.getActiveNetwork()); |
| } |
| |
| public void testBindToNetworkWithProxy() throws Exception { |
| if (!supportedHardware()) return; |
| String allowedApps = mPackageName; |
| Network initialNetwork = mCM.getActiveNetwork(); |
| ProxyInfo initialProxy = mCM.getDefaultProxy(); |
| ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("10.0.0.1", 8888); |
| // Receiver for the proxy change broadcast. |
| BlockingBroadcastReceiver proxyBroadcastReceiver = new ProxyChangeBroadcastReceiver(); |
| proxyBroadcastReceiver.register(); |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", |
| testProxyInfo, null /* underlyingNetworks */, false /* isAlwaysMetered */); |
| |
| assertDefaultProxy(testProxyInfo); |
| mCM.bindProcessToNetwork(initialNetwork); |
| try { |
| assertNotNull("No proxy change was broadcast.", |
| proxyBroadcastReceiver.awaitForBroadcast()); |
| } finally { |
| proxyBroadcastReceiver.unregisterQuietly(); |
| } |
| assertDefaultProxy(initialProxy); |
| } |
| |
| public void testVpnMeterednessWithNoUnderlyingNetwork() throws Exception { |
| if (!supportedHardware()) { |
| return; |
| } |
| // VPN is not routing any traffic i.e. its underlying networks is an empty array. |
| ArrayList<Network> underlyingNetworks = new ArrayList<>(); |
| String allowedApps = mPackageName; |
| |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null, |
| underlyingNetworks, false /* isAlwaysMetered */); |
| |
| // VPN should now be the active network. |
| assertEquals(mNetwork, mCM.getActiveNetwork()); |
| assertVpnTransportContains(NetworkCapabilities.TRANSPORT_VPN); |
| // VPN with no underlying networks should be metered by default. |
| assertTrue(isNetworkMetered(mNetwork)); |
| assertTrue(mCM.isActiveNetworkMetered()); |
| |
| maybeExpectVpnTransportInfo(mCM.getActiveNetwork()); |
| } |
| |
| public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception { |
| if (!supportedHardware()) { |
| return; |
| } |
| Network underlyingNetwork = mCM.getActiveNetwork(); |
| if (underlyingNetwork == null) { |
| Log.i(TAG, "testVpnMeterednessWithNullUnderlyingNetwork cannot execute" |
| + " unless there is an active network"); |
| return; |
| } |
| // VPN tracks platform default. |
| ArrayList<Network> underlyingNetworks = null; |
| String allowedApps = mPackageName; |
| |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null, |
| underlyingNetworks, false /*isAlwaysMetered */); |
| |
| // Ensure VPN transports contains underlying network's transports. |
| assertVpnTransportContains(underlyingNetwork); |
| // Its meteredness should be same as that of underlying network. |
| assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork)); |
| // Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync. |
| assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered()); |
| |
| maybeExpectVpnTransportInfo(mCM.getActiveNetwork()); |
| } |
| |
| public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception { |
| if (!supportedHardware()) { |
| return; |
| } |
| Network underlyingNetwork = mCM.getActiveNetwork(); |
| if (underlyingNetwork == null) { |
| Log.i(TAG, "testVpnMeterednessWithNonNullUnderlyingNetwork cannot execute" |
| + " unless there is an active network"); |
| return; |
| } |
| // VPN explicitly declares WiFi to be its underlying network. |
| ArrayList<Network> underlyingNetworks = new ArrayList<>(1); |
| underlyingNetworks.add(underlyingNetwork); |
| String allowedApps = mPackageName; |
| |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null, |
| underlyingNetworks, false /* isAlwaysMetered */); |
| |
| // Ensure VPN transports contains underlying network's transports. |
| assertVpnTransportContains(underlyingNetwork); |
| // Its meteredness should be same as that of underlying network. |
| assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork)); |
| // Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync. |
| assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered()); |
| |
| maybeExpectVpnTransportInfo(mCM.getActiveNetwork()); |
| } |
| |
| public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception { |
| if (!supportedHardware()) { |
| return; |
| } |
| Network underlyingNetwork = mCM.getActiveNetwork(); |
| if (underlyingNetwork == null) { |
| Log.i(TAG, "testAlwaysMeteredVpnWithNullUnderlyingNetwork cannot execute" |
| + " unless there is an active network"); |
| return; |
| } |
| // VPN tracks platform default. |
| ArrayList<Network> underlyingNetworks = null; |
| String allowedApps = mPackageName; |
| boolean isAlwaysMetered = true; |
| |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null, |
| underlyingNetworks, isAlwaysMetered); |
| |
| // VPN's meteredness does not depend on underlying network since it is always metered. |
| assertTrue(isNetworkMetered(mNetwork)); |
| assertTrue(mCM.isActiveNetworkMetered()); |
| |
| maybeExpectVpnTransportInfo(mCM.getActiveNetwork()); |
| } |
| |
| public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception { |
| if (!supportedHardware()) { |
| return; |
| } |
| Network underlyingNetwork = mCM.getActiveNetwork(); |
| if (underlyingNetwork == null) { |
| Log.i(TAG, "testAlwaysMeteredVpnWithNonNullUnderlyingNetwork cannot execute" |
| + " unless there is an active network"); |
| return; |
| } |
| // VPN explicitly declares its underlying network. |
| ArrayList<Network> underlyingNetworks = new ArrayList<>(1); |
| underlyingNetworks.add(underlyingNetwork); |
| String allowedApps = mPackageName; |
| boolean isAlwaysMetered = true; |
| |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"0.0.0.0/0", "::/0"}, allowedApps, "", null, |
| underlyingNetworks, isAlwaysMetered); |
| |
| // VPN's meteredness does not depend on underlying network since it is always metered. |
| assertTrue(isNetworkMetered(mNetwork)); |
| assertTrue(mCM.isActiveNetworkMetered()); |
| |
| maybeExpectVpnTransportInfo(mCM.getActiveNetwork()); |
| } |
| |
| public void testB141603906() throws Exception { |
| if (!supportedHardware()) { |
| return; |
| } |
| final InetSocketAddress src = new InetSocketAddress(0); |
| final InetSocketAddress dst = new InetSocketAddress(0); |
| final int NUM_THREADS = 8; |
| final int NUM_SOCKETS = 5000; |
| final Thread[] threads = new Thread[NUM_THREADS]; |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"0.0.0.0/0", "::/0"}, |
| "" /* allowedApplications */, "com.android.shell" /* disallowedApplications */, |
| null /* proxyInfo */, null /* underlyingNetworks */, false /* isAlwaysMetered */); |
| |
| for (int i = 0; i < NUM_THREADS; i++) { |
| threads[i] = new Thread(() -> { |
| for (int j = 0; j < NUM_SOCKETS; j++) { |
| mCM.getConnectionOwnerUid(IPPROTO_TCP, src, dst); |
| } |
| }); |
| } |
| for (Thread thread : threads) { |
| thread.start(); |
| } |
| for (Thread thread : threads) { |
| thread.join(); |
| } |
| stopVpn(); |
| } |
| |
| private boolean isNetworkMetered(Network network) { |
| NetworkCapabilities nc = mCM.getNetworkCapabilities(network); |
| return !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); |
| } |
| |
| private void assertVpnTransportContains(Network underlyingNetwork) { |
| int[] transports = mCM.getNetworkCapabilities(underlyingNetwork).getTransportTypes(); |
| assertVpnTransportContains(transports); |
| } |
| |
| private void assertVpnTransportContains(int... transports) { |
| NetworkCapabilities vpnCaps = mCM.getNetworkCapabilities(mNetwork); |
| for (int transport : transports) { |
| assertTrue(vpnCaps.hasTransport(transport)); |
| } |
| } |
| |
| private void maybeExpectVpnTransportInfo(Network network) { |
| if (!SdkLevel.isAtLeastS()) return; |
| final NetworkCapabilities vpnNc = mCM.getNetworkCapabilities(network); |
| assertTrue(vpnNc.hasTransport(TRANSPORT_VPN)); |
| final TransportInfo ti = vpnNc.getTransportInfo(); |
| assertTrue(ti instanceof VpnTransportInfo); |
| assertEquals(VpnManager.TYPE_VPN_SERVICE, ((VpnTransportInfo) ti).type); |
| } |
| |
| private void assertDefaultProxy(ProxyInfo expected) { |
| assertEquals("Incorrect proxy config.", expected, mCM.getDefaultProxy()); |
| String expectedHost = expected == null ? null : expected.getHost(); |
| String expectedPort = expected == null ? null : String.valueOf(expected.getPort()); |
| assertEquals("Incorrect proxy host system property.", expectedHost, |
| System.getProperty("http.proxyHost")); |
| assertEquals("Incorrect proxy port system property.", expectedPort, |
| System.getProperty("http.proxyPort")); |
| } |
| |
| private void assertNetworkHasExpectedProxy(ProxyInfo expected, Network network) { |
| LinkProperties lp = mCM.getLinkProperties(network); |
| assertNotNull("The network link properties object is null.", lp); |
| assertEquals("Incorrect proxy config.", expected, lp.getHttpProxy()); |
| |
| assertEquals(expected, mCM.getProxyForNetwork(network)); |
| } |
| |
| class ProxyChangeBroadcastReceiver extends BlockingBroadcastReceiver { |
| private boolean received; |
| |
| public ProxyChangeBroadcastReceiver() { |
| super(VpnTest.this.getInstrumentation().getContext(), Proxy.PROXY_CHANGE_ACTION); |
| received = false; |
| } |
| |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (!received) { |
| // Do not call onReceive() more than once. |
| super.onReceive(context, intent); |
| } |
| received = true; |
| } |
| } |
| |
| /** |
| * Verifies that DownloadManager has CONNECTIVITY_USE_RESTRICTED_NETWORKS permission that can |
| * bind socket to VPN when it is in VPN disallowed list but requested downloading app is in VPN |
| * allowed list. |
| * See b/165774987. |
| */ |
| public void testDownloadWithDownloadManagerDisallowed() throws Exception { |
| if (!supportedHardware()) return; |
| |
| // Start a VPN with DownloadManager package in disallowed list. |
| startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"}, |
| new String[] {"192.0.2.0/24", "2001:db8::/32"}, |
| "" /* allowedApps */, "com.android.providers.downloads", null /* proxyInfo */, |
| null /* underlyingNetworks */, false /* isAlwaysMetered */); |
| |
| final Context context = VpnTest.this.getInstrumentation().getContext(); |
| final DownloadManager dm = context.getSystemService(DownloadManager.class); |
| final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver(); |
| try { |
| context.registerReceiver(receiver, |
| new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); |
| |
| // Enqueue a request and check only one download. |
| final long id = dm.enqueue(new Request(Uri.parse("https://www.google.com"))); |
| assertEquals(1, getTotalNumberDownloads(dm, new Query())); |
| assertEquals(1, getTotalNumberDownloads(dm, new Query().setFilterById(id))); |
| |
| // Wait for download complete and check status. |
| assertEquals(id, receiver.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); |
| assertEquals(1, getTotalNumberDownloads(dm, |
| new Query().setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL))); |
| |
| // Remove download. |
| assertEquals(1, dm.remove(id)); |
| assertEquals(0, getTotalNumberDownloads(dm, new Query())); |
| } finally { |
| context.unregisterReceiver(receiver); |
| } |
| } |
| |
| private static int getTotalNumberDownloads(final DownloadManager dm, final Query query) { |
| try (Cursor cursor = dm.query(query)) { return cursor.getCount(); } |
| } |
| |
| private static class DownloadCompleteReceiver extends BroadcastReceiver { |
| private final CompletableFuture<Long> future = new CompletableFuture<>(); |
| |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| future.complete(intent.getLongExtra( |
| DownloadManager.EXTRA_DOWNLOAD_ID, -1 /* defaultValue */)); |
| } |
| |
| public long get(long timeout, TimeUnit unit) throws Exception { |
| return future.get(timeout, unit); |
| } |
| } |
| } |