Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f087ec64 authored by Paul Hu's avatar Paul Hu
Browse files

Send VPN connection state metrics

Notifies that a VPN connection/disconnection event has occurred.
And gathers the current VPN state information from internal
fields and reports it to the system's statistics logging service.

Bug: 306313287
Test: atest FrameworksVpnTests
Test: statsd_testdrive 851
Flag: android.net.platform.flags.collect_vpn_metrics
Change-Id: I8d703eedb9c1b90c9c0e030dd2f6e25b1ee76a8c
parent cbbc2ed8
Loading
Loading
Loading
Loading
+14 −4
Original line number Diff line number Diff line
@@ -711,6 +711,10 @@ public class Vpn {
        return mEnableTeardown;
    }

    private boolean isVpnMetricsLoggable() {
        return mVpnConnectivityMetrics != null && mVpnConnectivityMetrics.isPlatformVpn();
    }

    /**
     * Update current state, dispatching event to listeners.
     */
@@ -731,6 +735,9 @@ public class Vpn {
            case CONNECTED:
                if (null != mNetworkAgent) {
                    mNetworkAgent.markConnected();
                    if (isVpnMetricsLoggable()) {
                        mVpnConnectivityMetrics.notifyVpnConnected();
                    }
                }
                break;
            case DISCONNECTED:
@@ -738,6 +745,9 @@ public class Vpn {
                if (null != mNetworkAgent) {
                    mNetworkAgent.unregister();
                    mNetworkAgent = null;
                    if (isVpnMetricsLoggable()) {
                        mVpnConnectivityMetrics.notifyVpnDisconnected();
                    }
                }
                break;
            case CONNECTING:
@@ -2292,7 +2302,7 @@ public class Vpn {
    private void setMtu(int mtu) {
        synchronized (Vpn.this) {
            mConfig.mtu = mtu;
            if (mVpnConnectivityMetrics != null) {
            if (isVpnMetricsLoggable()) {
                mVpnConnectivityMetrics.setMtu(mtu);
            }
        }
@@ -2301,7 +2311,7 @@ public class Vpn {
    private void setUnderlyingNetworksAndMetrics(@NonNull Network[] networks) {
        synchronized (Vpn.this) {
            mConfig.underlyingNetworks = networks;
            if (mVpnConnectivityMetrics != null) {
            if (isVpnMetricsLoggable()) {
                mVpnConnectivityMetrics.setUnderlyingNetwork(mConfig.underlyingNetworks);
            }
        }
@@ -2992,7 +3002,7 @@ public class Vpn {
            // The update on VPN and the IPsec tunnel will be done when migration is fully complete
            // in onChildMigrated
            mIkeConnectionInfo = ikeConnectionInfo;
            if (mVpnConnectivityMetrics != null) {
            if (isVpnMetricsLoggable()) {
                mVpnConnectivityMetrics.setServerIpProtocol(ikeConnectionInfo.getRemoteAddress());
            }
        }
@@ -3064,7 +3074,7 @@ public class Vpn {

                    mConfig.addresses.clear();
                    mConfig.addresses.addAll(internalAddresses);
                    if (mVpnConnectivityMetrics != null) {
                    if (isVpnMetricsLoggable()) {
                        mVpnConnectivityMetrics.setVpnNetworkIpProtocol(mConfig.addresses);
                    }

+81 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA384;
import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA512;
import static android.net.IpSecAlgorithm.CRYPT_AES_CBC;
import static android.net.IpSecAlgorithm.CRYPT_AES_CTR;
import static android.net.VpnManager.TYPE_VPN_PLATFORM;

import android.net.ConnectivityManager;
import android.net.LinkAddress;
@@ -39,6 +40,8 @@ import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import com.android.internal.util.FrameworkStatsLog;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.Arrays;
@@ -66,6 +69,8 @@ public class VpnConnectivityMetrics {
    private final int mUserId;
    @NonNull
    private final ConnectivityManager mConnectivityManager;
    @NonNull
    private final Dependencies mDependencies;
    private int mVpnType = VPN_TYPE_UNKNOWN;
    private int mVpnProfileType = VPN_PROFILE_TYPE_UNKNOWN;
    private int mMtu = 0;
@@ -107,9 +112,33 @@ public class VpnConnectivityMetrics {
        sAlgorithms.put(10, CRYPT_AES_CTR);
    }

    /**
     * Dependencies of VpnConnectivityMetrics, for injection in tests.
     */
    public static class Dependencies {

        /**
         * @see FrameworkStatsLog
         */
        public void statsWrite(int vpnType, int vpnNetworkIpProtocol, int serverIpProtocol,
                int vpnProfileType, int allowedAlgorithms, int mtu, int[] underlyingNetworkType,
                boolean connected, int userId) {
            FrameworkStatsLog.write(FrameworkStatsLog.VPN_CONNECTION_REPORTED, vpnType,
                    vpnNetworkIpProtocol, serverIpProtocol, vpnProfileType, allowedAlgorithms, mtu,
                    underlyingNetworkType, connected, userId);
        }
    }

    public VpnConnectivityMetrics(int userId, ConnectivityManager connectivityManager) {
        this(userId, connectivityManager, new Dependencies());
    }

    @VisibleForTesting
    VpnConnectivityMetrics(int userId, ConnectivityManager connectivityManager,
            Dependencies dependencies) {
        mUserId = userId;
        mConnectivityManager = connectivityManager;
        mDependencies = dependencies;
    }

    /**
@@ -265,4 +294,56 @@ public class VpnConnectivityMetrics {
        }
        return ipProtocol;
    }

    /**
     * Checks if the VPN associated with these metrics is a platform-managed VPN.
     * The determination is based on the internal {@code mVpnType} field, which
     * should be set during the VPN's configuration.
     *
     * @return {@code true} if the VPN type matches {@code TYPE_VPN_PLATFORM};
     *         {@code false} otherwise.
     */
    public boolean isPlatformVpn() {
        return mVpnType == TYPE_VPN_PLATFORM;
    }



    /**
     * Notifies that a VPN connected event has occurred.
     *
     * This method gathers the current VPN state information from internal fields and reports it to
     * the system's statistics logging service.
     */
    public void notifyVpnConnected() {
        mDependencies.statsWrite(
                mVpnType,
                mVpnNetworkIpProtocol,
                mServerIpProtocol,
                mVpnProfileType,
                mAllowedAlgorithms,
                mMtu,
                mUnderlyingNetworkTypes,
                true /* connected */,
                mUserId);
    }

    /**
     * Notifies that a VPN disconnected event has occurred.
     *
     * This method gathers the current VPN state information from internal fields and reports it to
     * the system's statistics logging service.
     */
    public void notifyVpnDisconnected() {
        mDependencies.statsWrite(
                mVpnType,
                mVpnNetworkIpProtocol,
                mServerIpProtocol,
                mVpnProfileType,
                mAllowedAlgorithms,
                mMtu,
                mUnderlyingNetworkTypes,
                false /* connected */,
                mUserId);
    }
}
+94 −2
Original line number Diff line number Diff line
@@ -38,16 +38,28 @@ import static com.android.server.connectivity.VpnConnectivityMetrics.checkIpProt

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;

import android.net.ConnectivityManager;
import android.net.Ikev2VpnProfile;
import android.net.InetAddresses;
import android.net.LinkAddress;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.VpnManager;
import android.util.Log;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.internal.net.VpnProfile;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -55,6 +67,25 @@ import java.util.concurrent.atomic.AtomicBoolean;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VpnConnectivityMetricsTest {
    private static final int USER_ID = 10;
    private static final String VPN_CLIENT_IP_V4 = "192.0.2.1/32";
    private static final String VPN_CLIENT_IP_V6 = "2001:db8:1:2::ffe/128";
    private static final String VPN_SERVER_IP_V4 = "192.0.2.2";
    private static final String VPN_SERVER_IP_V6 = "2001:db8::1";

    @Mock
    private ConnectivityManager mCm;
    @Mock
    private VpnConnectivityMetrics.Dependencies mDeps;
    private VpnConnectivityMetrics mMetrics;


    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mMetrics = new VpnConnectivityMetrics(USER_ID, mCm, mDeps);
    }

    @Test
    public void testBuildAllowedAlgorithmsBitmask() {
        assertEquals(1536, buildAllowedAlgorithmsBitmask(List.of(CRYPT_AES_CBC, CRYPT_AES_CTR)));
@@ -80,12 +111,73 @@ public class VpnConnectivityMetricsTest {

    @Test
    public void testCheckIpProtocol() {
        final LinkAddress vpnClientIpv4 = new LinkAddress("192.0.2.1/32");
        final LinkAddress vpnClientIpv6 = new LinkAddress("2001:db8:1:2::ffe/128");
        final LinkAddress vpnClientIpv4 = new LinkAddress(VPN_CLIENT_IP_V4);
        final LinkAddress vpnClientIpv6 = new LinkAddress(VPN_CLIENT_IP_V6);

        assertEquals(IP_PROTOCOL_UNKNOWN, checkIpProtocol(List.of()));
        assertEquals(IP_PROTOCOL_IPv4, checkIpProtocol(List.of(vpnClientIpv4)));
        assertEquals(IP_PROTOCOL_IPv6, checkIpProtocol(List.of(vpnClientIpv6)));
        assertEquals(IP_PROTOCOL_IPv4v6, checkIpProtocol(List.of(vpnClientIpv4, vpnClientIpv6)));
    }

    @Test
    public void testNotifyVpnConnected() {
        final Network cellNetwork = new Network(1234);
        final NetworkCapabilities cellCap = new NetworkCapabilities();
        cellCap.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
        doReturn(cellCap).when(mCm).getNetworkCapabilities(cellNetwork);

        // Fill in metrics data
        mMetrics.setVpnType(VpnManager.TYPE_VPN_PLATFORM);
        mMetrics.setMtu(1327);
        mMetrics.setVpnProfileType(VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS);
        mMetrics.setAllowedAlgorithms(Ikev2VpnProfile.DEFAULT_ALGORITHMS);
        mMetrics.setUnderlyingNetwork(new Network[] { cellNetwork });
        mMetrics.setVpnNetworkIpProtocol(
                List.of(new LinkAddress(VPN_CLIENT_IP_V4), new LinkAddress(VPN_CLIENT_IP_V6)));
        mMetrics.setServerIpProtocol(InetAddresses.parseNumericAddress(VPN_SERVER_IP_V4));

        // Verify a vpn connected event with the filled in data.
        mMetrics.notifyVpnConnected();
        verify(mDeps).statsWrite(
                VpnManager.TYPE_VPN_PLATFORM,
                IP_PROTOCOL_IPv4v6,
                IP_PROTOCOL_IPv4,
                VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS + 1,
                1999 /* allowedAlgorithms */,
                1327 /* mtu */,
                new int[] { NetworkCapabilities.TRANSPORT_CELLULAR },
                true /* connected */,
                USER_ID);
    }

    @Test
    public void testNotifyDisconnected() {
        final Network wifiNetwork = new Network(1235);
        final NetworkCapabilities wifiCap = new NetworkCapabilities();
        wifiCap.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
        doReturn(wifiCap).when(mCm).getNetworkCapabilities(wifiNetwork);

        // Fill in metrics data
        mMetrics.setVpnType(VpnManager.TYPE_VPN_PLATFORM);
        mMetrics.setMtu(1280);
        mMetrics.setVpnProfileType(VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS);
        mMetrics.setAllowedAlgorithms(Ikev2VpnProfile.DEFAULT_ALGORITHMS);
        mMetrics.setUnderlyingNetwork(new Network[] { wifiNetwork });
        mMetrics.setVpnNetworkIpProtocol(List.of(new LinkAddress(VPN_CLIENT_IP_V6)));
        mMetrics.setServerIpProtocol(InetAddresses.parseNumericAddress(VPN_SERVER_IP_V6));

        // Verify a vpn disconnected event with the filled in data.
        mMetrics.notifyVpnDisconnected();
        verify(mDeps).statsWrite(
                VpnManager.TYPE_VPN_PLATFORM,
                IP_PROTOCOL_IPv6,
                IP_PROTOCOL_IPv6,
                VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS + 1,
                1999 /* allowedAlgorithms */,
                1280 /* mtu */,
                new int[] { NetworkCapabilities.TRANSPORT_WIFI },
                false /* connected */,
                USER_ID);
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -2227,6 +2227,8 @@ public class VpnTest extends VpnTestBase {
                                new LinkAddress(TEST_VPN_INTERNAL_IP6, IP6_PREFIX_LEN)),
                        address -> addresses.contains(address))));
        verify(mVpnConnectivityMetrics).setServerIpProtocol(TEST_VPN_SERVER_IP);
        // Verify connection metrics notification.
        verify(mVpnConnectivityMetrics).notifyVpnConnected();
        // Check LinkProperties
        final LinkProperties lp = lpCaptor.getValue();
        final List<RouteInfo> expectedRoutes =
@@ -3027,6 +3029,8 @@ public class VpnTest extends VpnTestBase {
        assertEquals(expectedRoutes, lp.getRoutes());

        verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS)).unregister();
        // Verify disconnection metrics notification.
        verify(mVpnConnectivityMetrics).notifyVpnDisconnected();
    }

    @Test
@@ -3321,6 +3325,7 @@ public class VpnTest extends VpnTestBase {
        verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat(
                provider -> provider.getName().contains("VpnNetworkProvider")
        ));
        doReturn(true).when(mVpnConnectivityMetrics).isPlatformVpn();
        return vpn;
    }