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