Home | History | Annotate | Download | only in vpn
      1 /*
      2  * Copyright (C) 2009, The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.vpn;
     18 
     19 import android.app.Notification;
     20 import android.app.NotificationManager;
     21 import android.app.PendingIntent;
     22 import android.content.Context;
     23 import android.net.vpn.VpnManager;
     24 import android.net.vpn.VpnProfile;
     25 import android.net.vpn.VpnState;
     26 import android.os.SystemProperties;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 
     30 import java.io.IOException;
     31 import java.io.Serializable;
     32 import java.net.DatagramSocket;
     33 import java.net.InetAddress;
     34 import java.net.NetworkInterface;
     35 import java.net.UnknownHostException;
     36 
     37 /**
     38  * The service base class for managing a type of VPN connection.
     39  */
     40 abstract class VpnService<E extends VpnProfile> implements Serializable {
     41     static final long serialVersionUID = 1L;
     42     private static final boolean DBG = true;
     43     private static final int NOTIFICATION_ID = 1;
     44 
     45     private static final String DNS1 = "net.dns1";
     46     private static final String DNS2 = "net.dns2";
     47     private static final String VPN_DNS1 = "vpn.dns1";
     48     private static final String VPN_DNS2 = "vpn.dns2";
     49     private static final String VPN_STATUS = "vpn.status";
     50     private static final String VPN_IS_UP = "ok";
     51     private static final String VPN_IS_DOWN = "down";
     52 
     53     private static final String REMOTE_IP = "net.ipremote";
     54     private static final String DNS_DOMAIN_SUFFICES = "net.dns.search";
     55 
     56     private final String TAG = VpnService.class.getSimpleName();
     57 
     58     // FIXME: profile is only needed in connecting phase, so we can just save
     59     // the profile name and service class name for recovery
     60     E mProfile;
     61     transient VpnServiceBinder mContext;
     62 
     63     private VpnState mState = VpnState.IDLE;
     64     private Throwable mError;
     65 
     66     // connection settings
     67     private String mOriginalDns1;
     68     private String mOriginalDns2;
     69     private String mOriginalDomainSuffices;
     70     private String mLocalIp;
     71     private String mLocalIf;
     72 
     73     private long mStartTime; // VPN connection start time
     74 
     75     // for helping managing daemons
     76     private VpnDaemons mDaemons = new VpnDaemons();
     77 
     78     // for helping showing, updating notification
     79     private transient NotificationHelper mNotification;
     80 
     81     /**
     82      * Establishes a VPN connection with the specified username and password.
     83      */
     84     protected abstract void connect(String serverIp, String username,
     85             String password) throws IOException;
     86 
     87     /**
     88      * Returns the daemons management class for this service object.
     89      */
     90     protected VpnDaemons getDaemons() {
     91         return mDaemons;
     92     }
     93 
     94     /**
     95      * Returns the VPN profile associated with the connection.
     96      */
     97     protected E getProfile() {
     98         return mProfile;
     99     }
    100 
    101     /**
    102      * Returns the IP address of the specified host name.
    103      */
    104     protected String getIp(String hostName) throws IOException {
    105         return InetAddress.getByName(hostName).getHostAddress();
    106     }
    107 
    108     void setContext(VpnServiceBinder context, E profile) {
    109         mProfile = profile;
    110         recover(context);
    111     }
    112 
    113     void recover(VpnServiceBinder context) {
    114         mContext = context;
    115         mNotification = new NotificationHelper();
    116 
    117         if (VpnState.CONNECTED.equals(mState)) {
    118             Log.i("VpnService", "     recovered: " + mProfile.getName());
    119             startConnectivityMonitor();
    120         }
    121     }
    122 
    123     VpnState getState() {
    124         return mState;
    125     }
    126 
    127     synchronized boolean onConnect(String username, String password) {
    128         try {
    129             setState(VpnState.CONNECTING);
    130 
    131             mDaemons.stopAll();
    132             String serverIp = getIp(getProfile().getServerName());
    133             saveLocalIpAndInterface(serverIp);
    134             onBeforeConnect();
    135             connect(serverIp, username, password);
    136             waitUntilConnectedOrTimedout();
    137             return true;
    138         } catch (Throwable e) {
    139             onError(e);
    140             return false;
    141         }
    142     }
    143 
    144     synchronized void onDisconnect() {
    145         try {
    146             Log.i(TAG, "disconnecting VPN...");
    147             setState(VpnState.DISCONNECTING);
    148             mNotification.showDisconnect();
    149 
    150             mDaemons.stopAll();
    151         } catch (Throwable e) {
    152             Log.e(TAG, "onDisconnect()", e);
    153         } finally {
    154             onFinalCleanUp();
    155         }
    156     }
    157 
    158     private void onError(Throwable error) {
    159         // error may occur during or after connection setup
    160         // and it may be due to one or all services gone
    161         if (mError != null) {
    162             Log.w(TAG, "   multiple errors occur, record the last one: "
    163                     + error);
    164         }
    165         Log.e(TAG, "onError()", error);
    166         mError = error;
    167         onDisconnect();
    168     }
    169 
    170     private void onError(int errorCode) {
    171         onError(new VpnConnectingError(errorCode));
    172     }
    173 
    174 
    175     private void onBeforeConnect() throws IOException {
    176         mNotification.disableNotification();
    177 
    178         SystemProperties.set(VPN_DNS1, "");
    179         SystemProperties.set(VPN_DNS2, "");
    180         SystemProperties.set(VPN_STATUS, VPN_IS_DOWN);
    181         if (DBG) {
    182             Log.d(TAG, "       VPN UP: " + SystemProperties.get(VPN_STATUS));
    183         }
    184     }
    185 
    186     private void waitUntilConnectedOrTimedout() throws IOException {
    187         sleep(2000); // 2 seconds
    188         for (int i = 0; i < 80; i++) {
    189             if (mState != VpnState.CONNECTING) {
    190                 break;
    191             } else if (VPN_IS_UP.equals(
    192                     SystemProperties.get(VPN_STATUS))) {
    193                 onConnected();
    194                 return;
    195             } else {
    196                 int err = mDaemons.getSocketError();
    197                 if (err != 0) {
    198                     onError(err);
    199                     return;
    200                 }
    201             }
    202             sleep(500); // 0.5 second
    203         }
    204 
    205         if (mState == VpnState.CONNECTING) {
    206             onError(new IOException("Connecting timed out"));
    207         }
    208     }
    209 
    210     private synchronized void onConnected() throws IOException {
    211         if (DBG) Log.d(TAG, "onConnected()");
    212 
    213         mDaemons.closeSockets();
    214         saveOriginalDns();
    215         saveAndSetDomainSuffices();
    216 
    217         mStartTime = System.currentTimeMillis();
    218 
    219         // Correct order to make sure VpnService doesn't break when killed:
    220         // (1) set state to CONNECTED
    221         // (2) save states
    222         // (3) set DNS
    223         setState(VpnState.CONNECTED);
    224         saveSelf();
    225         setVpnDns();
    226 
    227         startConnectivityMonitor();
    228     }
    229 
    230     private void saveSelf() throws IOException {
    231         mContext.saveStates();
    232     }
    233 
    234     private synchronized void onFinalCleanUp() {
    235         if (DBG) Log.d(TAG, "onFinalCleanUp()");
    236 
    237         if (mState == VpnState.IDLE) return;
    238 
    239         // keep the notification when error occurs
    240         if (!anyError()) mNotification.disableNotification();
    241 
    242         restoreOriginalDns();
    243         restoreOriginalDomainSuffices();
    244         setState(VpnState.IDLE);
    245 
    246         // stop the service itself
    247         SystemProperties.set(VPN_STATUS, VPN_IS_DOWN);
    248         mContext.removeStates();
    249         mContext.stopSelf();
    250     }
    251 
    252     private boolean anyError() {
    253         return (mError != null);
    254     }
    255 
    256     private void restoreOriginalDns() {
    257         // restore only if they are not overridden
    258         String vpnDns1 = SystemProperties.get(VPN_DNS1);
    259         if (vpnDns1.equals(SystemProperties.get(DNS1))) {
    260             Log.i(TAG, String.format("restore original dns prop: %s --> %s",
    261                     SystemProperties.get(DNS1), mOriginalDns1));
    262             Log.i(TAG, String.format("restore original dns prop: %s --> %s",
    263                     SystemProperties.get(DNS2), mOriginalDns2));
    264             SystemProperties.set(DNS1, mOriginalDns1);
    265             SystemProperties.set(DNS2, mOriginalDns2);
    266         }
    267     }
    268 
    269     private void saveOriginalDns() {
    270         mOriginalDns1 = SystemProperties.get(DNS1);
    271         mOriginalDns2 = SystemProperties.get(DNS2);
    272         Log.i(TAG, String.format("save original dns prop: %s, %s",
    273                 mOriginalDns1, mOriginalDns2));
    274     }
    275 
    276     private void setVpnDns() {
    277         String vpnDns1 = SystemProperties.get(VPN_DNS1);
    278         String vpnDns2 = SystemProperties.get(VPN_DNS2);
    279         SystemProperties.set(DNS1, vpnDns1);
    280         SystemProperties.set(DNS2, vpnDns2);
    281         Log.i(TAG, String.format("set vpn dns prop: %s, %s",
    282                 vpnDns1, vpnDns2));
    283     }
    284 
    285     private void saveAndSetDomainSuffices() {
    286         mOriginalDomainSuffices = SystemProperties.get(DNS_DOMAIN_SUFFICES);
    287         Log.i(TAG, "save original suffices: " + mOriginalDomainSuffices);
    288         String list = mProfile.getDomainSuffices();
    289         if (!TextUtils.isEmpty(list)) {
    290             SystemProperties.set(DNS_DOMAIN_SUFFICES, list);
    291         }
    292     }
    293 
    294     private void restoreOriginalDomainSuffices() {
    295         Log.i(TAG, "restore original suffices --> " + mOriginalDomainSuffices);
    296         SystemProperties.set(DNS_DOMAIN_SUFFICES, mOriginalDomainSuffices);
    297     }
    298 
    299     private void setState(VpnState newState) {
    300         mState = newState;
    301         broadcastConnectivity(newState);
    302     }
    303 
    304     private void broadcastConnectivity(VpnState s) {
    305         VpnManager m = new VpnManager(mContext);
    306         Throwable err = mError;
    307         if ((s == VpnState.IDLE) && (err != null)) {
    308             if (err instanceof UnknownHostException) {
    309                 m.broadcastConnectivity(mProfile.getName(), s,
    310                         VpnManager.VPN_ERROR_UNKNOWN_SERVER);
    311             } else if (err instanceof VpnConnectingError) {
    312                 m.broadcastConnectivity(mProfile.getName(), s,
    313                         ((VpnConnectingError) err).getErrorCode());
    314             } else if (VPN_IS_UP.equals(SystemProperties.get(VPN_STATUS))) {
    315                 m.broadcastConnectivity(mProfile.getName(), s,
    316                         VpnManager.VPN_ERROR_CONNECTION_LOST);
    317             } else {
    318                 m.broadcastConnectivity(mProfile.getName(), s,
    319                         VpnManager.VPN_ERROR_CONNECTION_FAILED);
    320             }
    321         } else {
    322             m.broadcastConnectivity(mProfile.getName(), s);
    323         }
    324     }
    325 
    326     private void startConnectivityMonitor() {
    327         new Thread(new Runnable() {
    328             public void run() {
    329                 Log.i(TAG, "VPN connectivity monitor running");
    330                 try {
    331                     for (int i = 10; ; i--) {
    332                         long now = System.currentTimeMillis();
    333 
    334                         boolean heavyCheck = i == 0;
    335                         synchronized (VpnService.this) {
    336                             if (mState != VpnState.CONNECTED) break;
    337                             mNotification.update(now);
    338 
    339                             if (heavyCheck) {
    340                                 i = 10;
    341                                 if (checkConnectivity()) checkDns();
    342                             }
    343                             long t = 1000L - System.currentTimeMillis() + now;
    344                             if (t > 100L) VpnService.this.wait(t);
    345                         }
    346                     }
    347                 } catch (InterruptedException e) {
    348                     onError(e);
    349                 }
    350                 Log.i(TAG, "VPN connectivity monitor stopped");
    351             }
    352         }).start();
    353     }
    354 
    355     private void saveLocalIpAndInterface(String serverIp) throws IOException {
    356         DatagramSocket s = new DatagramSocket();
    357         int port = 80; // arbitrary
    358         s.connect(InetAddress.getByName(serverIp), port);
    359         InetAddress localIp = s.getLocalAddress();
    360         mLocalIp = localIp.getHostAddress();
    361         NetworkInterface localIf = NetworkInterface.getByInetAddress(localIp);
    362         mLocalIf = (localIf == null) ? null : localIf.getName();
    363         if (TextUtils.isEmpty(mLocalIf)) {
    364             throw new IOException("Local interface is empty!");
    365         }
    366         if (DBG) {
    367             Log.d(TAG, "  Local IP: " + mLocalIp + ", if: " + mLocalIf);
    368         }
    369     }
    370 
    371     // returns false if vpn connectivity is broken
    372     private boolean checkConnectivity() {
    373         if (mDaemons.anyDaemonStopped() || isLocalIpChanged()) {
    374             onError(new IOException("Connectivity lost"));
    375             return false;
    376         } else {
    377             return true;
    378         }
    379     }
    380 
    381     private void checkDns() {
    382         String dns1 = SystemProperties.get(DNS1);
    383         String vpnDns1 = SystemProperties.get(VPN_DNS1);
    384         if (!dns1.equals(vpnDns1) && dns1.equals(mOriginalDns1)) {
    385             // dhcp expires?
    386             setVpnDns();
    387         }
    388     }
    389 
    390     private boolean isLocalIpChanged() {
    391         try {
    392             InetAddress localIp = InetAddress.getByName(mLocalIp);
    393             NetworkInterface localIf =
    394                     NetworkInterface.getByInetAddress(localIp);
    395             if (localIf == null || !mLocalIf.equals(localIf.getName())) {
    396                 Log.w(TAG, "       local If changed from " + mLocalIf
    397                         + " to " + localIf);
    398                 return true;
    399             } else {
    400                 return false;
    401             }
    402         } catch (IOException e) {
    403             Log.w(TAG, "isLocalIpChanged()", e);
    404             return true;
    405         }
    406     }
    407 
    408     protected void sleep(int ms) {
    409         try {
    410             Thread.currentThread().sleep(ms);
    411         } catch (InterruptedException e) {
    412         }
    413     }
    414 
    415     private class DaemonHelper implements Serializable {
    416     }
    417 
    418     // Helper class for showing, updating notification.
    419     private class NotificationHelper {
    420         void update(long now) {
    421             String title = getNotificationTitle(true);
    422             Notification n = new Notification(R.drawable.vpn_connected, title,
    423                     mStartTime);
    424             n.setLatestEventInfo(mContext, title,
    425                     getConnectedNotificationMessage(now),
    426                     prepareNotificationIntent());
    427             n.flags |= Notification.FLAG_NO_CLEAR;
    428             n.flags |= Notification.FLAG_ONGOING_EVENT;
    429             enableNotification(n);
    430         }
    431 
    432         void showDisconnect() {
    433             String title = getNotificationTitle(false);
    434             Notification n = new Notification(R.drawable.vpn_disconnected,
    435                     title, System.currentTimeMillis());
    436             n.setLatestEventInfo(mContext, title,
    437                     getDisconnectedNotificationMessage(),
    438                     prepareNotificationIntent());
    439             n.flags |= Notification.FLAG_AUTO_CANCEL;
    440             disableNotification();
    441             enableNotification(n);
    442         }
    443 
    444         void disableNotification() {
    445             ((NotificationManager) mContext.getSystemService(
    446                     Context.NOTIFICATION_SERVICE)).cancel(NOTIFICATION_ID);
    447         }
    448 
    449         private void enableNotification(Notification n) {
    450             ((NotificationManager) mContext.getSystemService(
    451                     Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, n);
    452         }
    453 
    454         private PendingIntent prepareNotificationIntent() {
    455             return PendingIntent.getActivity(mContext, 0,
    456                     new VpnManager(mContext).createSettingsActivityIntent(), 0);
    457         }
    458 
    459         private String getNotificationTitle(boolean connected) {
    460             String formatString = connected
    461                     ? mContext.getString(
    462                             R.string.vpn_notification_title_connected)
    463                     : mContext.getString(
    464                             R.string.vpn_notification_title_disconnected);
    465             return String.format(formatString, mProfile.getName());
    466         }
    467 
    468         private String getFormattedTime(int duration) {
    469             int hours = duration / 3600;
    470             StringBuilder sb = new StringBuilder();
    471             if (hours > 0) sb.append(hours).append(':');
    472             sb.append(String.format("%02d:%02d", (duration % 3600 / 60),
    473                     (duration % 60)));
    474             return sb.toString();
    475         }
    476 
    477         private String getConnectedNotificationMessage(long now) {
    478             return getFormattedTime((int) (now - mStartTime) / 1000);
    479         }
    480 
    481         private String getDisconnectedNotificationMessage() {
    482             return mContext.getString(
    483                     R.string.vpn_notification_hint_disconnected);
    484         }
    485     }
    486 }
    487