diff --git a/src/frontends/android/app/build.gradle b/src/frontends/android/app/build.gradle index f5b13a8fd..198615857 100644 --- a/src/frontends/android/app/build.gradle +++ b/src/frontends/android/app/build.gradle @@ -2,14 +2,13 @@ apply plugin: 'com.android.application' android { compileSdkVersion 26 - buildToolsVersion '27.0.3' defaultConfig { applicationId "org.strongswan.android" minSdkVersion 15 targetSdkVersion 26 - versionCode 62 - versionName "2.0.2" + versionCode 63 + versionName "2.1.0" } sourceSets.main { diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java index 8e19d99e8..eb863867d 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2018 Tobias Brunner + * Copyright (C) 2012-2019 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager * HSR Hochschule fuer Technik Rapperswil @@ -39,7 +39,7 @@ public class VpnProfile implements Cloneable private String mName, mGateway, mUsername, mPassword, mCertificate, mUserCertificate; private String mRemoteId, mLocalId, mExcludedSubnets, mIncludedSubnets, mSelectedApps; - private String mIkeProposal, mEspProposal; + private String mIkeProposal, mEspProposal, mDnsServers; private Integer mMTU, mPort, mSplitTunneling, mNATKeepAlive, mFlags; private SelectedAppsHandling mSelectedAppsHandling = SelectedAppsHandling.SELECTED_APPS_DISABLE; private VpnType mVpnType; @@ -140,6 +140,16 @@ public class VpnProfile implements Cloneable this.mEspProposal = proposal; } + public String getDnsServers() + { + return mDnsServers; + } + + public void setDnsServers(String dns) + { + this.mDnsServers = dns; + } + public String getUsername() { return mUsername; diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java index afd0d4fb1..48c468ec1 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2018 Tobias Brunner + * Copyright (C) 2012-2019 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager * HSR Hochschule fuer Technik Rapperswil @@ -55,6 +55,7 @@ public class VpnProfileDataSource public static final String KEY_FLAGS = "flags"; public static final String KEY_IKE_PROPOSAL = "ike_proposal"; public static final String KEY_ESP_PROPOSAL = "esp_proposal"; + public static final String KEY_DNS_SERVERS = "dns_servers"; private DatabaseHelper mDbHelper; private SQLiteDatabase mDatabase; @@ -63,7 +64,7 @@ public class VpnProfileDataSource private static final String DATABASE_NAME = "strongswan.db"; private static final String TABLE_VPNPROFILE = "vpnprofile"; - private static final int DATABASE_VERSION = 16; + private static final int DATABASE_VERSION = 17; public static final DbColumn[] COLUMNS = new DbColumn[] { new DbColumn(KEY_ID, "INTEGER PRIMARY KEY AUTOINCREMENT", 1), @@ -88,6 +89,7 @@ public class VpnProfileDataSource new DbColumn(KEY_FLAGS, "INTEGER", 14), new DbColumn(KEY_IKE_PROPOSAL, "TEXT", 15), new DbColumn(KEY_ESP_PROPOSAL, "TEXT", 15), + new DbColumn(KEY_DNS_SERVERS, "TEXT", 17), }; private static final String[] ALL_COLUMNS = getColumns(DATABASE_VERSION); @@ -243,6 +245,11 @@ public class VpnProfileDataSource db.endTransaction(); } } + if (oldVersion < 17) + { + db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_DNS_SERVERS + + " TEXT;"); + } } private void updateColumns(SQLiteDatabase db, int version) @@ -448,6 +455,7 @@ public class VpnProfileDataSource profile.setFlags(getInt(cursor, cursor.getColumnIndex(KEY_FLAGS))); profile.setIkeProposal(cursor.getString(cursor.getColumnIndex(KEY_IKE_PROPOSAL))); profile.setEspProposal(cursor.getString(cursor.getColumnIndex(KEY_ESP_PROPOSAL))); + profile.setDnsServers(cursor.getString(cursor.getColumnIndex(KEY_DNS_SERVERS))); return profile; } @@ -475,6 +483,7 @@ public class VpnProfileDataSource values.put(KEY_FLAGS, profile.getFlags()); values.put(KEY_IKE_PROPOSAL, profile.getIkeProposal()); values.put(KEY_ESP_PROPOSAL, profile.getEspProposal()); + values.put(KEY_DNS_SERVERS, profile.getDnsServers()); return values; } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java index ac9ff495f..367248039 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java @@ -58,6 +58,7 @@ import org.strongswan.android.utils.Constants; import org.strongswan.android.utils.IPRange; import org.strongswan.android.utils.IPRangeSet; import org.strongswan.android.utils.SettingsWriter; +import org.strongswan.android.utils.Utils; import java.io.File; import java.io.FileInputStream; @@ -838,8 +839,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe { try { - mBuilder.addDnsServer(address); - mCache.recordAddressFamily(address); + mCache.addDnsServer(address); } catch (IllegalArgumentException ex) { @@ -1073,8 +1073,9 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe private final int mSplitTunneling; private final SelectedAppsHandling mAppHandling; private final SortedSet mSelectedApps; + private final List mDnsServers = new ArrayList<>(); private int mMtu; - private boolean mIPv4Seen, mIPv6Seen; + private boolean mIPv4Seen, mIPv6Seen, mDnsServersConfigured; public BuilderCache(VpnProfile profile) { @@ -1111,6 +1112,23 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe } mAppHandling = appHandling; + if (profile.getDnsServers() != null) + { + for (String server : profile.getDnsServers().split("\\s+")) + { + try + { + mDnsServers.add(Utils.parseInetAddress(server)); + recordAddressFamily(server); + mDnsServersConfigured = true; + } + catch (UnknownHostException e) + { + e.printStackTrace(); + } + } + } + /* set a default MTU, will be set by the daemon for regular interfaces */ Integer mtu = profile.getMTU(); mMtu = mtu == null ? Constants.MTU_MAX : mtu; @@ -1129,6 +1147,25 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe } } + public void addDnsServer(String address) + { + /* ignore received DNS servers if any were configured */ + if (mDnsServersConfigured) + { + return; + } + + try + { + mDnsServers.add(Utils.parseInetAddress(address)); + recordAddressFamily(address); + } + catch (UnknownHostException e) + { + e.printStackTrace(); + } + } + public void addRoute(String address, int prefixLength) { try @@ -1179,6 +1216,10 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe { builder.addAddress(address.getFrom(), address.getPrefix()); } + for (InetAddress server : mDnsServers) + { + builder.addDnsServer(server); + } /* 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) @@ -1301,7 +1342,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe private boolean isIPv6(String address) throws UnknownHostException { - InetAddress addr = InetAddress.getByName(address); + InetAddress addr = Utils.parseInetAddress(address); if (addr instanceof Inet4Address) { return false; diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java index 75618b564..e0965873c 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Tobias Brunner + * Copyright (C) 2012-2019 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager * HSR Hochschule fuer Technik Rapperswil @@ -72,6 +72,7 @@ import org.strongswan.android.utils.Constants; import org.strongswan.android.utils.IPRangeSet; import org.strongswan.android.utils.Utils; +import java.net.UnknownHostException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.SortedSet; @@ -138,6 +139,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity private EditText mEspProposal; private TextView mProfileIdLabel; private TextView mProfileId; + private MultiAutoCompleteTextView mDnsServers; + private TextInputLayoutHelper mDnsServersWrap; @Override public void onCreate(Bundle savedInstanceState) @@ -176,6 +179,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity mRemoteId = (MultiAutoCompleteTextView)findViewById(R.id.remote_id); mRemoteIdWrap = (TextInputLayoutHelper) findViewById(R.id.remote_id_wrap); + mDnsServers = (MultiAutoCompleteTextView)findViewById(R.id.dns_servers); + mDnsServersWrap = (TextInputLayoutHelper) findViewById(R.id.dns_servers_wrap); mMTU = (EditText)findViewById(R.id.mtu); mMTUWrap = (TextInputLayoutHelper) findViewById(R.id.mtu_wrap); mPort = (EditText)findViewById(R.id.port); @@ -573,7 +578,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity (flags != null && flags != 0) || (st != null && st != 0) || mProfile.getIncludedSubnets() != null || mProfile.getExcludedSubnets() != null || mProfile.getSelectedAppsHandling() != SelectedAppsHandling.SELECTED_APPS_DISABLE || - mProfile.getIkeProposal() != null || mProfile.getEspProposal() != null; + mProfile.getIkeProposal() != null || mProfile.getEspProposal() != null || + mProfile.getDnsServers() != null; } mShowAdvanced.setVisibility(!show ? View.VISIBLE : View.GONE); mAdvancedSettings.setVisibility(show ? View.VISIBLE : View.GONE); @@ -683,6 +689,11 @@ public class VpnProfileDetailActivity extends AppCompatActivity mEspProposalWrap.setError(getString(R.string.alert_text_no_proposal)); valid = false; } + if (!validateAddresses(mDnsServers)) + { + mDnsServersWrap.setError(getString(R.string.alert_text_no_ips)); + valid = false; + } return valid; } @@ -737,6 +748,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity mProfile.setIkeProposal(ike.isEmpty() ? null : ike); String esp = mEspProposal.getText().toString().trim(); mProfile.setEspProposal(esp.isEmpty() ? null : esp); + String dns = mDnsServers.getText().toString().trim(); + mProfile.setDnsServers(dns.isEmpty() ? null : dns); } /** @@ -772,6 +785,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity mSelectedApps = mProfile.getSelectedAppsSet(); mIkeProposal.setText(mProfile.getIkeProposal()); mEspProposal.setText(mProfile.getEspProposal()); + mDnsServers.setText(mProfile.getDnsServers()); mProfileId.setText(mProfile.getUUID().toString()); flags = mProfile.getFlags(); useralias = mProfile.getUserCertificateAlias(); @@ -884,6 +898,32 @@ public class VpnProfileDetailActivity extends AppCompatActivity return value.isEmpty() || IPRangeSet.fromString(value) != null; } + /** + * Check that the value in the given text box is a valid list of IP addresses + * + * @param view text box + */ + private boolean validateAddresses(EditText view) + { + String value = view.getText().toString().trim(); + if (value.isEmpty()) + { + return true; + } + for (String addr : value.split("\\s+")) + { + try + { + Utils.parseInetAddress(addr); + } + catch (UnknownHostException e) + { + return false; + } + } + return true; + } + /** * Check that the value in the given text box is a valid proposal * diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java index 1d8b677ee..221840be8 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2018 Tobias Brunner + * Copyright (C) 2016-2019 Tobias Brunner * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it @@ -74,6 +74,7 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Collections; import java.util.UUID; import javax.net.ssl.SSLHandshakeException; @@ -522,6 +523,7 @@ public class VpnProfileImportActivity extends AppCompatActivity profile.setIkeProposal(getProposal(obj, "ike-proposal", true)); profile.setEspProposal(getProposal(obj, "esp-proposal", false)); + profile.setDnsServers(getAddressList(obj, "dns-servers")); profile.setMTU(getInteger(obj, "mtu", Constants.MTU_MIN, Constants.MTU_MAX)); profile.setNATKeepAlive(getInteger(obj, "nat-keepalive", Constants.NAT_KEEPALIVE_MIN, Constants.NAT_KEEPALIVE_MAX)); JSONObject split = obj.optJSONObject("split-tunneling"); @@ -605,6 +607,44 @@ public class VpnProfileImportActivity extends AppCompatActivity return null; } + private String getAddressList(JSONObject obj, String key) throws JSONException + { + ArrayList addrs = new ArrayList<>(); + JSONArray arr = obj.optJSONArray(key); + if (arr != null) + { + for (int i = 0; i < arr.length(); i++) + { + String addr = arr.getString(i).replace(" ", ""); + addrs.add(addr); + } + } + else + { + String value = obj.optString(key, null); + if (!TextUtils.isEmpty(value)) + { + Collections.addAll(addrs, value.split("\\s+")); + } + } + if (addrs.size() > 0) + { + for (String addr : addrs) + { + try + { + Utils.parseInetAddress(addr); + } + catch (UnknownHostException e) + { + throw new JSONException(getString(R.string.profile_import_failed_value, key)); + } + } + return TextUtils.join(" ", addrs); + } + return null; + } + private String getApps(JSONArray arr) throws JSONException { ArrayList apps = new ArrayList<>(); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java index 4a3ed9f19..904b4341c 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2018 Tobias Brunner + * Copyright (C) 2012-2019 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager * HSR Hochschule fuer Technik Rapperswil @@ -49,6 +49,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.UUID; public class VpnProfileListFragment extends Fragment { @@ -230,11 +231,13 @@ public class VpnProfileListFragment extends Fragment private final MultiChoiceModeListener mVpnProfileSelected = new MultiChoiceModeListener() { private MenuItem mEditProfile; + private MenuItem mCopyProfile; @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { mEditProfile.setEnabled(mSelected.size() == 1); + mCopyProfile.setEnabled(mEditProfile.isEnabled()); return true; } @@ -250,6 +253,7 @@ public class VpnProfileListFragment extends Fragment MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.profile_list_context, menu); mEditProfile = menu.findItem(R.id.edit_profile); + mCopyProfile = menu.findItem(R.id.copy_profile); mode.setTitle(R.string.select_profiles); return true; } @@ -268,6 +272,24 @@ public class VpnProfileListFragment extends Fragment startActivityForResult(connectionIntent, EDIT_REQUEST); break; } + case R.id.copy_profile: + { + int position = mSelected.iterator().next(); + VpnProfile profile = (VpnProfile)mListView.getItemAtPosition(position); + profile = profile.clone(); + profile.setUUID(UUID.randomUUID()); + profile.setName(String.format(getString(R.string.copied_name), profile.getName())); + mDataSource.insertProfile(profile); + + Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED); + intent.putExtra(Constants.VPN_PROFILES_SINGLE, profile.getId()); + LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent); + + Intent connectionIntent = new Intent(getActivity(), VpnProfileDetailActivity.class); + connectionIntent.putExtra(VpnProfileDataSource.KEY_ID, profile.getId()); + startActivityForResult(connectionIntent, EDIT_REQUEST); + break; + } case R.id.delete_profile: { ArrayList profiles = new ArrayList<>(); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/IPRange.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/IPRange.java index 6c0aa659b..e8397707a 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/IPRange.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/IPRange.java @@ -76,7 +76,7 @@ public class IPRange implements Comparable public IPRange(String from, String to) throws UnknownHostException { - this(InetAddress.getByName(from), InetAddress.getByName(to)); + this(Utils.parseInetAddress(from), Utils.parseInetAddress(to)); } public IPRange(InetAddress from, InetAddress to) @@ -106,7 +106,7 @@ public class IPRange implements Comparable public IPRange(String base, int prefix) throws UnknownHostException { - this(InetAddress.getByName(base), prefix); + this(Utils.parseInetAddress(base), prefix); } public IPRange(InetAddress base, int prefix) diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Utils.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Utils.java index f2e8e0058..9dec3c0e3 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Utils.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Utils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2017 Tobias Brunner + * Copyright (C) 2014-2019 Tobias Brunner * HSR Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it @@ -16,6 +16,9 @@ package org.strongswan.android.utils; +import java.net.InetAddress; +import java.net.UnknownHostException; + public class Utils { static final char[] HEXDIGITS = "0123456789abcdef".toCharArray(); @@ -46,4 +49,29 @@ public class Utils * @return true if valid */ public native static boolean isProposalValid(boolean ike, String proposal); + + /** + * Parse an IP address without doing a name lookup + * + * @param address IP address string + * @return address bytes if valid + */ + private native static byte[] parseInetAddressBytes(String address); + + /** + * Parse an IP address without doing a name lookup (as compared to InetAddress.fromName()) + * + * @param address IP address string + * @return address if valid + * @throws UnknownHostException if address is invalid + */ + public static InetAddress parseInetAddress(String address) throws UnknownHostException + { + byte[] bytes = parseInetAddressBytes(address); + if (bytes == null) + { + throw new UnknownHostException(); + } + return InetAddress.getByAddress(bytes); + } } diff --git a/src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c b/src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c index aa84a2b9c..78abede58 100644 --- a/src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c +++ b/src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2018 Tobias Brunner + * Copyright (C) 2012-2019 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager * HSR Hochschule fuer Technik Rapperswil @@ -758,3 +758,34 @@ JNI_METHOD_P(org_strongswan_android_utils, Utils, isProposalValid, jboolean, library_deinit(); return valid; } + +/** + * Utility function to parse an IP address from a string (static, so `this` is the class) + */ +JNI_METHOD_P(org_strongswan_android_utils, Utils, parseInetAddressBytes, jbyteArray, + jstring address) +{ + jbyteArray bytes; + host_t *host; + char *str; + + dbg = dbg_android; + + if (!library_init(NULL, "charon")) + { + library_deinit(); + return NULL; + } + str = androidjni_convert_jstring(env, address); + host = host_create_from_string(str, 0); + if (!host) + { + free(str); + return NULL; + } + bytes = byte_array_from_chunk(env, host->get_address(host)); + host->destroy(host); + free(str); + library_deinit(); + return bytes; +} diff --git a/src/frontends/android/app/src/main/res/layout/profile_detail_view.xml b/src/frontends/android/app/src/main/res/layout/profile_detail_view.xml index 3cf0b552d..671c58d67 100644 --- a/src/frontends/android/app/src/main/res/layout/profile_detail_view.xml +++ b/src/frontends/android/app/src/main/res/layout/profile_detail_view.xml @@ -1,6 +1,6 @@