org.strongswan.android.logic.CharonVpnService.java Source code

Java tutorial

Introduction

Here is the source code for org.strongswan.android.logic.CharonVpnService.java

Source

/*
 * Copyright (C) 2012-2017 Tobias Brunner
 * Copyright (C) 2012 Giuliano Grassi
 * Copyright (C) 2012 Ralf Sager
 * HSR Hochschule fuer Technik Rapperswil
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 */

package org.strongswan.android.logic;

import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.VpnService;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.security.KeyChain;
import android.security.KeyChainException;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.system.OsConstants;
import android.util.Log;

import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.data.VpnProfile.SelectedAppsHandling;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.data.VpnType.VpnTypeFeature;
import org.strongswan.android.logic.VpnStateService.ErrorState;
import org.strongswan.android.logic.VpnStateService.State;
import org.strongswan.android.logic.imc.ImcState;
import org.strongswan.android.logic.imc.RemediationInstruction;
import org.strongswan.android.ui.MainActivity;
import org.strongswan.android.utils.IPRange;
import org.strongswan.android.utils.IPRangeSet;
import org.strongswan.android.utils.SettingsWriter;

import java.io.File;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.SortedSet;

public class CharonVpnService extends VpnService implements Runnable, VpnStateService.VpnStateListener {
    private static final String TAG = CharonVpnService.class.getSimpleName();
    public static final String DISCONNECT_ACTION = "org.strongswan.android.CharonVpnService.DISCONNECT";
    public static final String LOG_FILE = "charon.log";
    public static final int VPN_STATE_NOTIFICATION_ID = 1;

