package be.mygod.vpnhotspot import android.content.Intent import android.content.IntentFilter import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.net.Routing import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock import be.mygod.vpnhotspot.util.Event0 import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.widget.SmartSnackbar import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber class TetheringService : IpNeighbourMonitoringService() { companion object { const val EXTRA_ADD_INTERFACES = "interface.add" const val EXTRA_REMOVE_INTERFACE = "interface.remove" } inner class Binder : android.os.Binder() { val routingsChanged = Event0() fun isActive(iface: String): Boolean = synchronized(routings) { routings.containsKey(iface) } } private val binder = Binder() private val routings = HashMap() private var locked = false private var receiverRegistered = false private val receiver = broadcastReceiver { _, intent -> val extras = intent.extras ?: return@broadcastReceiver synchronized(routings) { for (iface in routings.keys - TetheringManager.getTetheredIfaces(extras)) routings.remove(iface)?.revert() updateRoutingsLocked() } } override val activeIfaces get() = synchronized(routings) { routings.keys.toList() } private fun updateRoutingsLocked() { if (locked && routings.keys.all { !TetherType.ofInterface(it).isWifi }) { WifiDoubleLock.release() locked = false } if (routings.isEmpty()) { unregisterReceiver() ServiceNotification.stopForeground(this) stopSelf() } else { if (!receiverRegistered) { receiverRegistered = true registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED)) app.onPreCleanRoutings[this] = { synchronized(routings) { for (iface in routings.keys) routings.put(iface, null)?.stop() } } app.onRoutingsCleaned[this] = { synchronized(routings) { updateRoutingsLocked() } } IpNeighbourMonitor.registerCallback(this) } val disableIpv6 = app.pref.getBoolean("service.disableIpv6", true) val iterator = routings.iterator() while (iterator.hasNext()) { val (downstream, value) = iterator.next() if (value != null) continue try { routings[downstream] = Routing(downstream).apply { try { if (app.dhcpWorkaround) dhcpWorkaround() // system tethering already has working forwarding rules // so it doesn't make sense to add additional forwarding rules forward() if (app.masquerade) masquerade() if (disableIpv6) disableIpv6() commit() } catch (e: Exception) { revert() throw e } } } catch (e: Exception) { Timber.w(e) SmartSnackbar.make(e).show() iterator.remove() } } if (routings.isEmpty()) { updateRoutingsLocked() return } updateNotification() } GlobalScope.launch(Dispatchers.Main) { binder.routingsChanged() } } override fun onBind(intent: Intent?) = binder override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent != null) { val ifaces = intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray() synchronized(routings) { for (iface in ifaces) { routings[iface] = null if (TetherType.ofInterface(iface).isWifi && !locked) { WifiDoubleLock.acquire() locked = true } } routings.remove(intent.getStringExtra(EXTRA_REMOVE_INTERFACE))?.revert() updateRoutingsLocked() } } else if (routings.isEmpty()) stopSelf(startId) return START_NOT_STICKY } override fun onDestroy() { routings.values.forEach { it?.revert() } // force clean to prevent leakage unregisterReceiver() super.onDestroy() } private fun unregisterReceiver() { if (receiverRegistered) { unregisterReceiver(receiver) app.onPreCleanRoutings -= this app.onRoutingsCleaned -= this IpNeighbourMonitor.unregisterCallback(this) receiverRegistered = false } } }