aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt')
-rw-r--r--app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt236
1 files changed, 236 insertions, 0 deletions
diff --git a/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt b/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt
new file mode 100644
index 0000000..884229d
--- /dev/null
+++ b/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt
@@ -0,0 +1,236 @@
1/*
2 * Tinc App, an Android binding and user interface for the tinc mesh VPN daemon
3 * Copyright (C) 2017-2018 Pacien TRAN-GIRARD
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19package org.pacien.tincapp.service
20
21import android.app.Service
22import android.content.Context
23import android.content.Intent
24import android.net.VpnService
25import android.os.ParcelFileDescriptor
26import android.support.v4.content.LocalBroadcastManager
27import java8.util.concurrent.CompletableFuture
28import org.apache.commons.configuration2.ex.ConversionException
29import org.bouncycastle.openssl.PEMException
30import org.pacien.tincapp.BuildConfig
31import org.pacien.tincapp.R
32import org.pacien.tincapp.commands.Executor
33import org.pacien.tincapp.commands.Tinc
34import org.pacien.tincapp.commands.Tincd
35import org.pacien.tincapp.context.App
36import org.pacien.tincapp.context.AppPaths
37import org.pacien.tincapp.data.TincConfiguration
38import org.pacien.tincapp.data.VpnInterfaceConfiguration
39import org.pacien.tincapp.extensions.Java.applyIgnoringException
40import org.pacien.tincapp.extensions.Java.defaultMessage
41import org.pacien.tincapp.extensions.VpnServiceBuilder.applyCfg
42import org.pacien.tincapp.intent.Actions
43import org.pacien.tincapp.utils.TincKeyring
44import org.slf4j.LoggerFactory
45import java.io.FileNotFoundException
46
47/**
48 * @author pacien
49 */
50class TincVpnService : VpnService() {
51 private val log by lazy { LoggerFactory.getLogger(this.javaClass)!! }
52
53 override fun onDestroy() {
54 stopVpn()
55 super.onDestroy()
56 }
57
58 override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
59 log.info("Intent received: {}", intent.toString())
60
61 when {
62 intent.action == Actions.ACTION_CONNECT && intent.scheme == Actions.TINC_SCHEME ->
63 startVpn(intent.data.schemeSpecificPart, intent.data.fragment)
64 intent.action == Actions.ACTION_DISCONNECT ->
65 stopVpn()
66 intent.action == Actions.ACTION_SYSTEM_CONNECT ->
67 restorePreviousConnection()
68 else ->
69 throw IllegalArgumentException("Invalid intent action received.")
70 }
71
72 return Service.START_NOT_STICKY
73 }
74
75 private fun restorePreviousConnection() {
76 val netName = getCurrentNetName()
77 if (netName == null) {
78 log.info("No connection to restore.")
79 return
80 }
81
82 log.info("Restoring previous connection to \"$netName\".")
83 startVpn(netName, getPassphrase())
84 }
85
86 private fun startVpn(netName: String, passphrase: String? = null): Unit = synchronized(this) {
87 if (netName.isBlank())
88 return reportError(resources.getString(R.string.message_no_network_name_provided), docTopic = "intent-api")
89
90 if (TincKeyring.needsPassphrase(netName) && passphrase == null)
91 return reportError(resources.getString(R.string.message_passphrase_required))
92
93 if (!AppPaths.storageAvailable())
94 return reportError(resources.getString(R.string.message_storage_unavailable))
95
96 if (!AppPaths.confDir(netName).exists())
97 return reportError(resources.getString(R.string.message_no_configuration_for_network_format, netName), docTopic = "configuration")
98
99 log.info("Starting tinc daemon for network \"$netName\".")
100 if (isConnected()) stopVpn()
101
102 val privateKeys = try {
103 TincConfiguration.fromTincConfiguration(AppPaths.existing(AppPaths.tincConfFile(netName))).let { tincCfg ->
104 Pair(
105 TincKeyring.openPrivateKey(tincCfg.ed25519PrivateKeyFile ?: AppPaths.defaultEd25519PrivateKeyFile(netName), passphrase),
106 TincKeyring.openPrivateKey(tincCfg.privateKeyFile ?: AppPaths.defaultRsaPrivateKeyFile(netName), passphrase))
107 }
108 } catch (e: FileNotFoundException) {
109 Pair(null, null)
110 } catch (e: PEMException) {
111 return reportError(resources.getString(R.string.message_could_not_decrypt_private_keys_format, e.message))
112 } catch (e: Exception) {
113 return reportError(resources.getString(R.string.message_could_not_read_private_key_format, e.defaultMessage()), e)
114 }
115
116 val interfaceCfg = try {
117 VpnInterfaceConfiguration.fromIfaceConfiguration(AppPaths.existing(AppPaths.netConfFile(netName)))
118 } catch (e: FileNotFoundException) {
119 return reportError(resources.getString(R.string.message_network_config_not_found_format, e.defaultMessage()), e, "configuration")
120 } catch (e: ConversionException) {
121 return reportError(resources.getString(R.string.message_network_config_invalid_format, e.defaultMessage()), e, "network-interface")
122 } catch (e: Exception) {
123 return reportError(resources.getString(R.string.message_could_not_read_network_configuration_format, e.defaultMessage()), e)
124 }
125
126 val deviceFd = try {
127 Builder().setSession(netName)
128 .applyCfg(interfaceCfg)
129 .also { applyIgnoringException(it::addDisallowedApplication, BuildConfig.APPLICATION_ID) }
130 .establish()!!
131 } catch (e: IllegalArgumentException) {
132 return reportError(resources.getString(R.string.message_network_config_invalid_format, e.defaultMessage()), e, "network-interface")
133 } catch (e: NullPointerException) {
134 return reportError(resources.getString(R.string.message_could_not_bind_iface), e)
135 } catch (e: Exception) {
136 return reportError(resources.getString(R.string.message_could_not_configure_iface, e.defaultMessage()), e)
137 }
138
139 val daemon = Tincd.start(netName, deviceFd.fd, privateKeys.first?.fd, privateKeys.second?.fd)
140 setState(netName, passphrase, interfaceCfg, deviceFd, daemon)
141
142 waitForDaemonStartup().whenComplete { _, exception ->
143 deviceFd.close()
144 privateKeys.first?.close()
145 privateKeys.second?.close()
146
147 if (exception != null) {
148 reportError(resources.getString(R.string.message_daemon_exited, exception.cause!!.defaultMessage()), exception)
149 } else {
150 log.info("tinc daemon started.")
151 broadcastEvent(Actions.EVENT_CONNECTED)
152 }
153 }
154 }
155
156 private fun stopVpn(): Unit = synchronized(this) {
157 log.info("Stopping any running tinc daemon.")
158 getCurrentNetName()?.let {
159 Tinc.stop(it).thenRun {
160 log.info("All tinc daemons stopped.")
161 broadcastEvent(Actions.EVENT_DISCONNECTED)
162 setState(null, null, null, null, null)
163 }
164 }
165 }
166
167 private fun reportError(msg: String, e: Throwable? = null, docTopic: String? = null) {
168 if (e != null)
169 log.error(msg, e)
170 else
171 log.error(msg)
172
173 broadcastEvent(Actions.EVENT_ABORTED)
174 App.alert(R.string.title_unable_to_start_tinc, msg,
175 if (docTopic != null) resources.getString(R.string.app_doc_url_format, docTopic) else null)
176 }
177
178 private fun broadcastEvent(event: String) {
179 LocalBroadcastManager.getInstance(this).sendBroadcast(Intent(event))
180 }
181
182 private fun waitForDaemonStartup() =
183 Executor
184 .runAsyncTask { Thread.sleep(SETUP_DELAY) }
185 .thenCompose { if (daemon!!.isDone) daemon!! else Executor.runAsyncTask { Unit } }
186
187 companion object {
188 private const val SETUP_DELAY = 500L // ms
189
190 private val STORE_NAME = this::class.java.`package`.name
191 private const val STORE_KEY_NETNAME = "netname"
192 private const val STORE_KEY_PASSPHRASE = "passphrase"
193
194 private val context by lazy { App.getContext() }
195 private val store by lazy { context.getSharedPreferences(STORE_NAME, Context.MODE_PRIVATE)!! }
196
197 private var interfaceCfg: VpnInterfaceConfiguration? = null
198 private var fd: ParcelFileDescriptor? = null
199 private var daemon: CompletableFuture<Unit>? = null
200
201 private fun saveConnection(netName: String?, passphrase: String?) =
202 store.edit()
203 .putString(STORE_KEY_NETNAME, netName)
204 .putString(STORE_KEY_PASSPHRASE, passphrase)
205 .apply()