diff options
Diffstat (limited to 'app/src/main/kotlin/org/pacien/tincapp/service')
-rw-r--r-- | app/src/main/kotlin/org/pacien/tincapp/service/TincVpnService.kt | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/app/src/main/kotlin/org/pacien/tincapp/service/TincVpnService.kt b/app/src/main/kotlin/org/pacien/tincapp/service/TincVpnService.kt new file mode 100644 index 0000000..884229d --- /dev/null +++ b/app/src/main/kotlin/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 | |||
19 | package org.pacien.tincapp.service | ||
20 | |||
21 | import android.app.Service | ||
22 | import android.content.Context | ||
23 | import android.content.Intent | ||
24 | import android.net.VpnService | ||
25 | import android.os.ParcelFileDescriptor | ||
26 | import android.support.v4.content.LocalBroadcastManager | ||
27 | import java8.util.concurrent.CompletableFuture | ||
28 | import org.apache.commons.configuration2.ex.ConversionException | ||
29 | import org.bouncycastle.openssl.PEMException | ||
30 | import org.pacien.tincapp.BuildConfig | ||
31 | import org.pacien.tincapp.R | ||
32 | import org.pacien.tincapp.commands.Executor | ||
33 | import org.pacien.tincapp.commands.Tinc | ||
34 | import org.pacien.tincapp.commands.Tincd | ||
35 | import org.pacien.tincapp.context.App | ||
36 | import org.pacien.tincapp.context.AppPaths | ||
37 | import org.pacien.tincapp.data.TincConfiguration | ||
38 | import org.pacien.tincapp.data.VpnInterfaceConfiguration | ||
39 | import org.pacien.tincapp.extensions.Java.applyIgnoringException | ||
40 | import org.pacien.tincapp.extensions.Java.defaultMessage | ||
41 | import org.pacien.tincapp.extensions.VpnServiceBuilder.applyCfg | ||
42 | import org.pacien.tincapp.intent.Actions | ||
43 | import org.pacien.tincapp.utils.TincKeyring | ||
44 | import org.slf4j.LoggerFactory | ||
45 | import java.io.FileNotFoundException | ||
46 | |||
47 | /** | ||
48 | * @author pacien | ||
49 | */ | ||
50 | class 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 | .put |