aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt
blob: ec0512ab4817466af8f614ae513db5c4e0b55315 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package org.pacien.tincapp.service

import android.app.Service
import android.content.Intent
import android.net.Uri
import android.net.VpnService
import android.os.ParcelFileDescriptor
import android.util.Log
import java8.util.concurrent.CompletableFuture
import org.apache.commons.configuration2.ex.ConversionException
import org.bouncycastle.openssl.PEMException
import org.pacien.tincapp.BuildConfig
import org.pacien.tincapp.R
import org.pacien.tincapp.commands.Tinc
import org.pacien.tincapp.commands.Tincd
import org.pacien.tincapp.context.App
import org.pacien.tincapp.context.AppPaths
import org.pacien.tincapp.data.TincConfiguration
import org.pacien.tincapp.data.VpnInterfaceConfiguration
import org.pacien.tincapp.extensions.Java.applyIgnoringException
import org.pacien.tincapp.extensions.VpnServiceBuilder.applyCfg
import org.pacien.tincapp.intent.action.TINC_SCHEME
import org.pacien.tincapp.utils.PemUtils
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException

/**
 * @author pacien
 */
class TincVpnService : VpnService() {

  override fun onDestroy() {
    stopVpn()
    super.onDestroy()
  }

  override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
    if (isConnected()) stopVpn()
    startVpn(intent.data.schemeSpecificPart, intent.data.fragment)
    return Service.START_REDELIVER_INTENT
  }

  private fun startVpn(netName: String, passphrase: String? = null) {
    if (netName.isBlank())
      return reportError(resources.getString(R.string.message_no_network_name_provided), docTopic = "intent-api")

    if (!AppPaths.storageAvailable())
      return reportError(resources.getString(R.string.message_storage_unavailable))

    if (!AppPaths.confDir(netName).exists())
      return reportError(resources.getString(R.string.message_no_configuration_for_network_format, netName), docTopic = "configuration")

    Log.i(TAG, "Starting tinc daemon for network \"$netName\".")

    val interfaceCfg = try {
      VpnInterfaceConfiguration.fromIfaceConfiguration(AppPaths.existing(AppPaths.netConfFile(netName)))
    } catch (e: FileNotFoundException) {
      return reportError(resources.getString(R.string.message_network_config_not_found_format, e.message!!), e, "configuration")
    } catch (e: ConversionException) {
      return reportError(resources.getString(R.string.message_network_config_invalid_format, e.message!!), e, "network-interface")
    }

    val deviceFd = try {
      Builder().setSession(netName)
        .applyCfg(interfaceCfg)
        .also { applyIgnoringException(it::addDisallowedApplication, BuildConfig.APPLICATION_ID) }
        .establish()
    } catch (e: IllegalArgumentException) {
      return reportError(resources.getString(R.string.message_network_config_invalid_format, e.message!!), e, "network-interface")
    }

    val privateKeys = try {
      val tincCfg = TincConfiguration.fromTincConfiguration(AppPaths.existing(AppPaths.tincConfFile(netName)))

      Pair(
        openPrivateKey(tincCfg.ed25519PrivateKeyFile ?: AppPaths.defaultEd25519PrivateKeyFile(netName), passphrase),
        openPrivateKey(tincCfg.privateKeyFile ?: AppPaths.defaultRsaPrivateKeyFile(netName), passphrase)
      )
    } catch (e: FileNotFoundException) {
      Pair(null, null)
    } catch (e: PEMException) {
      return reportError(resources.getString(R.string.message_could_not_decrypt_private_keys_format, e.message))
    }

    val daemon = Tincd.start(netName, deviceFd!!.fd, privateKeys.first?.fd, privateKeys.second?.fd)
    setState(netName, interfaceCfg, deviceFd, daemon)
    Log.i(TAG, "tinc daemon started.")
  }

  private fun openPrivateKey(f: File?, passphrase: String?): ParcelFileDescriptor? {
    if (f == null || !f.exists() || passphrase == null) return null

    val pipe = ParcelFileDescriptor.createPipe()
    val decryptedKey = PemUtils.decrypt(PemUtils.read(f), passphrase)
    val outputStream = ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])
    PemUtils.write(decryptedKey, outputStream.writer())
    return pipe[0]
  }

  private fun reportError(msg: String, e: Throwable? = null, docTopic: String? = null) {
    if (e != null)
      Log.e(TAG, msg, e)
    else
      Log.e(TAG, msg)

    App.alert(R.string.title_unable_to_start_tinc, msg,
      if (docTopic != null) resources.getString(R.string.app_doc_url_format, docTopic) else null)
  }

  companion object {

    val TAG = this::class.java.canonicalName!!

    private var netName: String? = null
    private var interfaceCfg: VpnInterfaceConfiguration? = null
    private var fd: ParcelFileDescriptor? = null
    private var daemon: CompletableFuture<Void>? = null

    private fun setState(netName: String?, interfaceCfg: VpnInterfaceConfiguration?,
                         fd: ParcelFileDescriptor?, daemon: CompletableFuture<Void>?) {
      TincVpnService.netName = netName
      TincVpnService.interfaceCfg = interfaceCfg
      TincVpnService.fd = fd
      TincVpnService.daemon = daemon
    }

    fun startVpn(netName: String, passphrase: String? = null) {
      App.getContext().startService(Intent(App.getContext(), TincVpnService::class.java)
        .setData(Uri.Builder().scheme(TINC_SCHEME).opaquePart(netName).fragment(passphrase).build()))
    }

    fun stopVpn() {
      try {
        Log.i(TAG, "Stopping any running tinc daemon.")
        if (netName != null) Tinc.stop(netName!!)
        daemon?.get()
        fd?.close()
        Log.i(TAG, "All tinc daemons stopped.")
      } catch (e: IOException) {
        Log.wtf(TAG, e)
      } finally {
        setState(null, null, null, null)
      }
    }

    fun getCurrentNetName() = netName
    fun getCurrentInterfaceCfg() = interfaceCfg
    fun isConnected() = !(daemon?.isDone ?: true)

  }

}