blob: fdefe9cf7890e543219930e1400891d38a7809d1 [file] [log] [blame]
Hung-ying Tyanf94b6442009-06-08 13:27:11 +08001/*
2 * Copyright (C) 2007, 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
17package com.android.server.vpn;
18
19import android.app.Notification;
20import android.app.NotificationManager;
21import android.app.PendingIntent;
22import android.content.Context;
Hung-ying Tyanf94b6442009-06-08 13:27:11 +080023import android.net.vpn.VpnManager;
Hung-ying Tyan4c424d62009-06-15 11:30:11 +080024import android.net.vpn.VpnProfile;
Hung-ying Tyanf94b6442009-06-08 13:27:11 +080025import android.net.vpn.VpnState;
26import android.os.FileObserver;
27import android.os.SystemProperties;
28import android.util.Log;
29
30import java.io.File;
31import java.io.IOException;
32import java.net.InetAddress;
33import java.net.InetSocketAddress;
34import java.net.Socket;
35import java.util.ArrayList;
36import java.util.List;
37
38/**
39 * The service base class for managing a type of VPN connection.
40 */
Hung-ying Tyan4c424d62009-06-15 11:30:11 +080041abstract class VpnService<E extends VpnProfile> {
Hung-ying Tyanf94b6442009-06-08 13:27:11 +080042 private static final int NOTIFICATION_ID = 1;
43 private static final String PROFILES_ROOT = VpnManager.PROFILES_PATH + "/";
44 public static final String DEFAULT_CONFIG_PATH = "/etc";
45
46 private static final int DNS_TIMEOUT = 3000; // ms
47 private static final String DNS1 = "net.dns1";
48 private static final String DNS2 = "net.dns2";
49 private static final String REMOTE_IP = "net.ipremote";
50 private static final String DNS_DOMAIN_SUFFICES = "net.dns.search";
51 private static final String SERVER_IP = "net.vpn.server_ip";
52
53 private static final int VPN_TIMEOUT = 30000; // milliseconds
54 private static final int ONE_SECOND = 1000; // milliseconds
55 private static final int FIVE_SECOND = 5000; // milliseconds
56
57 private static final String LOGWRAPPER = "/system/bin/logwrapper";
58 private final String TAG = VpnService.class.getSimpleName();
59
60 E mProfile;
61 VpnServiceBinder mContext;
62
63 private VpnState mState = VpnState.IDLE;
64 private boolean mInError;
65
66 // connection settings
67 private String mOriginalDns1;
68 private String mOriginalDns2;
69 private String mVpnDns1 = "";
70 private String mVpnDns2 = "";
71 private String mOriginalDomainSuffices;
72 private String mHostIp;
73
74 private long mStartTime; // VPN connection start time
75
76 // monitors if the VPN connection is sucessfully established
77 private FileMonitor mConnectMonitor;
78
79 // watch dog timer; fired up if the connection cannot be established within
80 // VPN_TIMEOUT
81 private Object mWatchdog;
82
83 // for helping managing multiple Android services
84 private ServiceHelper mServiceHelper = new ServiceHelper();
85
86 // for helping showing, updating notification
87 private NotificationHelper mNotification = new NotificationHelper();
88
89 /**
90 * Establishes a VPN connection with the specified username and password.
91 */
92 protected abstract void connect(String serverIp, String username,
93 String password) throws IOException;
94
95 /**
96 * Tears down the VPN connection. The base class simply terminates all the
97 * Android services. A subclass may need to do some clean-up before that.
98 */
99 protected void disconnect() {
100 }
101
102 /**
103 * Starts an Android service defined in init.rc.
104 */
105 protected AndroidServiceProxy startService(String serviceName)
106 throws IOException {
107 return mServiceHelper.startService(serviceName);
108 }
109
110 protected String getPppOptionFilePath() throws IOException {
111 String subpath = getProfileSubpath("/ppp/peers");
112 String[] kids = new File(subpath).list();
113 if ((kids == null) || (kids.length == 0)) {
114 throw new IOException("no option file found in " + subpath);
115 }
116 if (kids.length > 1) {
117 Log.w(TAG, "more than one option file found in " + subpath
118 + ", arbitrarily choose " + kids[0]);
119 }
120 return subpath + "/" + kids[0];
121 }
122
123 /**
124 * Returns the VPN profile associated with the connection.
125 */
126 protected E getProfile() {
127 return mProfile;
128 }
129
130 /**
131 * Returns the profile path where configuration files reside.
132 */
133 protected String getProfilePath() throws IOException {
134 String path = PROFILES_ROOT + mProfile.getId();
135 File dir = new File(path);
136 if (!dir.exists()) throw new IOException("Profile dir does not exist");
137 return path;
138 }
139
140 /**
141 * Returns the path where default configuration files reside.
142 */
143 protected String getDefaultConfigPath() throws IOException {
144 return DEFAULT_CONFIG_PATH;
145 }
146
147 /**
148 * Returns the host IP for establishing the VPN connection.
149 */
150 protected String getHostIp() throws IOException {
151 if (mHostIp == null) mHostIp = reallyGetHostIp();
152 return mHostIp;
153 }
154
155 /**
156 * Returns the IP of the specified host name.
157 */
158 protected String getIp(String hostName) throws IOException {
159 InetAddress iaddr = InetAddress.getByName(hostName);
160 byte[] aa = iaddr.getAddress();
161 StringBuilder sb = new StringBuilder().append(byteToInt(aa[0]));
162 for (int i = 1; i < aa.length; i++) {
163 sb.append(".").append(byteToInt(aa[i]));
164 }
165 return sb.toString();
166 }
167
168 /**
169 * Returns the path of the script file that is executed when the VPN
170 * connection is established.
171 */
172 protected String getConnectMonitorFile() {
173 return "/etc/ppp/ip-up";
174 }
175
176 /**
177 * Sets the system property. The method is blocked until the value is
178 * settled in.
179 * @param name the name of the property
180 * @param value the value of the property
181 * @throws IOException if it fails to set the property within 2 seconds
182 */
183 protected void setSystemProperty(String name, String value)
184 throws IOException {
185 SystemProperties.set(name, value);
186 for (int i = 0; i < 5; i++) {
187 String v = SystemProperties.get(name);
188 if (v.equals(value)) {
189 return;
190 } else {
191 Log.d(TAG, "sys_prop: wait for " + name + " to settle in");
192 sleep(400);
193 }
194 }
195 throw new IOException("Failed to set system property: " + name);
196 }
197
198 void setContext(VpnServiceBinder context, E profile) {
199 mContext = context;
200 mProfile = profile;
201 }
202
203 VpnState getState() {
204 return mState;
205 }
206
207 synchronized void onConnect(String username, String password)
208 throws IOException {
209 mState = VpnState.CONNECTING;
210 broadcastConnectivity(VpnState.CONNECTING);
211
212 String serverIp = getIp(getProfile().getServerName());
213 setSystemProperty(SERVER_IP, serverIp);
214 onBeforeConnect();
215
216 connect(serverIp, username, password);
217 }
218
219 synchronized void onDisconnect(boolean cleanUpServices) {
220 try {
221 mState = VpnState.DISCONNECTING;
222 broadcastConnectivity(VpnState.DISCONNECTING);
223 mNotification.showDisconnect();
224
225 // subclass implementation
226 if (cleanUpServices) disconnect();
227
228 mServiceHelper.stop();
229 } catch (Throwable e) {
230 Log.e(TAG, "onError()", e);
231 onFinalCleanUp();
232 }
233 }
234
235 synchronized void onError() {
236 // error may occur during or after connection setup
237 // and it may be due to one or all services gone
238 mInError = true;
239 switch (mState) {
240 case CONNECTED:
241 onDisconnect(true);
242 break;
243
244 case CONNECTING:
245 onDisconnect(false);
246 break;
247 }
248 }
249
250 private void createConnectMonitor() {
251 mConnectMonitor = new FileMonitor(getConnectMonitorFile(),
252 new Runnable() {
253 public void run() {
254 onConnectMonitorTriggered();
255 }
256 });
257 }
258
259 private void onBeforeConnect() {
260 mNotification.disableNotification();
261
262 createConnectMonitor();
263 mConnectMonitor.startWatching();
264 saveOriginalDnsProperties();
265
266 mWatchdog = startTimer(VPN_TIMEOUT, new Runnable() {
267 public void run() {
268 synchronized (VpnService.this) {
269 if (mState == VpnState.CONNECTING) {
270 Log.d(TAG, " watchdog timer is fired !!");
271 onError();
272 }
273 }
274 }
275 });
276 }
277
278 private synchronized void onConnectMonitorTriggered() {
279 Log.d(TAG, "onConnectMonitorTriggered()");
280
281 stopTimer(mWatchdog);
282 mConnectMonitor.stopWatching();
283 saveVpnDnsProperties();
284 saveAndSetDomainSuffices();
285 startConnectivityMonitor();
286
287 mState = VpnState.CONNECTED;
288 broadcastConnectivity(VpnState.CONNECTED);
289 }
290
291 private synchronized void onFinalCleanUp() {
292 Log.d(TAG, "onFinalCleanUp()");
293
294 if (mState == VpnState.IDLE) return;
295
296 // keep the notification when error occurs
297 if (!mInError) mNotification.disableNotification();
298
299 restoreOriginalDnsProperties();
300 restoreOriginalDomainSuffices();
301 if (mConnectMonitor != null) mConnectMonitor.stopWatching();
302 if (mWatchdog != null) stopTimer(mWatchdog);
303 mState = VpnState.IDLE;
304 broadcastConnectivity(VpnState.IDLE);
305
306 // stop the service itself
307 mContext.stopSelf();
308 }
309
310 private synchronized void onOneServiceGone() {
311 switch (mState) {
312 case IDLE:
313 case DISCONNECTING:
314 break;
315
316 default:
317 onError();
318 }
319 }
320
321 private synchronized void onAllServicesGone() {
322 switch (mState) {
323 case IDLE:
324 break;
325
326 case DISCONNECTING:
327 // daemons are gone; now clean up everything
328 onFinalCleanUp();
329 break;
330
331 default:
332 onError();
333 }
334 }
335
336 private void saveOriginalDnsProperties() {
337 mOriginalDns1 = SystemProperties.get(DNS1);
338 mOriginalDns2 = SystemProperties.get(DNS2);
339 Log.d(TAG, String.format("save original dns prop: %s, %s",
340 mOriginalDns1, mOriginalDns2));
341 }
342
343 private void restoreOriginalDnsProperties() {
344 // restore only if they are not overridden
345 if (mVpnDns1.equals(SystemProperties.get(DNS1))) {
346 Log.d(TAG, String.format("restore original dns prop: %s --> %s",
347 SystemProperties.get(DNS1), mOriginalDns1));
348 Log.d(TAG, String.format("restore original dns prop: %s --> %s",
349 SystemProperties.get(DNS2), mOriginalDns2));
350 SystemProperties.set(DNS1, mOriginalDns1);
351 SystemProperties.set(DNS2, mOriginalDns2);
352 }
353 }
354
355 private void saveVpnDnsProperties() {
356 mVpnDns1 = mVpnDns2 = "";
357 for (int i = 0; i < 10; i++) {
358 mVpnDns1 = SystemProperties.get(DNS1);
359 mVpnDns2 = SystemProperties.get(DNS2);
360 if (mVpnDns1.equals(mOriginalDns1)) {
361 Log.d(TAG, "wait for vpn dns to settle in..." + i);
362 sleep(500);
363 } else {
364 Log.d(TAG, String.format("save vpn dns prop: %s, %s",
365 mVpnDns1, mVpnDns2));
366 return;
367 }
368 }
369 Log.e(TAG, "saveVpnDnsProperties(): DNS not updated??");
370 }
371
372 private void restoreVpnDnsProperties() {
373 if (isNullOrEmpty(mVpnDns1) && isNullOrEmpty(mVpnDns2)) {
374 return;
375 }
376 Log.d(TAG, String.format("restore vpn dns prop: %s --> %s",
377 SystemProperties.get(DNS1), mVpnDns1));
378 Log.d(TAG, String.format("restore vpn dns prop: %s --> %s",
379 SystemProperties.get(DNS2), mVpnDns2));
380 SystemProperties.set(DNS1, mVpnDns1);
381 SystemProperties.set(DNS2, mVpnDns2);
382 }
383
384 private void saveAndSetDomainSuffices() {
385 mOriginalDomainSuffices = SystemProperties.get(DNS_DOMAIN_SUFFICES);
386 Log.d(TAG, "save original dns search: " + mOriginalDomainSuffices);
387 String list = mProfile.getDomainSuffices();
388 if (!isNullOrEmpty(list)) {
389 SystemProperties.set(DNS_DOMAIN_SUFFICES, list);
390 }
391 }
392
393 private void restoreOriginalDomainSuffices() {
394 Log.d(TAG, "restore original dns search --> " + mOriginalDomainSuffices);
395 SystemProperties.set(DNS_DOMAIN_SUFFICES, mOriginalDomainSuffices);
396 }
397
398 private void broadcastConnectivity(VpnState s) {
399 new VpnManager(mContext).broadcastConnectivity(mProfile.getName(), s);
400 }
401
402 private void startConnectivityMonitor() {
403 mStartTime = System.currentTimeMillis();
404
405 new Thread(new Runnable() {
406 public void run() {
407 Log.d(TAG, " +++++ connectivity monitor running");
408 try {
409 for (;;) {
410 synchronized (VpnService.this) {
411 if (mState != VpnState.CONNECTED) break;
412 mNotification.update();
413 checkConnectivity();
414 VpnService.this.wait(ONE_SECOND);
415 }
416 }
417 } catch (InterruptedException e) {
418 Log.e(TAG, "connectivity monitor", e);
419 }
420 Log.d(TAG, " ----- connectivity monitor stopped");
421 }
422 }).start();
423 }
424
425 private void checkConnectivity() {
426 checkDnsProperties();
427 }
428
429 private void checkDnsProperties() {
430 String dns1 = SystemProperties.get(DNS1);
431 if (!mVpnDns1.equals(dns1)) {
432 Log.w(TAG, " @@ !!! dns being overridden");
433 onError();
434 }
435 }
436
437 private Object startTimer(final int milliseconds, final Runnable task) {
438 Thread thread = new Thread(new Runnable() {
439 public void run() {
440 Log.d(TAG, "watchdog timer started");
441 Thread t = Thread.currentThread();
442 try {
443 synchronized (t) {
444 t.wait(milliseconds);
445 }
446 task.run();
447 } catch (InterruptedException e) {
448 // ignored
449 }
450 Log.d(TAG, "watchdog timer stopped");
451 }
452 });
453 thread.start();
454 return thread;
455 }
456
457 private void stopTimer(Object timer) {
458 synchronized (timer) {
459 timer.notify();
460 }
461 }
462
463 private String reallyGetHostIp() throws IOException {
464 Socket s = new Socket();
465 s.connect(new InetSocketAddress("www.google.com", 80), DNS_TIMEOUT);
466 String ipAddress = s.getLocalAddress().getHostAddress();
467 Log.d(TAG, "Host IP: " + ipAddress);
468 s.close();
469 return ipAddress;
470 }
471
472 private String getProfileSubpath(String subpath) throws IOException {
473 String path = getProfilePath() + subpath;
474 if (new File(path).exists()) {
475 return path;
476 } else {
477 Log.w(TAG, "Profile subpath does not exist: " + path
478 + ", use default one");
479 String path2 = getDefaultConfigPath() + subpath;
480 if (!new File(path2).exists()) {
481 throw new IOException("Profile subpath does not exist at "
482 + path + " or " + path2);
483 }
484 return path2;
485 }
486 }
487
488 private void sleep(int ms) {
489 try {
490 Thread.currentThread().sleep(ms);
491 } catch (InterruptedException e) {
492 }
493 }
494
495 private static boolean isNullOrEmpty(String message) {
496 return ((message == null) || (message.length() == 0));
497 }
498
499 private static int byteToInt(byte b) {
500 return ((int) b) & 0x0FF;
501 }
502
503 private class ServiceHelper implements ProcessProxy.Callback {
504 private List<AndroidServiceProxy> mServiceList =
505 new ArrayList<AndroidServiceProxy>();
506
507 // starts an Android service
508 AndroidServiceProxy startService(String serviceName)
509 throws IOException {
510 AndroidServiceProxy service = new AndroidServiceProxy(serviceName);
511 mServiceList.add(service);
512 service.start(this);
513 return service;
514 }
515
516 // stops all the Android services
517 void stop() {
518 if (mServiceList.isEmpty()) {
519 onFinalCleanUp();
520 } else {
521 for (AndroidServiceProxy s : mServiceList) s.stop();
522 }
523 }
524
525 //@Override
526 public void done(ProcessProxy p) {
527 Log.d(TAG, "service done: " + p.getName());
528 commonCallback((AndroidServiceProxy) p);
529 }
530
531 //@Override
532 public void error(ProcessProxy p, Throwable e) {
533 Log.e(TAG, "service error: " + p.getName(), e);
534 commonCallback((AndroidServiceProxy) p);
535 }
536
537 private void commonCallback(AndroidServiceProxy service) {
538 mServiceList.remove(service);
539 onOneServiceGone();
540 if (mServiceList.isEmpty()) onAllServicesGone();
541 }
542 }
543
544 private class FileMonitor extends FileObserver {
545 private Runnable mCallback;
546
547 FileMonitor(String path, Runnable callback) {
548 super(path, CLOSE_NOWRITE);
549 mCallback = callback;
550 }
551
552 @Override
553 public void onEvent(int event, String path) {
554 if ((event & CLOSE_NOWRITE) > 0) mCallback.run();
555 }
556 }
557
558 // Helper class for showing, updating notification.
559 private class NotificationHelper {
560 void update() {
561 String title = getNotificationTitle(true);
562 Notification n = new Notification(R.drawable.vpn_connected, title,
563 mStartTime);
564 n.setLatestEventInfo(mContext, title,
565 getNotificationMessage(true), prepareNotificationIntent());
566 n.flags |= Notification.FLAG_NO_CLEAR;
567 n.flags |= Notification.FLAG_ONGOING_EVENT;
568 enableNotification(n);
569 }
570
571 void showDisconnect() {
572 String title = getNotificationTitle(false);
573 Notification n = new Notification(R.drawable.vpn_disconnected,
574 title, System.currentTimeMillis());
575 n.setLatestEventInfo(mContext, title,
576 getNotificationMessage(false), prepareNotificationIntent());
577 n.flags |= Notification.FLAG_AUTO_CANCEL;
578 disableNotification();
579 enableNotification(n);
580 }
581
582 void disableNotification() {
583 ((NotificationManager) mContext.getSystemService(
584 Context.NOTIFICATION_SERVICE)).cancel(NOTIFICATION_ID);
585 }
586
587 private void enableNotification(Notification n) {
588 ((NotificationManager) mContext.getSystemService(
589 Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, n);
590 }
591
592 private PendingIntent prepareNotificationIntent() {
593 return PendingIntent.getActivity(mContext, 0,
594 new VpnManager(mContext).createSettingsActivityIntent(), 0);
595 }
596
597 private String getNotificationTitle(boolean connected) {
598 String connectedOrNot = connected
599 ? mContext.getString(R.string.vpn_notification_connected)
600 : mContext.getString(
601 R.string.vpn_notification_disconnected);
602 return String.format(
603 mContext.getString(R.string.vpn_notification_title),
604 mProfile.getName(), connectedOrNot);
605 }
606
607 private String getTimeFormat(long duration) {
608 long hours = duration / 3600;
609 StringBuilder sb = new StringBuilder();
610 if (hours > 0) sb.append(hours).append(':');
611 sb.append(String.format("%02d:%02d", (duration % 3600 / 60),
612 (duration % 60)));
613 return sb.toString();
614 }
615
616 private String getNotificationMessage(boolean connected) {
617 if (connected) {
618 long time = (System.currentTimeMillis() - mStartTime) / 1000;
619 return String.format(mContext.getString(
620 R.string.vpn_notification_connected_message),
621 getTimeFormat(time));
622 } else {
623 return "";
624 }
625 }
626 }
627}