    private String mLogFile;
    private String mAppDir;
    private VpnProfileDataSource mDataSource;
    private Thread mConnectionHandler;
    private VpnProfile mCurrentProfile;
    private volatile String mCurrentCertificateAlias;
    private volatile String mCurrentUserCertificateAlias;
    private VpnProfile mNextProfile;
    private volatile boolean mProfileUpdated;
    private volatile boolean mTerminate;
    private volatile boolean mIsDisconnecting;
    private volatile boolean mShowNotification;
    private VpnStateService mService;
    private final Object mServiceLock = new Object();
    private final ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceDisconnected(
                ComponentName name) { /* since the service is local this is theoretically only called when the process is terminated */
            synchronized (mServiceLock) {
                mService = null;
            }
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            synchronized (mServiceLock) {
                mService = ((VpnStateService.LocalBinder) service).getService();
            }
            /* we are now ready to start the handler thread */
            mService.registerListener(CharonVpnService.this);
            mConnectionHandler.start();
        }
    };

    /**
     * as defined in charonservice.h
     */
    static final int STATE_CHILD_SA_UP = 1;
    static final int STATE_CHILD_SA_DOWN = 2;
    static final int STATE_AUTH_ERROR = 3;
    static final int STATE_PEER_AUTH_ERROR = 4;
    static final int STATE_LOOKUP_ERROR = 5;
    static final int STATE_UNREACHABLE_ERROR = 6;
    static final int STATE_GENERIC_ERROR = 7;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent != null) {
            if (DISCONNECT_ACTION.equals(intent.getAction())) {
                setNextProfile(null);
            } else {
                Bundle bundle = intent.getExtras();
                VpnProfile profile = null;
                if (bundle != null) {
                    profile = mDataSource.getVpnProfile(bundle.getLong(VpnProfileDataSource.KEY_ID));
                    if (profile != null) {
                        String password = bundle.getString(VpnProfileDataSource.KEY_PASSWORD);
                        profile.setPassword(password);
                    }
                }
                setNextProfile(profile);
            }
        }
        return START_NOT_STICKY;
    }

    @Override
    public void onCreate() {
        mLogFile = getFilesDir().getAbsolutePath() + File.separator + LOG_FILE;
        mAppDir = getFilesDir().getAbsolutePath();

        mDataSource = new VpnProfileDataSource(this);
        mDataSource.open();
        /* use a separate thread as main thread for charon */
        mConnectionHandler = new Thread(this);
        /* the thread is started when the service is bound */
        bindService(new Intent(this, VpnStateService.class), mServiceConnection, Service.BIND_AUTO_CREATE);
    }

    @Override
    public void onRevoke() { /* the system revoked the rights grated with the initial prepare() call.
                             * called when the user clicks disconnect in the system's VPN dialog */
        setNextProfile(null);
    }

    @Override
    public void onDestroy() {
        mTerminate = true;
        setNextProfile(null);
        try {
            mConnectionHandler.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (mService != null) {
            mService.unregisterListener(this);
            unbindService(mServiceConnection);
        }
        mDataSource.close();
    }

    /**
     * Set the profile that is to be initiated next. Notify the handler thread.
     *
     * @param profile the profile to initiate
     */
    private void setNextProfile(VpnProfile profile) {
        synchronized (this) {
            this.mNextProfile = profile;
            mProfileUpdated = true;
            notifyAll();
        }
    }

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                try {
                    while (!mProfileUpdated) {
                        wait();
                    }

                    mProfileUpdated = false;
                    stopCurrentConnection();
                    if (mNextProfile == null) {
                        setState(State.DISABLED);
                        if (mTerminate) {
                            break;
                        }
                    } else {
                        mCurrentProfile = mNextProfile;
                        mNextProfile = null;

                        /* store this in a separate (volatile) variable to avoid
                         * a possible deadlock during deinitialization */
                        mCurrentCertificateAlias = mCurrentProfile.getCertificateAlias();
                        mCurrentUserCertificateAlias = mCurrentProfile.getUserCertificateAlias();

                        startConnection(mCurrentProfile);
                        mIsDisconnecting = false;

                        addNotification();
                        BuilderAdapter builder = new BuilderAdapter(mCurrentProfile);
                        if (initializeCharon(builder, mLogFile, mAppDir,
                                mCurrentProfile.getVpnType().has(VpnTypeFeature.BYOD))) {
                            Log.i(TAG, "charon started");
                            SettingsWriter writer = new SettingsWriter();
                            writer.setValue("global.language", Locale.getDefault().getLanguage());
                            writer.setValue("global.mtu", mCurrentProfile.getMTU());
                            writer.setValue("global.nat_keepalive", mCurrentProfile.getNATKeepAlive());
                            writer.setValue("connection.type", mCurrentProfile.getVpnType().getIdentifier());
                            writer.setValue("connection.server", mCurrentProfile.getGateway());
                            writer.setValue("connection.port", mCurrentProfile.getPort());
                            writer.setValue("connection.username", mCurrentProfile.getUsername());
                            writer.setValue("connection.password", mCurrentProfile.getPassword());
                            writer.setValue("connection.local_id", mCurrentProfile.getLocalId());
                            writer.setValue("connection.remote_id", mCurrentProfile.getRemoteId());
                            writer.setValue("connection.certreq",
                                    (mCurrentProfile.getFlags() & VpnProfile.FLAGS_SUPPRESS_CERT_REQS) == 0);
                            writer.setValue("connection.ike_proposal", mCurrentProfile.getIkeProposal());
                            writer.setValue("connection.esp_proposal", mCurrentProfile.getEspProposal());
                            initiate(writer.serialize());
                        } else {
                            Log.e(TAG, "failed to start charon");
                            setError(ErrorState.GENERIC_ERROR);
                            setState(State.DISABLED);
                            mCurrentProfile = null;
                        }
                    }
                } catch (InterruptedException ex) {
                    stopCurrentConnection();
                    setState(State.DISABLED);
                }
            }
        }
    }

    /**
     * Stop any existing connection by deinitializing charon.
     */
    private void stopCurrentConnection() {
        synchronized (this) {
            if (mCurrentProfile != null) {
                setState(State.DISCONNECTING);
                mIsDisconnecting = true;
                deinitializeCharon();
                Log.i(TAG, "charon stopped");
                mCurrentProfile = null;
                removeNotification();
            }
        }
    }

    /**
     * Add a permanent notification while we are connected to avoid the service getting killed by
     * the system when low on memory.
     */
    private void addNotification() {
        mShowNotification = true;
        startForeground(VPN_STATE_NOTIFICATION_ID, buildNotification(false));
    }

    /**
     * Remove the permanent notification.
     */
    private void removeNotification() {
        mShowNotification = false;
        stopForeground(true);
    }

    /**
     * Build a notification matching the current state
     */
    private Notification buildNotification(boolean publicVersion) {
        VpnProfile profile = mService.getProfile();
        State state = mService.getState();
        ErrorState error = mService.getErrorState();
        String name = "";
        boolean add_action = false;

        if (profile != null) {
            name = profile.getName();
        }
        android.support.v4.app.NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.drawable.ic_notification).setCategory(NotificationCompat.CATEGORY_SERVICE)
                .setVisibility(publicVersion ? NotificationCompat.VISIBILITY_PUBLIC
                        : NotificationCompat.VISIBILITY_PRIVATE);
        int s = R.string.state_disabled;
        if (error != ErrorState.NO_ERROR) {
            s = R.string.state_error;
            builder.setSmallIcon(R.drawable.ic_notification_warning);
            builder.setColor(ContextCompat.getColor(this, R.color.error_text));
        } else {
            switch (state) {
            case CONNECTING:
                s = R.string.state_connecting;
                builder.setSmallIcon(R.drawable.ic_notification_warning);
                builder.setColor(ContextCompat.getColor(this, R.color.warning_text));
                add_action = true;
                break;
            case CONNECTED:
                s = R.string.state_connected;
                builder.setColor(ContextCompat.getColor(this, R.color.success_text));
                builder.setUsesChronometer(true);
                add_action = true;
                break;
            case DISCONNECTING:
                s = R.string.state_disconnecting;
                break;
            }
        }
        builder.setContentTitle(getString(s));
        if (!publicVersion) {
            if (add_action) {
                Intent intent = new Intent(getApplicationContext(), MainActivity.class);
                intent.setAction(MainActivity.DISCONNECT);
                PendingIntent pending = PendingIntent.getActivity(getApplicationContext(), 0, intent,
                        PendingIntent.FLAG_UPDATE_CURRENT);
                builder.addAction(R.drawable.ic_notification_disconnect, getString(R.string.disconnect), pending);
            }
            builder.setContentText(name);
            builder.setPublicVersion(buildNotification(true));
        }

        Intent intent = new Intent(getApplicationContext(), MainActivity.class);
        PendingIntent pending = PendingIntent.getActivity(getApplicationContext(), 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        builder.setContentIntent(pending);
        return builder.build();
    }

    @Override
    public void stateChanged() {
        if (mShowNotification) {
            NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            manager.notify(VPN_STATE_NOTIFICATION_ID, buildNotification(false));
        }
    }

    /**
     * Notify the state service about a new connection attempt.
     * Called by the handler thread.
     *
     * @param profile currently active VPN profile
     */
    private void startConnection(VpnProfile profile) {
        synchronized (mServiceLock) {
            if (mService != null) {
                mService.startConnection(profile);
            }
        }
    }

    /**
     * Update the current VPN state on the state service. Called by the handler
     * thread and any of charon's threads.
     *
     * @param state current state
     */
    private void setState(State state) {
        synchronized (mServiceLock) {
            if (mService != null) {
                mService.setState(state);
            }
        }
    }

    /**
     * Set an error on the state service. Called by the handler thread and any
     * of charon's threads.
     *
     * @param error error state
     */
    private void setError(ErrorState error) {
        synchronized (mServiceLock) {
            if (mService != null) {
                mService.setError(error);
            }
        }
    }

    /**
     * Set the IMC state on the state service. Called by the handler thread and
     * any of charon's threads.
     *
     * @param state IMC state
     */
    private void setImcState(ImcState state) {
        synchronized (mServiceLock) {
            if (mService != null) {
                mService.setImcState(state);
            }
        }
    }

    /**
     * Set an error on the state service. Called by the handler thread and any
     * of charon's threads.
     *
     * @param error error state
     */
    private void setErrorDisconnect(ErrorState error) {
        synchronized (mServiceLock) {
            if (mService != null) {
                if (!mIsDisconnecting) {
                    mService.setError(error);
                }
            }
        }
    }

    /**
     * Updates the state of the current connection.
     * Called via JNI by different threads (but not concurrently).
     *
     * @param status new state
     */
    public void updateStatus(int status) {
        switch (status) {
        case STATE_CHILD_SA_DOWN:
            if (!mIsDisconnecting) {
                setState(State.CONNECTING);
            }
            break;
        case STATE_CHILD_SA_UP:
            setState(State.CONNECTED);
            break;
        case STATE_AUTH_ERROR:
            setErrorDisconnect(ErrorState.AUTH_FAILED);
            break;
        case STATE_PEER_AUTH_ERROR:
            setErrorDisconnect(ErrorState.PEER_AUTH_FAILED);
            break;
        case STATE_LOOKUP_ERROR:
            setErrorDisconnect(ErrorState.LOOKUP_FAILED);
            break;
        case STATE_UNREACHABLE_ERROR:
            setErrorDisconnect(ErrorState.UNREACHABLE);
            break;
        case STATE_GENERIC_ERROR:
            setErrorDisconnect(ErrorState.GENERIC_ERROR);
            break;
        default:
            Log.e(TAG, "Unknown status code received");
            break;
        }
    }

    /**
     * Updates the IMC state of the current connection.
     * Called via JNI by different threads (but not concurrently).
     *
     * @param value new state
     */
    public void updateImcState(int value) {
        ImcState state = ImcState.fromValue(value);
        if (state != null) {
            setImcState(state);
        }
    }

    /**
     * Add a remediation instruction to the VPN state service.
     * Called via JNI by different threads (but not concurrently).
     *
     * @param xml XML text
     */
    public void addRemediationInstruction(String xml) {
        for (RemediationInstruction instruction : RemediationInstruction.fromXml(xml)) {
            synchronized (mServiceLock) {
                if (mService != null) {
                    mService.addRemediationInstruction(instruction);
                }
            }
        }
    }

    /**
     * Function called via JNI to generate a list of DER encoded CA certificates
     * as byte array.
     *
     * @return a list of DER encoded CA certificates
     */
    private byte[][] getTrustedCertificates() {
        ArrayList<byte[]> certs = new ArrayList<byte[]>();
        TrustedCertificateManager certman = TrustedCertificateManager.getInstance().load();
        try {
            String alias = this.mCurrentCertificateAlias;
            if (alias != null) {
                X509Certificate cert = certman.getCACertificateFromAlias(alias);
                if (cert == null) {
                    return null;
                }
                certs.add(cert.getEncoded());
            } else {
                for (X509Certificate cert : certman.getAllCACertificates().values()) {
                    certs.add(cert.getEncoded());
                }
            }
        } catch (CertificateEncodingException e) {
            e.printStackTrace();
            return null;
        }
        return certs.toArray(new byte[certs.size()][]);
    }

    /**
     * Function called via JNI to get a list containing the DER encoded certificates
     * of the user selected certificate chain (beginning with the user certificate).
     *
     * Since this method is called from a thread of charon's thread pool we are safe
     * to call methods on KeyChain directly.
     *
     * @return list containing the certificates (first element is the user certificate)
     * @throws InterruptedException
     * @throws KeyChainException
     * @throws CertificateEncodingException
     */
    private byte[][] getUserCertificate()
            throws KeyChainException, InterruptedException, CertificateEncodingException {
        ArrayList<byte[]> encodings = new ArrayList<byte[]>();
        X509Certificate[] chain = KeyChain.getCertificateChain(getApplicationContext(),
                mCurrentUserCertificateAlias);
        if (chain == null || chain.length == 0) {
            return null;
        }
        for (X509Certificate cert : chain) {
            encodings.add(cert.getEncoded());
        }
        return encodings.toArray(new byte[encodings.size()][]);
    }

    /**
     * Function called via JNI to get the private key the user selected.
     *
     * Since this method is called from a thread of charon's thread pool we are safe
     * to call methods on KeyChain directly.
     *
     * @return the private key
     * @throws InterruptedException
     * @throws KeyChainException
     * @throws CertificateEncodingException
     */
    private PrivateKey getUserKey() throws KeyChainException, InterruptedException {
        return KeyChain.getPrivateKey(getApplicationContext(), mCurrentUserCertificateAlias);
    }

    /**
     * Initialization of charon, provided by libandroidbridge.so
     *
     * @param builder BuilderAdapter for this connection
     * @param logfile absolute path to the logfile
     * @param appdir absolute path to the data directory of the app
     * @param byod enable BYOD features
     * @return TRUE if initialization was successful
     */
    public native boolean initializeCharon(BuilderAdapter builder, String logfile, String appdir, boolean byod);

    /**
     * Deinitialize charon, provided by libandroidbridge.so
     */
    public native void deinitializeCharon();

    /**
     * Initiate VPN, provided by libandroidbridge.so
     */
    public native void initiate(String config);

    /**
     * Adapter for VpnService.Builder which is used to access it safely via JNI.
     * There is a corresponding C object to access it from native code.
     */
    public class BuilderAdapter {
        private final VpnProfile mProfile;
        private VpnService.Builder mBuilder;
        private BuilderCache mCache;
        private BuilderCache mEstablishedCache;

        public BuilderAdapter(VpnProfile profile) {
            mProfile = profile;
            mBuilder = createBuilder(mProfile.getName());
            mCache = new BuilderCache(mProfile);
        }

        private VpnService.Builder createBuilder(String name) {
            VpnService.Builder builder = new CharonVpnService.Builder();
            builder.setSession(name);

            /* even though the option displayed in the system dialog says "Configure"
             * we just use our main Activity */
            Context context = getApplicationContext();
            Intent intent = new Intent(context, MainActivity.class);
            PendingIntent pending = PendingIntent.getActivity(context, 0, intent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            builder.setConfigureIntent(pending);
            return builder;
        }

        public synchronized boolean addAddress(String address, int prefixLength) {
            try {
                mCache.addAddress(address, prefixLength);
            } catch (IllegalArgumentException ex) {
                return false;
            }
            return true;
        }

        public synchronized boolean addDnsServer(String address) {
            try {
                mBuilder.addDnsServer(address);
                mCache.recordAddressFamily(address);
            } catch (IllegalArgumentException ex) {
                return false;
            }
            return true;
        }

        public synchronized boolean addRoute(String address, int prefixLength) {
            try {
                mCache.addRoute(address, prefixLength);
            } catch (IllegalArgumentException ex) {
                return false;
            }
            return true;
        }

        public synchronized boolean addSearchDomain(String domain) {
            try {
                mBuilder.addSearchDomain(domain);
            } catch (IllegalArgumentException ex) {
                return false;
            }
            return true;
        }

        public synchronized boolean setMtu(int mtu) {
            try {
                mCache.setMtu(mtu);
            } catch (IllegalArgumentException ex) {
                return false;
            }
            return true;
        }

        public synchronized int establish() {
            ParcelFileDescriptor fd;
            try {
                mCache.applyData(mBuilder);
                fd = mBuilder.establish();
            } catch (Exception ex) {
                ex.printStackTrace();
                return -1;
            }
            if (fd == null) {
                return -1;
            }
            /* now that the TUN device is created we don't need the current
             * builder anymore, but we might need another when reestablishing */
            mBuilder = createBuilder(mProfile.getName());
            mEstablishedCache = mCache;
            mCache = new BuilderCache(mProfile);
            return fd.detachFd();
        }

        public synchronized int establishNoDns() {
            ParcelFileDescriptor fd;

            if (mEstablishedCache == null) {
                return -1;
            }
            try {
                Builder builder = createBuilder(mProfile.getName());
                mEstablishedCache.applyData(builder);
                fd = builder.establish();
            } catch (Exception ex) {
                ex.printStackTrace();
                return -1;
            }
            if (fd == null) {
                return -1;
            }
            return fd.detachFd();
        }
    }

    /**
     * Cache non DNS related information so we can recreate the builder without
     * that information when reestablishing IKE_SAs
     */
    public class BuilderCache {
        private final List<IPRange> mAddresses = new ArrayList<>();
        private final List<IPRange> mRoutesIPv4 = new ArrayList<>();
        private final List<IPRange> mRoutesIPv6 = new ArrayList<>();
        private final IPRangeSet mIncludedSubnetsv4 = new IPRangeSet();
        private final IPRangeSet mIncludedSubnetsv6 = new IPRangeSet();
        private final IPRangeSet mExcludedSubnets;
        private final int mSplitTunneling;
        private final SelectedAppsHandling mAppHandling;
        private final SortedSet<String> mSelectedApps;
        private int mMtu;
        private boolean mIPv4Seen, mIPv6Seen;

        public BuilderCache(VpnProfile profile) {
            IPRangeSet included = IPRangeSet.fromString(profile.getIncludedSubnets());
            for (IPRange range : included) {
                if (range.getFrom() instanceof Inet4Address) {
                    mIncludedSubnetsv4.add(range);
                } else if (range.getFrom() instanceof Inet6Address) {
                    mIncludedSubnetsv6.add(range);
                }
            }
            mExcludedSubnets = IPRangeSet.fromString(profile.getExcludedSubnets());
            Integer splitTunneling = profile.getSplitTunneling();
            mSplitTunneling = splitTunneling != null ? splitTunneling : 0;
            mAppHandling = profile.getSelectedAppsHandling();
            mSelectedApps = profile.getSelectedAppsSet();
        }

        public void addAddress(String address, int prefixLength) {
            try {
                mAddresses.add(new IPRange(address, prefixLength));
                recordAddressFamily(address);
            } catch (UnknownHostException ex) {
                ex.printStackTrace();
            }
        }

        public void addRoute(String address, int prefixLength) {
            try {
                if (isIPv6(address)) {
                    mRoutesIPv6.add(new IPRange(address, prefixLength));
                } else {
                    mRoutesIPv4.add(new IPRange(address, prefixLength));
                }
            } catch (UnknownHostException ex) {
                ex.printStackTrace();
            }
        }

        public void setMtu(int mtu) {
            mMtu = mtu;
        }

        public void recordAddressFamily(String address) {
            try {
                if (isIPv6(address)) {
                    mIPv6Seen = true;
                } else {
                    mIPv4Seen = true;
                }
            } catch (UnknownHostException ex) {
                ex.printStackTrace();
            }
        }

        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        public void applyData(VpnService.Builder builder) {
            for (IPRange address : mAddresses) {
                builder.addAddress(address.getFrom(), address.getPrefix());
            }
            /* add routes depending on whether split tunneling is allowed or not,
             * that is, whether we have to handle and block non-VPN traffic */
            if ((mSplitTunneling & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) == 0) {
                if (mIPv4Seen) { /* split tunneling is used depending on the routes and configuration */
                    IPRangeSet ranges = new IPRangeSet();
                    if (mIncludedSubnetsv4.size() > 0) {
                        ranges.add(mIncludedSubnetsv4);
                    } else {
                        ranges.addAll(mRoutesIPv4);
                    }
                    ranges.remove(mExcludedSubnets);
                    for (IPRange subnet : ranges.subnets()) {
                        try {
                            builder.addRoute(subnet.getFrom(), subnet.getPrefix());
                        } catch (IllegalArgumentException e) { /* some Android versions don't seem to like multicast addresses here,
                                                               * ignore it for now */
                            if (!subnet.getFrom().isMulticastAddress()) {
                                throw e;
                            }
                        }
                    }
                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { /* allow traffic that would otherwise be blocked to bypass the VPN */
                    builder.allowFamily(OsConstants.AF_INET);
                }
            } else if (mIPv4Seen) { /* only needed if we've seen any addresses.  otherwise, traffic
                                    * is blocked by default (we also install no routes in that case) */
                builder.addRoute("0.0.0.0", 0);
            }
            /* same thing for IPv6 */
            if ((mSplitTunneling & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) == 0) {
                if (mIPv6Seen) {
                    IPRangeSet ranges = new IPRangeSet();
                    if (mIncludedSubnetsv6.size() > 0) {
                        ranges.add(mIncludedSubnetsv6);
                    } else {
                        ranges.addAll(mRoutesIPv6);
                    }
                    ranges.remove(mExcludedSubnets);
                    for (IPRange subnet : ranges.subnets()) {
                        try {
                            builder.addRoute(subnet.getFrom(), subnet.getPrefix());
                        } catch (IllegalArgumentException e) {
                            if (!subnet.getFrom().isMulticastAddress()) {
                                throw e;
                            }
                        }
                    }
                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    builder.allowFamily(OsConstants.AF_INET6);
                }
            } else if (mIPv6Seen) {
                builder.addRoute("::", 0);
            }
            /* apply selected applications */
            if (mSelectedApps.size() > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                switch (mAppHandling) {
                case SELECTED_APPS_EXCLUDE:
                    for (String app : mSelectedApps) {
                        try {
                            builder.addDisallowedApplication(app);
                        } catch (PackageManager.NameNotFoundException e) {
                            // possible if not configured via GUI or app was uninstalled
                        }
                    }
                    break;
                case SELECTED_APPS_ONLY:
                    for (String app : mSelectedApps) {
                        try {
                            builder.addAllowedApplication(app);
                        } catch (PackageManager.NameNotFoundException e) {
                            // possible if not configured via GUI or app was uninstalled
                        }
                    }
                    break;
                default:
                    break;
                }
            }
            builder.setMtu(mMtu);
        }

        private boolean isIPv6(String address) throws UnknownHostException {
            InetAddress addr = InetAddress.getByName(address);
            if (addr instanceof Inet4Address) {
                return false;
            } else if (addr instanceof Inet6Address) {
                return true;
            }
            return false;
        }
    }

    /**
     * Function called via JNI to determine information about the Android version.
     */
    private static String getAndroidVersion() {
        String version = "Android " + Build.VERSION.RELEASE + " - " + Build.DISPLAY;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            version += "/" + Build.VERSION.SECURITY_PATCH;
        }
        return version;
    }

    /**
     * Function called via JNI to determine information about the device.
     */
    private static String getDeviceString() {
        return Build.MODEL + " - " + Build.BRAND + "/" + Build.PRODUCT + "/" + Build.MANUFACTURER;
    }
}