Support refresh routing rules

Useful when VPN interface has changed.
This commit is contained in:
Mygod
2018-01-05 10:07:20 +08:00
parent e82ba20c20
commit c37901a2de
7 changed files with 87 additions and 74 deletions

View File

@@ -18,6 +18,7 @@ import android.support.v4.content.LocalBroadcastManager
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import java.net.InetAddress
import java.util.regex.Pattern import java.util.regex.Pattern
class HotspotService : Service(), WifiP2pManager.ChannelListener { class HotspotService : Service(), WifiP2pManager.ChannelListener {
@@ -67,6 +68,22 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
Status.ACTIVE_P2P -> removeGroup() Status.ACTIVE_P2P -> removeGroup()
else -> clean() else -> clean()
} }
fun reapplyRouting() {
val routing = routing
routing?.stop()
try {
if (!when (status) {
Status.ACTIVE_P2P -> initP2pRouting(routing!!.downstream, routing.hostAddress)
Status.ACTIVE_AP -> initApRouting(routing!!.hostAddress)
else -> false
}) Toast.makeText(this@HotspotService, "Something went wrong, please check logcat.",
Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Toast.makeText(this@HotspotService, e.message, Toast.LENGTH_SHORT).show()
return
}
}
} }
private val wifiManager by lazy { getSystemService(Context.WIFI_SERVICE) as WifiManager } private val wifiManager by lazy { getSystemService(Context.WIFI_SERVICE) as WifiManager }
@@ -164,26 +181,34 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
unregisterReceiver() unregisterReceiver()
registerReceiver(receiver, intentFilter(WIFI_AP_STATE_CHANGED_ACTION)) registerReceiver(receiver, intentFilter(WIFI_AP_STATE_CHANGED_ACTION))
receiverRegistered = true receiverRegistered = true
val routing = try { try {
Routing(upstream, wifi) if (initApRouting()) {
} catch (_: Routing.InterfaceNotFoundException) { apConfiguration = NetUtils.loadApConfiguration()
startFailure(getString(R.string.exception_interface_not_found)) status = Status.ACTIVE_AP
return START_NOT_STICKY showNotification()
}.rule().forward().dnsRedirect(dns) } else startFailure("Something went wrong, please check logcat.", group)
if (routing.start()) { } catch (e: Routing.InterfaceNotFoundException) {
this.routing = routing startFailure(e.message, group)
apConfiguration = NetUtils.loadApConfiguration() }
status = Status.ACTIVE_AP
showNotification()
} else startFailure("Something went wrong, please check logcat.")
} }
else -> startFailure("Wi-Fi direct unavailable and hotspot disabled, please enable either") else -> startFailure("Wi-Fi direct unavailable and hotspot disabled, please enable either")
} }
return START_NOT_STICKY return START_NOT_STICKY
} }
private fun initApRouting(owner: InetAddress? = null): Boolean {
val routing = Routing(upstream, wifi, owner).rule().forward().dnsRedirect(dns)
return if (routing.start()) {
this.routing = routing
true
} else {
routing.stop()
this.routing = null
false
}
}
private fun startFailure(msg: String, group: WifiP2pGroup? = null) { private fun startFailure(msg: String?, group: WifiP2pGroup? = null) {
Toast.makeText(this@HotspotService, msg, Toast.LENGTH_SHORT).show() Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
showNotification() showNotification()
if (group != null) removeGroup() else clean() if (group != null) removeGroup() else clean()
} }
@@ -224,17 +249,26 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
val downstream = group.`interface` val downstream = group.`interface`
if (!info.groupFormed || !info.isGroupOwner || downstream == null || owner == null) return if (!info.groupFormed || !info.isGroupOwner || downstream == null || owner == null) return
receiverRegistered = true receiverRegistered = true
val routing = try { try {
Routing(upstream, downstream, owner) if (initP2pRouting(downstream, owner)) doStart(group)
} catch (_: Routing.InterfaceNotFoundException) { else startFailure("Something went wrong, please check logcat.", group)
startFailure(getString(R.string.exception_interface_not_found), group) } catch (e: Routing.InterfaceNotFoundException) {
startFailure(e.message, group)
return return
}.ipForward() // Wi-Fi direct doesn't enable ip_forward }
}
private fun initP2pRouting(downstream: String, owner: InetAddress): Boolean {
val routing = Routing(upstream, downstream, owner)
.ipForward() // Wi-Fi direct doesn't enable ip_forward
.rule().forward().dnsRedirect(dns) .rule().forward().dnsRedirect(dns)
if (routing.start()) { return if (routing.start()) {
this.routing = routing this.routing = routing
doStart(group) true
} else startFailure("Something went wrong, please check logcat.", group) } else {
routing.stop()
this.routing = null
false
}
} }
private fun removeGroup() { private fun removeGroup() {

View File

@@ -87,7 +87,7 @@ class MainActivity : AppCompatActivity(), ServiceConnection, Toolbar.OnMenuItemC
} }
holder.binding.device = device holder.binding.device = device
holder.binding.ipAddress = when (position) { holder.binding.ipAddress = when (position) {
0 -> binder?.service?.routing?.hostAddress 0 -> binder?.service?.routing?.hostAddress?.hostAddress
else -> arpCache[device?.deviceAddress] else -> arpCache[device?.deviceAddress]
} }
holder.binding.executePendingBindings() holder.binding.executePendingBindings()
@@ -116,6 +116,14 @@ class MainActivity : AppCompatActivity(), ServiceConnection, Toolbar.OnMenuItemC
} }
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) { override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
R.id.reapply -> {
val binder = binder
when (binder?.service?.status) {
HotspotService.Status.IDLE -> Routing.clean()
HotspotService.Status.ACTIVE_P2P, HotspotService.Status.ACTIVE_AP -> binder.reapplyRouting()
}
true
}
R.id.settings -> { R.id.settings -> {
startActivity(Intent(this, SettingsActivity::class.java)) startActivity(Intent(this, SettingsActivity::class.java))
true true

View File

@@ -1,5 +1,6 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import be.mygod.vpnhotspot.App.Companion.app
import java.io.IOException import java.io.IOException
import java.net.Inet4Address import java.net.Inet4Address
import java.net.InetAddress import java.net.InetAddress
@@ -16,10 +17,12 @@ class Routing(private val upstream: String, val downstream: String, ownerAddress
"while ip rule del priority 17999; do done") "while ip rule del priority 17999; do done")
} }
class InterfaceNotFoundException : IOException() class InterfaceNotFoundException : IOException() {
override val message: String get() = app.getString(R.string.exception_interface_not_found)
}
val hostAddress: String = (ownerAddress ?: NetworkInterface.getByName(downstream)?.inetAddresses?.asSequence() val hostAddress = ownerAddress ?: NetworkInterface.getByName(downstream)?.inetAddresses?.asSequence()
?.singleOrNull { it is Inet4Address } ?: throw InterfaceNotFoundException()).hostAddress ?.singleOrNull { it is Inet4Address } ?: throw InterfaceNotFoundException()
private val startScript = LinkedList<String>() private val startScript = LinkedList<String>()
private val stopScript = LinkedList<String>() private val stopScript = LinkedList<String>()
@@ -52,6 +55,7 @@ class Routing(private val upstream: String, val downstream: String, ownerAddress
} }
fun dnsRedirect(dns: String): Routing { fun dnsRedirect(dns: String): Routing {
val hostAddress = hostAddress.hostAddress
startScript.add("iptables -t nat -A PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns") startScript.add("iptables -t nat -A PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns")
startScript.add("iptables -t nat -A PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns") startScript.add("iptables -t nat -A PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns")
stopScript.addFirst("iptables -t nat -D PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns") stopScript.addFirst("iptables -t nat -D PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns")

View File

@@ -1,36 +1,23 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.support.customtabs.CustomTabsIntent import android.support.customtabs.CustomTabsIntent
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v4.content.LocalBroadcastManager
import android.support.v7.preference.Preference import android.support.v7.preference.Preference
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompatDividers import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompatDividers
import java.net.NetworkInterface import java.net.NetworkInterface
class SettingsFragment : PreferenceFragmentCompatDividers(), ServiceConnection { class SettingsFragment : PreferenceFragmentCompatDividers() {
private lateinit var service: Preference
private var binder: HotspotService.HotspotBinder? = null
private val statusListener = broadcastReceiver { _, _ -> onStatusChanged() }
private val customTabsIntent by lazy { private val customTabsIntent by lazy {
CustomTabsIntent.Builder().setToolbarColor(ContextCompat.getColor(activity!!, R.color.colorPrimary)).build() CustomTabsIntent.Builder().setToolbarColor(ContextCompat.getColor(activity!!, R.color.colorPrimary)).build()
} }
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_settings) addPreferencesFromResource(R.xml.pref_settings)
service = findPreference("service")
findPreference("service.clean").setOnPreferenceClickListener {
Routing.clean()
true
}
findPreference("misc.logcat").setOnPreferenceClickListener { findPreference("misc.logcat").setOnPreferenceClickListener {
val intent = Intent(Intent.ACTION_SEND) val intent = Intent(Intent.ACTION_SEND)
.setType("text/plain") .setType("text/plain")
@@ -61,33 +48,4 @@ class SettingsFragment : PreferenceFragmentCompatDividers(), ServiceConnection {
.put(AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat.KEY_SUGGESTIONS, app.wifiInterfaces)) .put(AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat.KEY_SUGGESTIONS, app.wifiInterfaces))
else -> super.onDisplayPreferenceDialog(preference) else -> super.onDisplayPreferenceDialog(preference)
} }
override fun onStart() {
super.onStart()
val activity = activity!!
activity.bindService(Intent(activity, HotspotService::class.java), this, Context.BIND_AUTO_CREATE)
}
override fun onStop() {
onServiceDisconnected(null)
activity!!.unbindService(this)
super.onStop()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
binder = service as HotspotService.HotspotBinder
onStatusChanged()
LocalBroadcastManager.getInstance(activity!!)
.registerReceiver(statusListener, intentFilter(HotspotService.STATUS_CHANGED))
}
override fun onServiceDisconnected(name: ComponentName?) {
LocalBroadcastManager.getInstance(activity!!).unregisterReceiver(statusListener)
binder = null
service.isEnabled = false
}
private fun onStatusChanged() {
service.isEnabled = binder!!.service.status == HotspotService.Status.IDLE
}
} }

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#fff"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>

View File

@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/reapply"
android:icon="@drawable/ic_navigation_refresh"
android:title="Reapply routing rules"
app:showAsAction="always"/>
<item <item
android:id="@+id/settings" android:id="@+id/settings"
android:icon="@drawable/ic_action_settings" android:icon="@drawable/ic_action_settings"

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory <PreferenceCategory
android:key="service"
android:title="Service"> android:title="Service">
<be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreference <be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreference
android:key="service.upstream" android:key="service.upstream"
@@ -18,10 +17,6 @@
android:title="Downstream DNS server:port" android:title="Downstream DNS server:port"
android:summary="%s" android:summary="%s"
android:defaultValue="8.8.8.8:53"/> android:defaultValue="8.8.8.8:53"/>
<Preference
android:key="service.clean"
android:title="Clean up"
android:summary="Remove routing rules"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:title="Misc"> android:title="Misc">