From 772dfdef8c01388966a2a6c8fd80c045cfc98ece Mon Sep 17 00:00:00 2001 From: Revertron <105154+Revertron@users.noreply.github.com> Date: Sun, 30 Oct 2022 22:14:30 +0100 Subject: [PATCH] Added DNS configuration functionality. (#24) * Changed app icon from default to Yggdrasil leaf. * Added workaround for DNS-reslver and fix for unmetered networks. * Added DNS configuration functionality. * Changed DNS configuration UI. Disabled DNS config by default. Added DNS fix for Chrome-based browsers. --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 6 +- .../eu/neilalexander/yggdrasil/DnsActivity.kt | 166 ++++++++++++ .../neilalexander/yggdrasil/MainActivity.kt | 24 +- .../yggdrasil/PacketTunnelProvider.kt | 31 ++- .../neilalexander/yggdrasil/PeersActivity.kt | 10 +- app/src/main/res/layout/activity_dns.xml | 245 ++++++++++++++++++ app/src/main/res/layout/activity_main.xml | 50 +++- app/src/main/res/layout/activity_peers.xml | 4 +- .../main/res/layout/dialog_add_dns_server.xml | 34 +++ app/src/main/res/layout/dialog_addpeer.xml | 1 + app/src/main/res/layout/dns_server_usable.xml | 47 ++++ app/src/main/res/layout/peers_configured.xml | 3 +- app/src/main/res/values/strings.xml | 17 ++ build.gradle | 5 +- readme.md | 2 +- 16 files changed, 620 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt create mode 100644 app/src/main/res/layout/activity_dns.xml create mode 100644 app/src/main/res/layout/dialog_add_dns_server.xml create mode 100644 app/src/main/res/layout/dns_server_usable.xml diff --git a/app/build.gradle b/app/build.gradle index 213ae2b..52a4d45 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,6 +55,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.preference:preference-ktx:1.1.0' testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 776bbad..9a1557c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,17 +16,19 @@ android:parentActivityName=".MainActivity" /> - + + + android:permission="android.permission.BIND_VPN_SERVICE" + android:exported="true"> diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt new file mode 100644 index 0000000..fc1c3ef --- /dev/null +++ b/app/src/main/java/eu/neilalexander/yggdrasil/DnsActivity.kt @@ -0,0 +1,166 @@ +package eu.neilalexander.yggdrasil + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.content.SharedPreferences +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.view.ContextThemeWrapper +import android.view.LayoutInflater +import android.view.View +import android.widget.* +import com.google.android.material.textfield.TextInputEditText + +const val KEY_DNS_SERVERS = "dns_servers" +const val KEY_ENABLE_CHROME_FIX = "enable_chrome_fix" +const val DEFAULT_DNS_SERVERS = "302:7991::53,302:db60::53,300:6223::53,301:1088::53" + +class DnsActivity : AppCompatActivity() { + private lateinit var config: ConfigurationProxy + private lateinit var inflater: LayoutInflater + + private lateinit var serversTableLayout: TableLayout + private lateinit var serversTableLabel: TextView + private lateinit var serversTableHint: TextView + private lateinit var addServerButton: ImageButton + private lateinit var enableChromeFix: Switch + + private lateinit var servers: MutableList + private lateinit var preferences: SharedPreferences + + @SuppressLint("ApplySharedPref") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_dns) + + config = ConfigurationProxy(applicationContext) + inflater = LayoutInflater.from(this) + + serversTableLayout = findViewById(R.id.configuredDnsTableLayout) + serversTableLabel = findViewById(R.id.configuredDnsLabel) + serversTableHint = findViewById(R.id.configuredDnsHint) + enableChromeFix = findViewById(R.id.enableChromeFix) + + addServerButton = findViewById(R.id.addServerButton) + addServerButton.setOnClickListener { + val view = inflater.inflate(R.layout.dialog_add_dns_server, null) + val input = view.findViewById(R.id.addDnsInput) + val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.Theme_MaterialComponents_DayNight_Dialog)) + builder.setTitle(getString(R.string.dns_add_server_dialog_title)) + builder.setView(view) + builder.setPositiveButton(getString(R.string.add)) { dialog, _ -> + servers.add(input.text.toString()) + preferences.edit().apply { + putString(KEY_DNS_SERVERS, servers.joinToString(",")) + commit() + } + dialog.dismiss() + updateConfiguredServers() + } + builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> + dialog.cancel() + } + builder.show() + } + + enableChromeFix.setOnCheckedChangeListener { _, isChecked -> + preferences.edit().apply { + putBoolean(KEY_ENABLE_CHROME_FIX, isChecked) + commit() + } + } + + preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext) + val serverString = preferences.getString(KEY_DNS_SERVERS, "") + servers = if (serverString!!.isNotEmpty()) { + serverString.split(",").toMutableList() + } else { + mutableListOf() + } + updateUsableServers() + } + + override fun onResume() { + super.onResume() + updateConfiguredServers() + enableChromeFix.isChecked = preferences.getBoolean(KEY_ENABLE_CHROME_FIX, false) + } + + @SuppressLint("ApplySharedPref") + private fun updateConfiguredServers() { + when (servers.size) { + 0 -> { + serversTableLayout.visibility = View.GONE + serversTableLabel.text = getString(R.string.dns_no_configured_servers) + serversTableHint.text = getText(R.string.dns_configured_servers_hint_empty) + } + else -> { + serversTableLayout.visibility = View.VISIBLE + serversTableLabel.text = getString(R.string.dns_configured_servers) + serversTableHint.text = getText(R.string.dns_configured_servers_hint) + + serversTableLayout.removeAllViewsInLayout() + for (i in servers.indices) { + val server = servers[i] + val view = inflater.inflate(R.layout.peers_configured, null) + view.findViewById(R.id.addressValue).text = server + view.findViewById(R.id.deletePeerButton).tag = i + + view.findViewById(R.id.deletePeerButton).setOnClickListener { button -> + val builder: AlertDialog.Builder = AlertDialog.Builder(this) + builder.setTitle("Remove ${server}?") + builder.setPositiveButton(getString(R.string.remove)) { dialog, _ -> + servers.removeAt(button.tag as Int) + preferences.edit().apply { + this.putString(KEY_DNS_SERVERS, servers.joinToString(",")) + this.commit() + } + dialog.dismiss() + updateConfiguredServers() + } + builder.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> + dialog.cancel() + } + builder.show() + } + serversTableLayout.addView(view) + } + } + } + } + + @SuppressLint("ApplySharedPref") + private fun updateUsableServers() { + val usableTableLayout: TableLayout = findViewById(R.id.usableDnsTableLayout) + val defaultServers = DEFAULT_DNS_SERVERS.split(",") + + defaultServers.forEach { + val server = it + val view = inflater.inflate(R.layout.dns_server_usable, null) + view.findViewById(R.id.serverValue).text = server + val addButton = view.findViewById(R.id.addButton) + addButton.tag = server + + addButton.setOnClickListener { button -> + servers.add(button.tag as String) + preferences.edit().apply { + this.putString(KEY_DNS_SERVERS, servers.joinToString(",")) + this.commit() + } + updateConfiguredServers() + } + view.setOnLongClickListener { + val builder: AlertDialog.Builder = AlertDialog.Builder(this) + builder.setTitle(getString(R.string.dns_server_info_dialog_title)) + builder.setMessage(getText(R.string.dns_server_info_revertron)) + builder.setPositiveButton(getString(R.string.ok)) { dialog, _ -> + dialog.dismiss() + } + builder.show() + true + } + + usableTableLayout.addView(view) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt index 769b803..ea56090 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt @@ -27,6 +27,8 @@ class MainActivity : AppCompatActivity() { private lateinit var coordinatesLabel: TextView private lateinit var peersLabel: TextView private lateinit var peersRow: TableRow + private lateinit var dnsLabel: TextView + private lateinit var dnsRow: TableRow private lateinit var settingsRow: TableRow private fun start() { @@ -47,13 +49,15 @@ class MainActivity : AppCompatActivity() { findViewById(R.id.versionValue).text = Mobile.getVersion() - enabledSwitch = findViewById(R.id.enableMulticastBeacon) + enabledSwitch = findViewById(R.id.enableYggdrasil) enabledLabel = findViewById(R.id.yggdrasilStatusLabel) ipAddressLabel = findViewById(R.id.ipAddressValue) subnetLabel = findViewById(R.id.subnetValue) coordinatesLabel = findViewById(R.id.coordinatesValue) peersLabel = findViewById(R.id.peersValue) peersRow = findViewById(R.id.peersTableRow) + dnsLabel = findViewById(R.id.dnsValue) + dnsRow = findViewById(R.id.dnsTableRow) settingsRow = findViewById(R.id.settingsTableRow) enabledLabel.setTextColor(Color.GRAY) @@ -82,6 +86,12 @@ class MainActivity : AppCompatActivity() { startActivity(intent) } + dnsRow.isClickable = true + dnsRow.setOnClickListener { + val intent = Intent(this, DnsActivity::class.java) + startActivity(intent) + } + settingsRow.isClickable = true settingsRow.setOnClickListener { val intent = Intent(this, SettingsActivity::class.java) @@ -94,6 +104,18 @@ class MainActivity : AppCompatActivity() { LocalBroadcastManager.getInstance(this).registerReceiver( receiver, IntentFilter(PacketTunnelState.RECEIVER_INTENT) ) + val preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext) + val serverString = preferences.getString(KEY_DNS_SERVERS, "") + if (serverString!!.isNotEmpty()) { + val servers = serverString.split(",") + dnsLabel.text = when (servers.size) { + 0 -> "No servers" + 1 -> "1 server" + else -> "${servers.size} servers" + } + } else { + dnsLabel.text = "No servers" + } } private val receiver: BroadcastReceiver = object : BroadcastReceiver() { diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt b/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt index d92ce8f..dd8afc3 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt @@ -17,6 +17,8 @@ import java.util.concurrent.atomic.AtomicBoolean import kotlin.concurrent.thread +private const val TAG = "PacketTunnelProvider" + class PacketTunnelProvider: VpnService() { companion object { const val RECEIVER_INTENT = "eu.neilalexander.yggdrasil.PacketTunnelProvider.MESSAGE" @@ -50,16 +52,16 @@ class PacketTunnelProvider: VpnService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent == null) { - Log.d("PacketTunnelProvider", "Intent is null") + Log.d(TAG, "Intent is null") return START_NOT_STICKY } return when (intent.action ?: ACTION_STOP) { ACTION_STOP -> { - Log.d("PacketTunnelProvider", "Stopping...") + Log.d(TAG, "Stopping...") stop(); START_NOT_STICKY } else -> { - Log.d("PacketTunnelProvider", "Starting...") + Log.d(TAG, "Starting...") start(); START_STICKY } } @@ -70,11 +72,11 @@ class PacketTunnelProvider: VpnService() { return } - Log.d("PacketTunnelProvider", config.getJSON().toString()) + Log.d(TAG, config.getJSON().toString()) yggdrasil.startJSON(config.getJSONByteArray()) val address = yggdrasil.addressString - var builder = Builder() + val builder = Builder() .addAddress(address, 7) .addRoute("200::", 7) // We do this to trick the DNS-resolver into thinking that we have "regular" IPv6, @@ -97,6 +99,21 @@ class PacketTunnelProvider: VpnService() { builder.setMetered(false) } + val preferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this.baseContext) + val serverString = preferences.getString(KEY_DNS_SERVERS, "") + if (serverString!!.isNotEmpty()) { + val servers = serverString.split(",") + if (servers.isNotEmpty()) { + servers.forEach { + Log.i(TAG, "Using DNS server $it") + builder.addDnsServer(it) + } + } + } + if (preferences.getBoolean(KEY_ENABLE_CHROME_FIX, false)) { + builder.addRoute("2001:4860:4860::8888", 128) + } + parcel = builder.establish() val parcel = parcel if (parcel == null || !parcel.fileDescriptor.valid()) { @@ -123,7 +140,7 @@ class PacketTunnelProvider: VpnService() { intent.putExtra("ip", yggdrasil.addressString) intent.putExtra("subnet", yggdrasil.subnetString) intent.putExtra("coords", yggdrasil.coordsString) - intent.putExtra("peers", JSONArray(yggdrasil.peersJSON).length()) + intent.putExtra("peers", yggdrasil.peersJSON) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) } @@ -214,7 +231,7 @@ class PacketTunnelProvider: VpnService() { } private fun reader() { - var b = ByteArray(65535) + val b = ByteArray(65535) reads@ while (started.get()) { val readerStream = readerStream val readerThread = readerThread diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt index af76c8b..b9ec651 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt @@ -49,14 +49,14 @@ class PeersActivity : AppCompatActivity() { addPeerButton = findViewById(R.id.addPeerButton) addPeerButton.setOnClickListener { - var view = inflater.inflate(R.layout.dialog_addpeer, null) - var input = view.findViewById(R.id.addPeerInput) + val view = inflater.inflate(R.layout.dialog_addpeer, null) + val input = view.findViewById(R.id.addPeerInput) val builder: AlertDialog.Builder = AlertDialog.Builder(ContextThemeWrapper(this, R.style.Theme_MaterialComponents_DayNight_Dialog)) builder.setTitle("Add Configured Peer") builder.setView(view) builder.setPositiveButton("Add") { dialog, _ -> config.updateJSON { json -> - json.getJSONArray("Peers").put(input.text) + json.getJSONArray("Peers").put(input.text.toString().trim()) } dialog.dismiss() updateConfiguredPeers() @@ -90,7 +90,7 @@ class PeersActivity : AppCompatActivity() { configuredTableLayout.removeAllViewsInLayout() for (i in 0 until peers.length()) { val peer = peers[i].toString() - var view = inflater.inflate(R.layout.peers_configured, null) + val view = inflater.inflate(R.layout.peers_configured, null) view.findViewById(R.id.addressValue).text = peer view.findViewById(R.id.deletePeerButton).tag = i @@ -130,7 +130,7 @@ class PeersActivity : AppCompatActivity() { connectedTableLayout.removeAllViewsInLayout() for (i in 0 until peers.length()) { val peer = peers.getJSONObject(i) - var view = inflater.inflate(R.layout.peers_connected, null) + val view = inflater.inflate(R.layout.peers_connected, null) val ip = peer.getString("IP") view.findViewById(R.id.addressLabel).text = ip view.findViewById(R.id.detailsLabel).text = peer.getString("Remote") diff --git a/app/src/main/res/layout/activity_dns.xml b/app/src/main/res/layout/activity_dns.xml new file mode 100644 index 0000000..132f2a0 --- /dev/null +++ b/app/src/main/res/layout/activity_dns.xml @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9b2fcd8..2064b49 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -94,7 +94,7 @@ android:layout_weight="2" /> @@ -347,6 +347,50 @@ app:srcCompat="@drawable/ic_baseline_chevron_right_24" /> + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_peers.xml b/app/src/main/res/layout/activity_peers.xml index 3001949..a9d74b0 100644 --- a/app/src/main/res/layout/activity_peers.xml +++ b/app/src/main/res/layout/activity_peers.xml @@ -128,7 +128,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16pt" android:layout_marginLeft="16pt" - android:layout_marginTop="2pt" + android:layout_marginTop="4pt" android:layout_marginEnd="8pt" android:layout_marginRight="8pt" android:layout_marginBottom="4pt" @@ -237,7 +237,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16pt" android:layout_marginLeft="16pt" - android:layout_marginTop="2pt" + android:layout_marginTop="4pt" android:layout_marginEnd="8pt" android:layout_marginRight="8pt" android:alpha="0.7" diff --git a/app/src/main/res/layout/dialog_add_dns_server.xml b/app/src/main/res/layout/dialog_add_dns_server.xml new file mode 100644 index 0000000..341c502 --- /dev/null +++ b/app/src/main/res/layout/dialog_add_dns_server.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_addpeer.xml b/app/src/main/res/layout/dialog_addpeer.xml index c627c5c..0e334d2 100644 --- a/app/src/main/res/layout/dialog_addpeer.xml +++ b/app/src/main/res/layout/dialog_addpeer.xml @@ -27,6 +27,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4pt" + android:lines="1" android:hint="tcp://address:port" /> diff --git a/app/src/main/res/layout/dns_server_usable.xml b/app/src/main/res/layout/dns_server_usable.xml new file mode 100644 index 0000000..d26e788 --- /dev/null +++ b/app/src/main/res/layout/dns_server_usable.xml @@ -0,0 +1,47 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/peers_configured.xml b/app/src/main/res/layout/peers_configured.xml index 82615d5..19ede91 100644 --- a/app/src/main/res/layout/peers_configured.xml +++ b/app/src/main/res/layout/peers_configured.xml @@ -1,7 +1,6 @@ @@ -16,7 +15,7 @@ android:layout_marginBottom="12dp" android:ellipsize="end" android:singleLine="true" - android:text="TextView" + android:text="" android:textColor="?attr/textDefault" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/deletePeerButton" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c2e6bce..a8469d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,20 @@ Yggdrasil + Yggdrasil will use these DNS servers in VPN config when service starts. All your DNS requests will be resolved by them. + Yggdrasil will not configure any DNS servers when service starts. All your DNS requests will be resolved by system resolver. + These are DNS servers run by community members. Click plus button to add them to a list above. Long-tap to see more info. + No servers configured + Configured servers + The server supports resolving regular ICANN domains, ALFIS domains, OpenNIC domains.\n\nAlso, it blocks ads, analytics and malware websites.\n\nThe server is run by Revertron. + Server info + OK + Cancel + Remove + Add + Add DNS server + DNS + Usable servers + Fix Chrome-based browsers + Chrome-based browsers need an additional fix to understand that you have IPv6 connectivity. + DNS fixes \ No newline at end of file diff --git a/build.gradle b/build.gradle index b090095..0cf859d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.5.0" + ext.kotlin_version = '1.7.20' repositories { google() mavenCentral() } dependencies { - classpath "com.android.tools.build:gradle:4.2.1" + classpath "com.android.tools.build:gradle:4.2.2" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong @@ -18,7 +18,6 @@ allprojects { repositories { google() mavenCentral() - jcenter() // Warning: this repository is going to shut down soon } } diff --git a/readme.md b/readme.md index a4f2ec1..69fe52e 100644 --- a/readme.md +++ b/readme.md @@ -26,7 +26,7 @@ cp /tmp/yggdrasil-go/yggdrasil.aar /tmp/yggdrasil-android/app/libs/ ``` cd /tmp/yggdrasil-android -./gradew assembleRelease +./gradlew assembleRelease ``` note: you will need to use jdk-11 as jdk-16 `"doesn't work" ™`