VpnSettings.java revision c6e84c09590ec5e4da287fba32dd53775156ae76
1/* 2 * Copyright (C) 2011 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.settings.vpn2; 18 19import com.android.settings.R; 20 21import android.content.Context; 22import android.content.DialogInterface; 23import android.net.IConnectivityManager; 24import android.net.LinkProperties; 25import android.net.RouteInfo; 26import android.os.Bundle; 27import android.os.Handler; 28import android.os.Message; 29import android.os.ServiceManager; 30import android.preference.Preference; 31import android.preference.PreferenceGroup; 32import android.security.Credentials; 33import android.security.KeyStore; 34import android.util.Log; 35import android.view.ContextMenu; 36import android.view.ContextMenu.ContextMenuInfo; 37import android.view.Menu; 38import android.view.MenuItem; 39import android.view.View; 40import android.widget.AdapterView.AdapterContextMenuInfo; 41import android.widget.Toast; 42 43import com.android.internal.net.LegacyVpnInfo; 44import com.android.internal.net.VpnConfig; 45import com.android.internal.net.VpnProfile; 46import com.android.settings.SettingsPreferenceFragment; 47 48import java.net.Inet4Address; 49import java.nio.charset.Charsets; 50import java.util.Arrays; 51import java.util.HashMap; 52 53public class VpnSettings extends SettingsPreferenceFragment implements 54 Handler.Callback, Preference.OnPreferenceClickListener, 55 DialogInterface.OnClickListener, DialogInterface.OnDismissListener { 56 57 private static final String TAG = "VpnSettings"; 58 59 private final IConnectivityManager mService = IConnectivityManager.Stub 60 .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); 61 private final KeyStore mKeyStore = KeyStore.getInstance(); 62 private boolean mUnlocking = false; 63 64 private HashMap<String, VpnPreference> mPreferences; 65 private VpnDialog mDialog; 66 67 private Handler mUpdater; 68 private LegacyVpnInfo mInfo; 69 70 // The key of the profile for the current ContextMenu. 71 private String mSelectedKey; 72 73 @Override 74 public void onCreate(Bundle savedState) { 75 super.onCreate(savedState); 76 addPreferencesFromResource(R.xml.vpn_settings2); 77 getPreferenceScreen().setOrderingAsAdded(false); 78 79 if (savedState != null) { 80 VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"), 81 savedState.getByteArray("VpnProfile")); 82 if (profile != null) { 83 mDialog = new VpnDialog(getActivity(), this, profile, 84 savedState.getBoolean("VpnEditing")); 85 } 86 } 87 } 88 89 @Override 90 public void onSaveInstanceState(Bundle savedState) { 91 // We do not save view hierarchy, as they are just profiles. 92 if (mDialog != null) { 93 VpnProfile profile = mDialog.getProfile(); 94 savedState.putString("VpnKey", profile.key); 95 savedState.putByteArray("VpnProfile", profile.encode()); 96 savedState.putBoolean("VpnEditing", mDialog.isEditing()); 97 } 98 // else? 99 } 100 101 @Override 102 public void onResume() { 103 super.onResume(); 104 105 // Check KeyStore here, so others do not need to deal with it. 106 if (mKeyStore.state() != KeyStore.State.UNLOCKED) { 107 if (!mUnlocking) { 108 // Let us unlock KeyStore. See you later! 109 Credentials.getInstance().unlock(getActivity()); 110 } else { 111 // We already tried, but it is still not working! 112 finishFragment(); 113 } 114 mUnlocking = !mUnlocking; 115 return; 116 } 117 118 // Now KeyStore is always unlocked. Reset the flag. 119 mUnlocking = false; 120 121 // Currently we are the only user of profiles in KeyStore. 122 // Assuming KeyStore and KeyGuard do the right thing, we can 123 // safely cache profiles in the memory. 124 if (mPreferences == null) { 125 mPreferences = new HashMap<String, VpnPreference>(); 126 PreferenceGroup group = getPreferenceScreen(); 127 128 String[] keys = mKeyStore.saw(Credentials.VPN); 129 if (keys != null && keys.length > 0) { 130 Context context = getActivity(); 131 132 for (String key : keys) { 133 VpnProfile profile = VpnProfile.decode(key, 134 mKeyStore.get(Credentials.VPN + key)); 135 if (profile == null) { 136 Log.w(TAG, "bad profile: key = " + key); 137 mKeyStore.delete(Credentials.VPN + key); 138 } else { 139 VpnPreference preference = new VpnPreference(context, profile); 140 mPreferences.put(key, preference); 141 group.addPreference(preference); 142 } 143 } 144 } 145 group.findPreference("add_network").setOnPreferenceClickListener(this); 146 } 147 148 // Show the dialog if there is one. 149 if (mDialog != null) { 150 mDialog.setOnDismissListener(this); 151 mDialog.show(); 152 } 153 154 // Start monitoring. 155 if (mUpdater == null) { 156 mUpdater = new Handler(this); 157 } 158 mUpdater.sendEmptyMessage(0); 159 160 // Register for context menu. Hmmm, getListView() is hidden? 161 registerForContextMenu(getListView()); 162 } 163 164 @Override 165 public void onPause() { 166 super.onPause(); 167 168 // Hide the dialog if there is one. 169 if (mDialog != null) { 170 mDialog.setOnDismissListener(null); 171 mDialog.dismiss(); 172 } 173 174 // Unregister for context menu. 175 if (getView() != null) { 176 unregisterForContextMenu(getListView()); 177 } 178 } 179 180 @Override 181 public void onDismiss(DialogInterface dialog) { 182 // Here is the exit of a dialog. 183 mDialog = null; 184 } 185 186 @Override 187 public void onClick(DialogInterface dialog, int button) { 188 if (button == DialogInterface.BUTTON_POSITIVE) { 189 // Always save the profile. 190 VpnProfile profile = mDialog.getProfile(); 191 mKeyStore.put(Credentials.VPN + profile.key, profile.encode()); 192 193 // Update the preference. 194 VpnPreference preference = mPreferences.get(profile.key); 195 if (preference != null) { 196 disconnect(profile.key); 197 preference.update(profile); 198 } else { 199 preference = new VpnPreference(getActivity(), profile); 200 mPreferences.put(profile.key, preference); 201 getPreferenceScreen().addPreference(preference); 202 } 203 204 // If we are not editing, connect! 205 if (!mDialog.isEditing()) { 206 try { 207 connect(profile); 208 } catch (Exception e) { 209 Log.e(TAG, "connect", e); 210 } 211 } 212 } 213 } 214 215 @Override 216 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) { 217 if (mDialog != null) { 218 Log.v(TAG, "onCreateContextMenu() is called when mDialog != null"); 219 return; 220 } 221 222 if (info instanceof AdapterContextMenuInfo) { 223 Preference preference = (Preference) getListView().getItemAtPosition( 224 ((AdapterContextMenuInfo) info).position); 225 if (preference instanceof VpnPreference) { 226 VpnProfile profile = ((VpnPreference) preference).getProfile(); 227 mSelectedKey = profile.key; 228 menu.setHeaderTitle(profile.name); 229 menu.add(Menu.NONE, R.string.vpn_menu_edit, 0, R.string.vpn_menu_edit); 230 menu.add(Menu.NONE, R.string.vpn_menu_delete, 0, R.string.vpn_menu_delete); 231 } 232 } 233 } 234 235 @Override 236 public boolean onContextItemSelected(MenuItem item) { 237 if (mDialog != null) { 238 Log.v(TAG, "onContextItemSelected() is called when mDialog != null"); 239 return false; 240 } 241 242 VpnPreference preference = mPreferences.get(mSelectedKey); 243 if (preference == null) { 244 Log.v(TAG, "onContextItemSelected() is called but no preference is found"); 245 return false; 246 } 247 248 switch (item.getItemId()) { 249 case R.string.vpn_menu_edit: 250 mDialog = new VpnDialog(getActivity(), this, preference.getProfile(), true); 251 mDialog.setOnDismissListener(this); 252 mDialog.show(); 253 return true; 254 case R.string.vpn_menu_delete: 255 disconnect(mSelectedKey); 256 getPreferenceScreen().removePreference(preference); 257 mPreferences.remove(mSelectedKey); 258 mKeyStore.delete(Credentials.VPN + mSelectedKey); 259 return true; 260 } 261 return false; 262 } 263 264 @Override 265 public boolean onPreferenceClick(Preference preference) { 266 if (mDialog != null) { 267 Log.v(TAG, "onPreferenceClick() is called when mDialog != null"); 268 return true; 269 } 270 271 if (preference instanceof VpnPreference) { 272 VpnProfile profile = ((VpnPreference) preference).getProfile(); 273 if (mInfo != null && profile.key.equals(mInfo.key) && 274 mInfo.state == LegacyVpnInfo.STATE_CONNECTED) { 275 try { 276 mInfo.intent.send(); 277 return true; 278 } catch (Exception e) { 279 // ignore 280 } 281 } 282 mDialog = new VpnDialog(getActivity(), this, profile, false); 283 } else { 284 // Generate a new key. Here we just use the current time. 285 long millis = System.currentTimeMillis(); 286 while (mPreferences.containsKey(Long.toHexString(millis))) { 287 ++millis; 288 } 289 mDialog = new VpnDialog(getActivity(), this, 290 new VpnProfile(Long.toHexString(millis)), true); 291 } 292 mDialog.setOnDismissListener(this); 293 mDialog.show(); 294 return true; 295 } 296 297 @Override 298 public boolean handleMessage(Message message) { 299 mUpdater.removeMessages(0); 300 301 if (isResumed()) { 302 try { 303 LegacyVpnInfo info = mService.getLegacyVpnInfo(); 304 if (mInfo != null) { 305 VpnPreference preference = mPreferences.get(mInfo.key); 306 if (preference != null) { 307 preference.update(-1); 308 } 309 mInfo = null; 310 } 311 if (info != null) { 312 VpnPreference preference = mPreferences.get(info.key); 313 if (preference != null) { 314 preference.update(info.state); 315 mInfo = info; 316 } 317 } 318 } catch (Exception e) { 319 // ignore 320 } 321 mUpdater.sendEmptyMessageDelayed(0, 1000); 322 } 323 return true; 324 } 325 326 private String[] getDefaultNetwork() throws Exception { 327 LinkProperties network = mService.getActiveLinkProperties(); 328 if (network == null) { 329 Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show(); 330 throw new IllegalStateException("Network is not available"); 331 } 332 String interfaze = network.getInterfaceName(); 333 if (interfaze == null) { 334 Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show(); 335 throw new IllegalStateException("Cannot get the default interface"); 336 } 337 String gateway = null; 338 for (RouteInfo route : network.getRoutes()) { 339 // Currently legacy VPN only works on IPv4. 340 if (route.isDefaultRoute() && route.getGateway() instanceof Inet4Address) { 341 gateway = route.getGateway().getHostAddress(); 342 break; 343 } 344 } 345 if (gateway == null) { 346 Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show(); 347 throw new IllegalStateException("Cannot get the default gateway"); 348 } 349 return new String[] {interfaze, gateway}; 350 } 351 352 private void connect(VpnProfile profile) throws Exception { 353 // Get the default interface and the default gateway. 354 String[] network = getDefaultNetwork(); 355 String interfaze = network[0]; 356 String gateway = network[1]; 357 358 // Load certificates. 359 String privateKey = ""; 360 String userCert = ""; 361 String caCert = ""; 362 String serverCert = ""; 363 if (!profile.ipsecUserCert.isEmpty()) { 364 /* 365 * VPN has a special exception in keystore to allow it to use system 366 * UID certs. 367 */ 368 privateKey = Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert; 369 byte[] value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecUserCert); 370 userCert = (value == null) ? null : new String(value, Charsets.UTF_8); 371 } 372 if (!profile.ipsecCaCert.isEmpty()) { 373 byte[] value = mKeyStore.get(Credentials.CA_CERTIFICATE + profile.ipsecCaCert); 374 caCert = (value == null) ? null : new String(value, Charsets.UTF_8); 375 } 376 if (!profile.ipsecServerCert.isEmpty()) { 377 byte[] value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecServerCert); 378 serverCert = (value == null) ? null : new String(value, Charsets.UTF_8); 379 } 380 if (privateKey == null || userCert == null || caCert == null || serverCert == null) { 381 Toast.makeText(getActivity(), R.string.vpn_missing_cert, Toast.LENGTH_LONG).show(); 382 throw new IllegalStateException("Cannot load credentials"); 383 } 384 385 // Prepare arguments for racoon. 386 String[] racoon = null; 387 switch (profile.type) { 388 case VpnProfile.TYPE_L2TP_IPSEC_PSK: 389 racoon = new String[] { 390 interfaze, profile.server, "udppsk", profile.ipsecIdentifier, 391 profile.ipsecSecret, "1701", 392 }; 393 break; 394 case VpnProfile.TYPE_L2TP_IPSEC_RSA: 395 racoon = new String[] { 396 interfaze, profile.server, "udprsa", privateKey, userCert, 397 caCert, serverCert, "1701", 398 }; 399 break; 400 case VpnProfile.TYPE_IPSEC_XAUTH_PSK: 401 racoon = new String[] { 402 interfaze, profile.server, "xauthpsk", profile.ipsecIdentifier, 403 profile.ipsecSecret, profile.username, profile.password, "", gateway, 404 }; 405 break; 406 case VpnProfile.TYPE_IPSEC_XAUTH_RSA: 407 racoon = new String[] { 408 interfaze, profile.server, "xauthrsa", privateKey, userCert, 409 caCert, serverCert, profile.username, profile.password, "", gateway, 410 }; 411 break; 412 case VpnProfile.TYPE_IPSEC_HYBRID_RSA: 413 racoon = new String[] { 414 interfaze, profile.server, "hybridrsa", 415 caCert, serverCert, profile.username, profile.password, "", gateway, 416 }; 417 break; 418 } 419 420 // Prepare arguments for mtpd. 421 String[] mtpd = null; 422 switch (profile.type) { 423 case VpnProfile.TYPE_PPTP: 424 mtpd = new String[] { 425 interfaze, "pptp", profile.server, "1723", 426 "name", profile.username, "password", profile.password, 427 "linkname", "vpn", "refuse-eap", "nodefaultroute", 428 "usepeerdns", "idle", "1800", "mtu", "1400", "mru", "1400", 429 (profile.mppe ? "+mppe" : "nomppe"), 430 }; 431 break; 432 case VpnProfile.TYPE_L2TP_IPSEC_PSK: 433 case VpnProfile.TYPE_L2TP_IPSEC_RSA: 434 mtpd = new String[] { 435 interfaze, "l2tp", profile.server, "1701", profile.l2tpSecret, 436 "name", profile.username, "password", profile.password, 437 "linkname", "vpn", "refuse-eap", "nodefaultroute", 438 "usepeerdns", "idle", "1800", "mtu", "1400", "mru", "1400", 439 }; 440 break; 441 } 442 443 VpnConfig config = new VpnConfig(); 444 config.user = profile.key; 445 config.interfaze = interfaze; 446 config.session = profile.name; 447 config.routes = profile.routes; 448 if (!profile.dnsServers.isEmpty()) { 449 config.dnsServers = Arrays.asList(profile.dnsServers.split(" +")); 450 } 451 if (!profile.searchDomains.isEmpty()) { 452 config.searchDomains = Arrays.asList(profile.searchDomains.split(" +")); 453 } 454 455 mService.startLegacyVpn(config, racoon, mtpd); 456 } 457 458 private void disconnect(String key) { 459 if (mInfo != null && key.equals(mInfo.key)) { 460 try { 461 mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN); 462 } catch (Exception e) { 463 // ignore 464 } 465 } 466 } 467 468 @Override 469 protected int getHelpResource() { 470 return R.string.help_url_vpn; 471 } 472 473 private class VpnPreference extends Preference { 474 private VpnProfile mProfile; 475 private int mState = -1; 476 477 VpnPreference(Context context, VpnProfile profile) { 478 super(context); 479 setPersistent(false); 480 setOrder(0); 481 setOnPreferenceClickListener(VpnSettings.this); 482 483 mProfile = profile; 484 update(); 485 } 486 487 VpnProfile getProfile() { 488 return mProfile; 489 } 490 491 void update(VpnProfile profile) { 492 mProfile = profile; 493 update(); 494 } 495 496 void update(int state) { 497 mState = state; 498 update(); 499 } 500 501 void update() { 502 if (mState < 0) { 503 String[] types = getContext().getResources() 504 .getStringArray(R.array.vpn_types_long); 505 setSummary(types[mProfile.type]); 506 } else { 507 String[] states = getContext().getResources() 508 .getStringArray(R.array.vpn_states); 509 setSummary(states[mState]); 510 } 511 setTitle(mProfile.name); 512 notifyHierarchyChanged(); 513 } 514 515 @Override 516 public int compareTo(Preference preference) { 517 int result = -1; 518 if (preference instanceof VpnPreference) { 519 VpnPreference another = (VpnPreference) preference; 520 if ((result = another.mState - mState) == 0 && 521 (result = mProfile.name.compareTo(another.mProfile.name)) == 0 && 522 (result = mProfile.type - another.mProfile.type) == 0) { 523 result = mProfile.key.compareTo(another.mProfile.key); 524 } 525 } 526 return result; 527 } 528 } 529} 530