aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/org/pacien/tincapp/service/TincVpnService.kt
blob: 3db8dcedfc9099035f8cf41624c45096a6b1f092 (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
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 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.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!!))
        }

        Tincd.start(netName, deviceFd!!.fd, privateKeys.first?.fd, privateKeys.second?.fd)
        setState(true, netName, interfaceCfg, deviceFd)
        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 connected: Boolean = false
        private var netName: String? = null
        private var interfaceCfg: VpnInterfaceConfiguration? = null
        private var fd: ParcelFileDescriptor? = null

        private fun setState(connected: Boolean, netName: String?, interfaceCfg: VpnInterfaceConfiguration?, fd: ParcelFileDescriptor?) {
            TincVpnService.connected = connected
            TincVpnService.netName = netName
            TincVpnService.interfaceCfg = interfaceCfg
            TincVpnService.fd = fd
        }

        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!!)
                fd?.close()
                Log.i(TAG, "All tinc daemons stopped.")
            } catch (e: IOException) {
                Log.wtf(TAG, e)
            } finally {
                setState(false, null, null, null)
            }
        }

        fun getCurrentNetName() = netName
        fun getCurrentInterfaceCfg() = interfaceCfg
        fun isConnected() = connected

    }

}