blob: 67dd941253628213f49bc8320b1f4962d1f5d4e4 [file] [log] [blame]
/*
* Copyright (C) 2016 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.deviceandprofileowner;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.os.Bundle;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructPollfd;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.IPPROTO_ICMP;
import static android.system.OsConstants.POLLIN;
import static android.system.OsConstants.SOCK_DGRAM;
/**
* Validates that a device owner or profile owner can set an always-on VPN without user action.
*
* A trivial VPN app is installed which reflects ping packets back to the sender. One ping packet is
* sent, received after its round-trip, and compared to the original packet to make sure nothing
* strange happened on the way through the VPN.
*
* All of the addresses in this test are fictional and any resemblance to real addresses is the
* result of a misconfigured network.
*/
public class AlwaysOnVpnTest extends BaseDeviceAdminTest {
private static final String VPN_PACKAGE = "com.android.cts.vpnfirewall";
private static final int NETWORK_TIMEOUT_MS = 5000;
private static final int NETWORK_SETTLE_GRACE_MS = 100;
private static final int SOCKET_TIMEOUT_MS = 5000;
/** @see com.android.cts.vpnfirewall.ReflectorVpnService */
public static final String RESTRICTION_ADDRESSES = "vpn.addresses";
public static final String RESTRICTION_ROUTES = "vpn.routes";
public static final String RESTRICTION_ALLOWED = "vpn.allowed";
public static final String RESTRICTION_DISALLOWED = "vpn.disallowed";
private static final int ICMP_ECHO_REQUEST = 0x08;
private static final int ICMP_ECHO_REPLY = 0x00;
// IP address reserved for documentation by rfc5737
private static final String TEST_ADDRESS = "192.0.2.4";
private ConnectivityManager mConnectivityManager;
private String mPackageName;
@Override
public void setUp() throws Exception {
super.setUp();
mPackageName = mContext.getPackageName();
mConnectivityManager =
(ConnectivityManager) mContext.getSystemService(mContext.CONNECTIVITY_SERVICE);
}
@Override
public void tearDown() throws Exception {
mDevicePolicyManager.setAlwaysOnVpnPackage(ADMIN_RECEIVER_COMPONENT, null, false);
mDevicePolicyManager.setApplicationRestrictions(ADMIN_RECEIVER_COMPONENT, VPN_PACKAGE,
/* restrictions */ null);
super.tearDown();
}
public void testAlwaysOnVpn() throws Exception {
// test always-on is null by default
assertNull(mDevicePolicyManager.getAlwaysOnVpnPackage(ADMIN_RECEIVER_COMPONENT));
final CountDownLatch vpnLatch = new CountDownLatch(1);
setAndWaitForVpn(VPN_PACKAGE, /* usable */ true);
checkPing(TEST_ADDRESS);
}
public void testAllowedApps() throws Exception {
final Bundle restrictions = new Bundle();
restrictions.putStringArray(RESTRICTION_ALLOWED, new String[] {mPackageName});
mDevicePolicyManager.setApplicationRestrictions(ADMIN_RECEIVER_COMPONENT, VPN_PACKAGE,
restrictions);
setAndWaitForVpn(VPN_PACKAGE, /* usable */ true);
assertTrue(isNetworkVpn());
}
public void testDisallowedApps() throws Exception {
final Bundle restrictions = new Bundle();
restrictions.putStringArray(RESTRICTION_DISALLOWED, new String[] {mPackageName});
mDevicePolicyManager.setApplicationRestrictions(ADMIN_RECEIVER_COMPONENT, VPN_PACKAGE,
restrictions);
setAndWaitForVpn(VPN_PACKAGE, /* usable */ false);
assertFalse(isNetworkVpn());
}
private void setAndWaitForVpn(String packageName, boolean usable) {
final CountDownLatch vpnLatch = new CountDownLatch(1);
final NetworkRequest request = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build();
final ConnectivityManager.NetworkCallback callback
= new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network net) {
vpnLatch.countDown();
}
};
mConnectivityManager.registerNetworkCallback(request, callback);
try {
mDevicePolicyManager.setAlwaysOnVpnPackage(ADMIN_RECEIVER_COMPONENT, VPN_PACKAGE, true);
assertEquals(VPN_PACKAGE, mDevicePolicyManager.getAlwaysOnVpnPackage(
ADMIN_RECEIVER_COMPONENT));
if (!vpnLatch.await(NETWORK_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
fail("Took too long waiting to establish a VPN-backed connection");
}
// Give the VPN a moment to start transmitting data.
Thread.sleep(NETWORK_SETTLE_GRACE_MS);
} catch (InterruptedException | PackageManager.NameNotFoundException e) {
fail("Failed to send ping: " + e);
} finally {
mConnectivityManager.unregisterNetworkCallback(callback);
}
// Do we have a network?
NetworkInfo vpnInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_VPN);
assertTrue(vpnInfo != null);
// Is it usable?
assertEquals(usable, vpnInfo.isConnected());
}
private boolean isNetworkVpn() {
Network network = mConnectivityManager.getActiveNetwork();
NetworkCapabilities capabilities = mConnectivityManager.getNetworkCapabilities(network);
return capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
}
private static void checkPing(String host) throws ErrnoException, IOException {
FileDescriptor socket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
// Create an ICMP message
final int identifier = 0x7E57;
final String message = "test packet";
byte[] echo = createIcmpMessage(ICMP_ECHO_REQUEST, 0x00, identifier, 0, message.getBytes());
// Send the echo packet.
int port = new InetSocketAddress(0).getPort();
Os.connect(socket, InetAddress.getByName(host), port);
Os.write(socket, echo, 0, echo.length);
// Expect a reply.
StructPollfd pollfd = new StructPollfd();
pollfd.events = (short) POLLIN;
pollfd.fd = socket;
int ret = Os.poll(new StructPollfd[] { pollfd }, SOCKET_TIMEOUT_MS);
assertEquals("Expected reply after sending ping", 1, ret);
byte[] reply = new byte[echo.length];
int read = Os.read(socket, reply, 0, echo.length);
assertEquals(echo.length, read);
// Ignore control type differences since echo=8, reply=0.
assertEquals(echo[0], ICMP_ECHO_REQUEST);
assertEquals(reply[0], ICMP_ECHO_REPLY);
echo[0] = 0;
reply[0] = 0;
// Fix ICMP ID which kernel will have changed on the way out.
InetSocketAddress local = (InetSocketAddress) Os.getsockname(socket);
port = local.getPort();
echo[4] = (byte) ((port >> 8) & 0xFF);
echo[5] = (byte) (port & 0xFF);
// Ignore checksum differences since the types are not supposed to match.
echo[2] = echo[3] = 0;
reply[2] = reply[3] = 0;
assertTrue("Packet contents do not match."
+ "\nEcho packet: " + Arrays.toString(echo)
+ "\nReply packet: " + Arrays.toString(reply), Arrays.equals(echo, reply));
}
private static byte[] createIcmpMessage(int type, int code, int extra1, int extra2,
byte[] data) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
DataOutputStream stream = new DataOutputStream(output);
stream.writeByte(type);
stream.writeByte(code);
stream.writeShort(/* checksum */ 0);
stream.writeShort((short) extra1);
stream.writeShort((short) extra2);
stream.write(data, 0, data.length);
return output.toByteArray();
}
}