Implement VPN over native AP
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="be.mygod.vpnhotspot">
|
||||
|
||||
<uses-feature android:name="android.hardware.wifi.direct" android:required="true"/>
|
||||
<uses-feature android:name="android.hardware.wifi"/>
|
||||
<uses-feature android:name="android.hardware.wifi.direct" android:required="false"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.NetworkInfo
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.net.wifi.p2p.WifiP2pInfo
|
||||
import android.net.wifi.p2p.WifiP2pManager
|
||||
@@ -16,7 +18,6 @@ import android.support.v4.content.LocalBroadcastManager
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import java.net.NetworkInterface
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class HotspotService : Service(), WifiP2pManager.ChannelListener {
|
||||
@@ -24,7 +25,15 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
|
||||
const val CHANNEL = "hotspot"
|
||||
const val STATUS_CHANGED = "be.mygod.vpnhotspot.HotspotService.STATUS_CHANGED"
|
||||
const val KEY_UPSTREAM = "service.upstream"
|
||||
const val KEY_WIFI = "service.wifi"
|
||||
private const val TAG = "HotspotService"
|
||||
// constants from WifiManager
|
||||
private const val WIFI_AP_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_AP_STATE_CHANGED"
|
||||
private const val WIFI_AP_STATE_ENABLED = 13
|
||||
|
||||
private val upstream get() = app.pref.getString(KEY_UPSTREAM, "tun0")
|
||||
private val wifi get() = app.pref.getString(KEY_WIFI, "wlan0")
|
||||
private val dns get() = app.pref.getString("service.dns", "8.8.8.8:53")
|
||||
|
||||
/**
|
||||
* Matches the output of dumpsys wifip2p. This part is available since Android 4.2.
|
||||
@@ -37,10 +46,17 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
|
||||
* https://android.googlesource.com/platform/frameworks/base.git/+/220871a/core/java/android/net/NetworkInfo.java#415
|
||||
*/
|
||||
private val patternNetworkInfo = "^mNetworkInfo .* (isA|a)vailable: (true|false)".toPattern(Pattern.MULTILINE)
|
||||
|
||||
private val isWifiApEnabledMethod = WifiManager::class.java.getDeclaredMethod("isWifiApEnabled")
|
||||
val WifiManager.isWifiApEnabled get() = isWifiApEnabledMethod.invoke(this) as Boolean
|
||||
|
||||
init {
|
||||
isWifiApEnabledMethod.isAccessible = true
|
||||
}
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
IDLE, STARTING, ACTIVE
|
||||
IDLE, STARTING, ACTIVE_P2P, ACTIVE_AP
|
||||
}
|
||||
|
||||
inner class HotspotBinder : Binder() {
|
||||
@@ -48,25 +64,33 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
|
||||
var data: MainActivity.Data? = null
|
||||
|
||||
fun shutdown() {
|
||||
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
||||
override fun onSuccess() = clean()
|
||||
override fun onFailure(reason: Int) {
|
||||
if (reason == WifiP2pManager.BUSY) clean() else { // assuming it's already gone
|
||||
Toast.makeText(this@HotspotService, "Failed to remove P2P group (${formatReason(reason)})",
|
||||
Toast.LENGTH_SHORT).show()
|
||||
LocalBroadcastManager.getInstance(this@HotspotService).sendBroadcast(Intent(STATUS_CHANGED))
|
||||
when (status) {
|
||||
Status.ACTIVE_P2P -> p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
||||
override fun onSuccess() = clean()
|
||||
override fun onFailure(reason: Int) {
|
||||
if (reason == WifiP2pManager.BUSY) clean() else { // assuming it's already gone
|
||||
Toast.makeText(this@HotspotService, "Failed to remove P2P group (${formatReason(reason)})",
|
||||
Toast.LENGTH_SHORT).show()
|
||||
LocalBroadcastManager.getInstance(this@HotspotService)
|
||||
.sendBroadcast(Intent(STATUS_CHANGED))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
else -> clean()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var p2pManager: WifiP2pManager
|
||||
private lateinit var channel: WifiP2pManager.Channel
|
||||
private val wifiManager by lazy { getSystemService(Context.WIFI_SERVICE) as WifiManager }
|
||||
private val p2pManager by lazy { getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager }
|
||||
private var _channel: WifiP2pManager.Channel? = null
|
||||
private val channel: WifiP2pManager.Channel get() {
|
||||
if (_channel == null) onChannelDisconnected()
|
||||
return _channel!!
|
||||
}
|
||||
lateinit var group: WifiP2pGroup
|
||||
private set
|
||||
var hostAddress: String? = null
|
||||
private set
|
||||
private var apConfiguration: WifiConfiguration? = null
|
||||
private val binder = HotspotBinder()
|
||||
private var receiverRegistered = false
|
||||
private val receiver = broadcastReceiver { _, intent ->
|
||||
@@ -78,25 +102,30 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
|
||||
val info = intent.getParcelableExtra<WifiP2pInfo>(WifiP2pManager.EXTRA_WIFI_P2P_INFO)
|
||||
val net = intent.getParcelableExtra<NetworkInfo>(WifiP2pManager.EXTRA_NETWORK_INFO)
|
||||
val group = intent.getParcelableExtra<WifiP2pGroup>(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)
|
||||
if (downstream == null) onGroupCreated(info, group)
|
||||
if (routing == null) onGroupCreated(info, group)
|
||||
this.group = group
|
||||
binder.data?.onGroupChanged()
|
||||
showNotification(group)
|
||||
Log.d(TAG, "${intent.action}: $info, $net, $group")
|
||||
}
|
||||
WIFI_AP_STATE_CHANGED_ACTION ->
|
||||
if (intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0) != WIFI_AP_STATE_ENABLED) clean()
|
||||
}
|
||||
}
|
||||
|
||||
var downstream: String? = null
|
||||
val ssid get() = when (status) {
|
||||
HotspotService.Status.ACTIVE_P2P -> group.networkName
|
||||
HotspotService.Status.ACTIVE_AP -> apConfiguration?.SSID ?: "Unknown"
|
||||
else -> null
|
||||
}
|
||||
val password get() = when (status) {
|
||||
HotspotService.Status.ACTIVE_P2P -> group.passphrase
|
||||
HotspotService.Status.ACTIVE_AP -> apConfiguration?.preSharedKey
|
||||
else -> null
|
||||
}
|
||||
|
||||
var routing: Routing? = null
|
||||
private set
|
||||
private val upstream get() = app.pref.getString(KEY_UPSTREAM, "tun0")
|
||||
/**
|
||||
* subnetPrefixLength has been the same forever but this option is here anyways. Source:
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/android-4.0.1_r1/wifi/java/android/net/wifi/p2p/WifiP2pService.java#1028
|
||||
* https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/a8d5e40/service/java/com/android/server/wifi/p2p/WifiP2pServiceImpl.java#2547
|
||||
*/
|
||||
private var subnetPrefixLength: Short = 24
|
||||
private val dns get() = app.pref.getString("service.dns", "8.8.8.8:53")
|
||||
|
||||
var status = Status.IDLE
|
||||
private set(value) {
|
||||
@@ -115,50 +144,58 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
|
||||
|
||||
override fun onBind(intent: Intent) = binder
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
p2pManager = getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager
|
||||
onChannelDisconnected()
|
||||
}
|
||||
|
||||
override fun onChannelDisconnected() {
|
||||
channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
|
||||
_channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (status != Status.IDLE) return START_NOT_STICKY
|
||||
status = Status.STARTING
|
||||
val matcher = patternNetworkInfo.matcher(loggerSu("dumpsys ${Context.WIFI_P2P_SERVICE}"))
|
||||
if (!matcher.find()) {
|
||||
startFailure("Root unavailable")
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
if (matcher.group(2) != "true") {
|
||||
startFailure("Wi-Fi direct unavailable")
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
if (!receiverRegistered) {
|
||||
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
|
||||
receiverRegistered = true
|
||||
}
|
||||
p2pManager.requestGroupInfo(channel, {
|
||||
when {
|
||||
it == null -> doStart()
|
||||
it.isGroupOwner -> doStart(it)
|
||||
else -> {
|
||||
Log.i(TAG, "Removing old group ($it)")
|
||||
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
||||
override fun onSuccess() = doStart()
|
||||
override fun onFailure(reason: Int) {
|
||||
Toast.makeText(this@HotspotService,
|
||||
"Failed to remove old P2P group (${formatReason(reason)})", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
when {
|
||||
!matcher.find() -> startFailure("Root unavailable")
|
||||
matcher.group(2) == "true" -> {
|
||||
unregisterReceiver()
|
||||
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
|
||||
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
|
||||
receiverRegistered = true
|
||||
p2pManager.requestGroupInfo(channel, {
|
||||
when {
|
||||
it == null -> doStart()
|
||||
it.isGroupOwner -> doStart(it)
|
||||
else -> {
|
||||
Log.i(TAG, "Removing old group ($it)")
|
||||
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
||||
override fun onSuccess() = doStart()
|
||||
override fun onFailure(reason: Int) {
|
||||
Toast.makeText(this@HotspotService,
|
||||
"Failed to remove old P2P group (${formatReason(reason)})",
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
wifiManager.isWifiApEnabled -> {
|
||||
unregisterReceiver()
|
||||
registerReceiver(receiver, intentFilter(WIFI_AP_STATE_CHANGED_ACTION))
|
||||
receiverRegistered = true
|
||||
val routing = try {
|
||||
Routing(upstream, wifi)
|
||||
} catch (_: Routing.InterfaceNotFoundException) {
|
||||
startFailure(getString(R.string.exception_interface_not_found))
|
||||
return START_NOT_STICKY
|
||||
}.apRule().forward().dnsRedirect(dns)
|
||||
if (routing.start()) {
|
||||
this.routing = routing
|
||||
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")
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
@@ -173,72 +210,50 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
|
||||
})
|
||||
private fun doStart(group: WifiP2pGroup) {
|
||||
this.group = group
|
||||
status = Status.ACTIVE
|
||||
status = Status.ACTIVE_P2P
|
||||
showNotification(group)
|
||||
}
|
||||
private fun showNotification(group: WifiP2pGroup? = null) {
|
||||
val deviceCount = group?.clientList?.size ?: 0
|
||||
startForeground(1,
|
||||
NotificationCompat.Builder(this@HotspotService, CHANNEL)
|
||||
.setWhen(0)
|
||||
.setColor(ContextCompat.getColor(this@HotspotService, R.color.colorPrimary))
|
||||
.setContentTitle(group?.networkName)
|
||||
.setContentText(group?.passphrase)
|
||||
.setSubText(resources.getQuantityString(R.plurals.notification_connected_devices,
|
||||
deviceCount, deviceCount))
|
||||
.setSmallIcon(R.drawable.ic_device_wifi_tethering)
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0,
|
||||
Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.build())
|
||||
val builder = NotificationCompat.Builder(this, CHANNEL)
|
||||
.setWhen(0)
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
.setContentTitle(group?.networkName ?: ssid ?: "Connecting...")
|
||||
.setSmallIcon(R.drawable.ic_device_wifi_tethering)
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0,
|
||||
Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
if (group != null) builder.setContentText(resources.getQuantityString(R.plurals.notification_connected_devices,
|
||||
group.clientList.size, group.clientList.size))
|
||||
startForeground(1, builder.build())
|
||||
}
|
||||
|
||||
private fun onGroupCreated(info: WifiP2pInfo, group: WifiP2pGroup) {
|
||||
val owner = info.groupOwnerAddress
|
||||
val hostAddress = owner?.hostAddress
|
||||
val downstream = group.`interface`
|
||||
if (!info.groupFormed || !info.isGroupOwner || downstream == null || hostAddress == null) return
|
||||
this.downstream = downstream
|
||||
this.hostAddress = hostAddress
|
||||
var subnetPrefixLength = NetworkInterface.getByName(downstream)?.interfaceAddresses
|
||||
?.singleOrNull { it.address == owner }?.networkPrefixLength
|
||||
if (subnetPrefixLength == null) {
|
||||
Log.w(TAG, "Unable to find prefix length of interface $downstream, 24 is assumed")
|
||||
subnetPrefixLength = 24
|
||||
}
|
||||
this.subnetPrefixLength = subnetPrefixLength
|
||||
if (noisySu("echo 1 >/proc/sys/net/ipv4/ip_forward",
|
||||
"ip route add default dev $upstream scope link table 62",
|
||||
"ip route add $hostAddress/$subnetPrefixLength dev $downstream scope link table 62",
|
||||
"ip route add broadcast 255.255.255.255 dev $downstream scope link table 62",
|
||||
"ip rule add iif $downstream lookup 62",
|
||||
"iptables -N vpnhotspot_fwd",
|
||||
"iptables -A vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT",
|
||||
"iptables -A vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT",
|
||||
"iptables -I FORWARD -j vpnhotspot_fwd",
|
||||
"iptables -t nat -A PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns",
|
||||
"iptables -t nat -A PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns")) {
|
||||
if (!info.groupFormed || !info.isGroupOwner || downstream == null || owner == null) return
|
||||
receiverRegistered = true
|
||||
val routing = try {
|
||||
Routing(upstream, downstream, owner)
|
||||
} catch (_: Routing.InterfaceNotFoundException) {
|
||||
startFailure(getString(R.string.exception_interface_not_found))
|
||||
return
|
||||
}.p2pRule().forward().dnsRedirect(dns)
|
||||
if (routing.start()) {
|
||||
this.routing = routing
|
||||
doStart(group)
|
||||
} else startFailure("Something went wrong, please check logcat.")
|
||||
}
|
||||
|
||||
private fun clean() {
|
||||
private fun unregisterReceiver() {
|
||||
if (receiverRegistered) {
|
||||
unregisterReceiver(receiver)
|
||||
receiverRegistered = false
|
||||
}
|
||||
if (downstream != null)
|
||||
if (noisySu("iptables -t nat -D PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns",
|
||||
"iptables -t nat -D PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns",
|
||||
"iptables -D FORWARD -j vpnhotspot_fwd",
|
||||
"iptables -F vpnhotspot_fwd",
|
||||
"iptables -X vpnhotspot_fwd",
|
||||
"ip rule del iif $downstream lookup 62",
|
||||
"ip route del broadcast 255.255.255.255 dev $downstream scope link table 62",
|
||||
"ip route del $hostAddress/$subnetPrefixLength dev $downstream scope link table 62",
|
||||
"ip route del default dev $upstream scope link table 62")) {
|
||||
hostAddress = null
|
||||
downstream = null
|
||||
} else Toast.makeText(this, "Something went wrong, please check logcat.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
private fun clean() {
|
||||
unregisterReceiver()
|
||||
if (routing?.stop() == false)
|
||||
Toast.makeText(this, "Something went wrong, please check logcat.", Toast.LENGTH_SHORT).show()
|
||||
routing = null
|
||||
status = Status.IDLE
|
||||
stopForeground(true)
|
||||
}
|
||||
|
||||
@@ -24,14 +24,13 @@ class MainActivity : AppCompatActivity(), ServiceConnection, Toolbar.OnMenuItemC
|
||||
inner class Data : BaseObservable() {
|
||||
val switchEnabled: Boolean
|
||||
@Bindable get() = when (binder?.service?.status) {
|
||||
HotspotService.Status.IDLE -> true
|
||||
HotspotService.Status.ACTIVE -> true
|
||||
HotspotService.Status.IDLE, HotspotService.Status.ACTIVE_P2P, HotspotService.Status.ACTIVE_AP -> true
|
||||
else -> false
|
||||
}
|
||||
var serviceStarted: Boolean
|
||||
@Bindable get() = when (binder?.service?.status) {
|
||||
HotspotService.Status.STARTING -> true
|
||||
HotspotService.Status.ACTIVE -> true
|
||||
HotspotService.Status.STARTING, HotspotService.Status.ACTIVE_P2P, HotspotService.Status.ACTIVE_AP ->
|
||||
true
|
||||
else -> false
|
||||
}
|
||||
set(value) {
|
||||
@@ -40,13 +39,12 @@ class MainActivity : AppCompatActivity(), ServiceConnection, Toolbar.OnMenuItemC
|
||||
HotspotService.Status.IDLE ->
|
||||
if (value) ContextCompat.startForegroundService(this@MainActivity,
|
||||
Intent(this@MainActivity, HotspotService::class.java))
|
||||
HotspotService.Status.ACTIVE -> if (!value) binder.shutdown()
|
||||
HotspotService.Status.ACTIVE_P2P, HotspotService.Status.ACTIVE_AP -> if (!value) binder.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
val running get() = binder?.service?.status == HotspotService.Status.ACTIVE
|
||||
val ssid: String @Bindable get() = if (running) binder!!.service.group.networkName else ""
|
||||
val password: String @Bindable get() = if (running) binder!!.service.group.passphrase else ""
|
||||
val ssid @Bindable get() = binder?.service?.ssid ?: "Service inactive"
|
||||
val password @Bindable get() = binder?.service?.password ?: ""
|
||||
|
||||
fun onStatusChanged() {
|
||||
notifyPropertyChanged(BR.switchEnabled)
|
||||
@@ -69,11 +67,11 @@ class MainActivity : AppCompatActivity(), ServiceConnection, Toolbar.OnMenuItemC
|
||||
private lateinit var arpCache: Map<String, String>
|
||||
|
||||
fun fetchClients() {
|
||||
if (data.running) {
|
||||
val binder = binder!!
|
||||
val binder = binder
|
||||
if (binder?.service?.status == HotspotService.Status.ACTIVE_P2P) {
|
||||
owner = binder.service.group.owner
|
||||
clients = binder.service.group.clientList
|
||||
arpCache = NetUtils.arp(binder.service.downstream)
|
||||
arpCache = NetUtils.arp(binder.service.routing?.downstream)
|
||||
} else owner = null
|
||||
notifyDataSetChanged() // recreate everything
|
||||
binding.swipeRefresher.isRefreshing = false
|
||||
@@ -89,7 +87,7 @@ class MainActivity : AppCompatActivity(), ServiceConnection, Toolbar.OnMenuItemC
|
||||
}
|
||||
holder.binding.device = device
|
||||
holder.binding.ipAddress = when (position) {
|
||||
0 -> binder?.service?.hostAddress
|
||||
0 -> binder?.service?.routing?.hostAddress
|
||||
else -> arpCache[device?.deviceAddress]
|
||||
}
|
||||
holder.binding.executePendingBindings()
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package be.mygod.vpnhotspot
|
||||
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.util.Log
|
||||
import java.io.DataInputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
object NetUtils {
|
||||
private const val TAG = "NetUtils"
|
||||
private val spaces = " +".toPattern()
|
||||
private val mac = "^([0-9a-f]{2}:){5}[0-9a-f]{2}$".toPattern()
|
||||
|
||||
@@ -14,4 +19,34 @@ object NetUtils {
|
||||
mac.matcher(it[3]).matches() }
|
||||
.associateBy({ it[3] }, { it[0] })
|
||||
}
|
||||
|
||||
/**
|
||||
* Load AP configuration from persistent storage.
|
||||
*
|
||||
* Based on: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/0cafbe0/service/java/com/android/server/wifi/WifiApConfigStore.java#138
|
||||
*/
|
||||
fun loadApConfiguration(): WifiConfiguration? = try {
|
||||
loggerSuStream("cat /data/misc/wifi/softap.conf").buffered().use {
|
||||
val data = DataInputStream(it)
|
||||
val version = data.readInt()
|
||||
when (version) {
|
||||
1, 2 -> {
|
||||
val config = WifiConfiguration()
|
||||
config.SSID = data.readUTF()
|
||||
if (version >= 2) data.readLong() // apBand and apChannel
|
||||
val authType = data.readInt()
|
||||
config.allowedKeyManagement.set(authType)
|
||||
if (authType != WifiConfiguration.KeyMgmt.NONE) config.preSharedKey = data.readUTF()
|
||||
config
|
||||
}
|
||||
else -> {
|
||||
Log.e(TAG, "Bad version on hotspot configuration file $version")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error reading hotspot configuration $e")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
77
mobile/src/main/java/be/mygod/vpnhotspot/Routing.kt
Normal file
77
mobile/src/main/java/be/mygod/vpnhotspot/Routing.kt
Normal file
@@ -0,0 +1,77 @@
|
||||
package be.mygod.vpnhotspot
|
||||
|
||||
import java.io.IOException
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.util.*
|
||||
|
||||
class Routing(private val upstream: String, val downstream: String, ownerAddress: InetAddress? = null) {
|
||||
companion object {
|
||||
fun clean() = noisySu(
|
||||
"iptables -t nat -F PREROUTING",
|
||||
"while iptables -D FORWARD -j vpnhotspot_fwd; do done",
|
||||
"iptables -F vpnhotspot_fwd",
|
||||
"iptables -X vpnhotspot_fwd",
|
||||
"while ip rule del lookup 62; do done",
|
||||
"ip route flush table 62",
|
||||
"while ip rule del priority 17999; do done")
|
||||
}
|
||||
|
||||
class InterfaceNotFoundException : IOException()
|
||||
|
||||
val hostAddress: String
|
||||
private val subnetPrefixLength: Short
|
||||
private val startScript = LinkedList<String>()
|
||||
private val stopScript = LinkedList<String>()
|
||||
init {
|
||||
val address = NetworkInterface.getByName(downstream)?.interfaceAddresses
|
||||
?.singleOrNull { if (ownerAddress == null) it.address is Inet4Address else it.address == ownerAddress }
|
||||
?: throw InterfaceNotFoundException()
|
||||
hostAddress = address.address.hostAddress
|
||||
subnetPrefixLength = address.networkPrefixLength
|
||||
}
|
||||
|
||||
fun p2pRule(): Routing {
|
||||
startScript.add("echo 1 >/proc/sys/net/ipv4/ip_forward") // Wi-Fi direct doesn't enable ip_forward
|
||||
startScript.add("ip route add default dev $upstream scope link table 62")
|
||||
startScript.add("ip route add $hostAddress/$subnetPrefixLength dev $downstream scope link table 62")
|
||||
startScript.add("ip route add broadcast 255.255.255.255 dev $downstream scope link table 62")
|
||||
startScript.add("ip rule add iif $downstream lookup 62")
|
||||
stopScript.addFirst("ip route del default dev $upstream scope link table 62")
|
||||
stopScript.addFirst("ip route del $hostAddress/$subnetPrefixLength dev $downstream scope link table 62")
|
||||
stopScript.addFirst("ip route del broadcast 255.255.255.255 dev $downstream scope link table 62")
|
||||
stopScript.addFirst("ip rule del iif $downstream lookup 62")
|
||||
return this
|
||||
}
|
||||
|
||||
/* Since Android 5.0, RULE_PRIORITY_TETHERING = 18000.
|
||||
* https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#65 */
|
||||
fun apRule(): Routing {
|
||||
startScript.add("ip rule add from all iif $downstream lookup $upstream priority 17999")
|
||||
stopScript.addFirst("ip rule del from all iif $downstream lookup $upstream priority 17999")
|
||||
return this
|
||||
}
|
||||
|
||||
fun forward(): Routing {
|
||||
startScript.add("iptables -N vpnhotspot_fwd")
|
||||
startScript.add("iptables -A vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
||||
startScript.add("iptables -A vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT")
|
||||
startScript.add("iptables -I FORWARD -j vpnhotspot_fwd")
|
||||
stopScript.addFirst("iptables -X vpnhotspot_fwd")
|
||||
stopScript.addFirst("iptables -F vpnhotspot_fwd")
|
||||
stopScript.addFirst("iptables -D FORWARD -j vpnhotspot_fwd")
|
||||
return this
|
||||
}
|
||||
|
||||
fun dnsRedirect(dns: String): Routing {
|
||||
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")
|
||||
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 udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns")
|
||||
return this
|
||||
}
|
||||
|
||||
fun start() = noisySu(startScript)
|
||||
fun stop() = noisySu(stopScript)
|
||||
}
|
||||
@@ -27,12 +27,7 @@ class SettingsFragment : PreferenceFragmentCompatDividers(), ServiceConnection {
|
||||
addPreferencesFromResource(R.xml.pref_settings)
|
||||
service = findPreference("service")
|
||||
findPreference("service.clean").setOnPreferenceClickListener {
|
||||
noisySu("iptables -t nat -F PREROUTING",
|
||||
"while iptables -D FORWARD -j vpnhotspot_fwd; do done",
|
||||
"iptables -F vpnhotspot_fwd",
|
||||
"iptables -X vpnhotspot_fwd",
|
||||
"while ip rule del lookup 62; do done",
|
||||
"ip route flush table 62")
|
||||
Routing.clean()
|
||||
true
|
||||
}
|
||||
findPreference("misc.logcat").setOnPreferenceClickListener {
|
||||
@@ -59,7 +54,13 @@ class SettingsFragment : PreferenceFragmentCompatDividers(), ServiceConnection {
|
||||
Bundle().put(AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat.KEY_SUGGESTIONS,
|
||||
NetworkInterface.getNetworkInterfaces().asSequence()
|
||||
.filter { it.isUp && !it.isLoopback && it.interfaceAddresses.isNotEmpty() }
|
||||
.map { it.name }.toList().toTypedArray()))
|
||||
.map { it.name }.sorted().toList().toTypedArray()))
|
||||
HotspotService.KEY_WIFI -> displayPreferenceDialog(
|
||||
AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat(), HotspotService.KEY_WIFI,
|
||||
Bundle().put(AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat.KEY_SUGGESTIONS,
|
||||
NetworkInterface.getNetworkInterfaces().asSequence()
|
||||
.filter { !it.isLoopback } // wlan0 is down in airplane mode
|
||||
.map { it.name }.sorted().toList().toTypedArray()))
|
||||
else -> super.onDisplayPreferenceDialog(preference)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import java.io.InputStream
|
||||
|
||||
fun broadcastReceiver(receiver: (Context, Intent) -> Unit) = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) = receiver(context, intent)
|
||||
@@ -24,21 +25,23 @@ fun Bundle.put(key: String, map: Array<String>): Bundle {
|
||||
|
||||
const val NOISYSU_TAG = "NoisySU"
|
||||
const val NOISYSU_SUFFIX = "SUCCESS\n"
|
||||
fun loggerSu(vararg commands: String): String {
|
||||
val process = ProcessBuilder("su", "-c", commands.joinToString("\n"))
|
||||
fun loggerSuStream(command: String): InputStream {
|
||||
val process = ProcessBuilder("su", "-c", command)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
process.waitFor()
|
||||
val err = process.errorStream.bufferedReader().use { it.readText() }
|
||||
if (!err.isBlank()) Log.e(NOISYSU_TAG, err)
|
||||
return process.inputStream.bufferedReader().use { it.readText() }
|
||||
return process.inputStream
|
||||
}
|
||||
fun noisySu(vararg commands: String): Boolean {
|
||||
fun loggerSu(command: String): String = loggerSuStream(command).bufferedReader().use { it.readText() }
|
||||
fun noisySu(commands: Iterable<String>): Boolean {
|
||||
var out = loggerSu("""function noisy() { "$@" || echo "$@" exited with $?; }
|
||||
${commands.joinToString("\n") { if (it.startsWith("while ")) it else "noisy $it" }}
|
||||
echo $NOISYSU_SUFFIX""")
|
||||
val result = out != NOISYSU_SUFFIX
|
||||
val result = out == NOISYSU_SUFFIX
|
||||
out = out.removeSuffix(NOISYSU_SUFFIX)
|
||||
if (!out.isBlank()) Log.i(NOISYSU_TAG, out)
|
||||
return result
|
||||
}
|
||||
fun noisySu(vararg commands: String) = noisySu(commands.asIterable())
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
<item quantity="one">1 connected device</item>
|
||||
<item quantity="other">%d connected devices</item>
|
||||
</plurals>
|
||||
<string name="exception_interface_not_found">Fatal: Downstream interface not found</string>
|
||||
</resources>
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
android:title="Upstream interface"
|
||||
android:summary="%s"
|
||||
android:defaultValue="tun0"/>
|
||||
<be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreference
|
||||
android:key="service.wifi"
|
||||
android:title="Wi-Fi interface"
|
||||
android:summary="%s"
|
||||
android:defaultValue="wlan0"/>
|
||||
<AutoSummaryEditTextPreference
|
||||
android:key="service.dns"
|
||||
android:title="Downstream DNS server:port"
|
||||
|
||||
Reference in New Issue
Block a user