Support non-strict mode for repeater
This commit makes possible: * Starting repeater without VPN; * Repeater will work better with stub VPN apps; * LAN addresses will be reachable.
This commit is contained in:
@@ -38,11 +38,6 @@ You'll have to use WPS for now to make the repeater switch to 2.4GHz.
|
|||||||
|
|
||||||
### [IPv6 tethering?](https://github.com/Mygod/VPNHotspot/issues/6)
|
### [IPv6 tethering?](https://github.com/Mygod/VPNHotspot/issues/6)
|
||||||
|
|
||||||
### Start repeater without VPN?
|
|
||||||
|
|
||||||
This app is designed for VPN interface as upstreams to simplify handling of connection change and NAT issues.
|
|
||||||
You could use stub VPN apps like [Blokada](https://github.com/blokadaorg/blokada) together with this app as a workaround.
|
|
||||||
|
|
||||||
### No root?
|
### No root?
|
||||||
|
|
||||||
Without root, you can only:
|
Without root, you can only:
|
||||||
|
|||||||
@@ -138,11 +138,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
|||||||
App.ACTION_CLEAN_ROUTINGS -> {
|
App.ACTION_CLEAN_ROUTINGS -> {
|
||||||
val routing = routing
|
val routing = routing
|
||||||
routing!!.started = false
|
routing!!.started = false
|
||||||
if (status == Status.ACTIVE) {
|
if (status == Status.ACTIVE) resetup(routing, upstream)
|
||||||
val upstream = upstream
|
|
||||||
if (upstream != null && !initRouting(upstream, routing.downstream, routing.hostAddress))
|
|
||||||
Toast.makeText(this@RepeaterService, R.string.noisy_su_failure, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +182,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
if (status != Status.IDLE) return START_NOT_STICKY
|
if (status != Status.IDLE) return START_NOT_STICKY
|
||||||
status = Status.STARTING
|
status = Status.STARTING
|
||||||
VpnMonitor.registerCallback(this) { startFailure(getString(R.string.repeater_vpn_unavailable)) }
|
VpnMonitor.registerCallback(this) { setup() }
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
private fun startFailure(msg: CharSequence?, group: WifiP2pGroup? = null) {
|
private fun startFailure(msg: CharSequence?, group: WifiP2pGroup? = null) {
|
||||||
@@ -198,8 +194,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
|||||||
/**
|
/**
|
||||||
* startService 2nd stop, also called when VPN re-established
|
* startService 2nd stop, also called when VPN re-established
|
||||||
*/
|
*/
|
||||||
override fun onAvailable(ifname: String) = when (status) {
|
private fun setup(ifname: String? = null) {
|
||||||
Status.STARTING -> {
|
|
||||||
val matcher = patternNetworkInfo.matcher(loggerSu("dumpsys ${Context.WIFI_P2P_SERVICE}") ?: "")
|
val matcher = patternNetworkInfo.matcher(loggerSu("dumpsys ${Context.WIFI_P2P_SERVICE}") ?: "")
|
||||||
when {
|
when {
|
||||||
!matcher.find() -> startFailure(getString(R.string.root_unavailable))
|
!matcher.find() -> startFailure(getString(R.string.root_unavailable))
|
||||||
@@ -232,11 +227,20 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
|||||||
else -> startFailure(getString(R.string.repeater_p2p_unavailable))
|
else -> startFailure(getString(R.string.repeater_p2p_unavailable))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resetup(routing: Routing, ifname: String? = null) =
|
||||||
|
initRouting(ifname, routing.downstream, routing.hostAddress)
|
||||||
|
|
||||||
|
override fun onAvailable(ifname: String) = when (status) {
|
||||||
|
Status.STARTING -> setup(ifname)
|
||||||
Status.ACTIVE -> {
|
Status.ACTIVE -> {
|
||||||
val routing = routing
|
val routing = routing!!
|
||||||
check(!routing!!.started)
|
if (routing.started) {
|
||||||
if (!initRouting(ifname, routing.downstream, routing.hostAddress))
|
routing.stop()
|
||||||
Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show() else { }
|
check(routing.upstream == null)
|
||||||
|
}
|
||||||
|
resetup(routing, ifname)
|
||||||
|
while (false) { }
|
||||||
}
|
}
|
||||||
else -> throw IllegalStateException("RepeaterService is in unexpected state when receiving onAvailable")
|
else -> throw IllegalStateException("RepeaterService is in unexpected state when receiving onAvailable")
|
||||||
}
|
}
|
||||||
@@ -244,6 +248,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
|||||||
if (routing?.stop() == false)
|
if (routing?.stop() == false)
|
||||||
Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
||||||
upstream = null
|
upstream = null
|
||||||
|
if (status == Status.ACTIVE) resetup(routing!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doStart() = p2pManager.createGroup(channel, object : WifiP2pManager.ActionListener {
|
private fun doStart() = p2pManager.createGroup(channel, object : WifiP2pManager.ActionListener {
|
||||||
@@ -275,22 +280,25 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
|
|||||||
val downstream = group.`interface` ?: return
|
val downstream = group.`interface` ?: return
|
||||||
receiverRegistered = true
|
receiverRegistered = true
|
||||||
try {
|
try {
|
||||||
if (initRouting(upstream ?: throw Routing.InterfaceNotFoundException(), downstream, owner))
|
if (initRouting(upstream, downstream, owner)) doStart(group)
|
||||||
doStart(group) else startFailure(getText(R.string.noisy_su_failure), group)
|
|
||||||
} catch (e: Routing.InterfaceNotFoundException) {
|
} catch (e: Routing.InterfaceNotFoundException) {
|
||||||
startFailure(e.message, group)
|
startFailure(e.message, group)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun initRouting(upstream: String, downstream: String, owner: InetAddress): Boolean {
|
private fun initRouting(upstream: String?, downstream: String, owner: InetAddress): Boolean {
|
||||||
val routing = Routing(upstream, downstream, owner)
|
val routing = Routing(upstream, downstream, owner)
|
||||||
.ipForward() // Wi-Fi direct doesn't enable ip_forward
|
|
||||||
.rule().forward().dnsRedirect(app.dns)
|
|
||||||
return if (routing.start()) {
|
|
||||||
this.routing = routing
|
this.routing = routing
|
||||||
true
|
val strict = app.pref.getBoolean("service.repeater.strict", false)
|
||||||
} else {
|
if (strict && upstream == null) return true // in this case, nothing to be done
|
||||||
|
return if (routing
|
||||||
|
.ipForward() // Wi-Fi direct doesn't enable ip_forward
|
||||||
|
.rule()
|
||||||
|
.forward(strict)
|
||||||
|
.dnsRedirect(app.dns)
|
||||||
|
.start()) true else {
|
||||||
routing.stop()
|
routing.stop()
|
||||||
|
Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ import android.service.quicksettings.TileService
|
|||||||
import android.support.annotation.RequiresApi
|
import android.support.annotation.RequiresApi
|
||||||
import android.support.v4.content.ContextCompat
|
import android.support.v4.content.ContextCompat
|
||||||
import android.support.v4.content.LocalBroadcastManager
|
import android.support.v4.content.LocalBroadcastManager
|
||||||
import be.mygod.vpnhotspot.net.VpnMonitor
|
|
||||||
|
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
class RepeaterTileService : TileService(), ServiceConnection, VpnMonitor.Callback {
|
class RepeaterTileService : TileService(), ServiceConnection {
|
||||||
private val statusListener = broadcastReceiver { _, _ -> updateTile() }
|
private val statusListener = broadcastReceiver { _, _ -> updateTile() }
|
||||||
private val tileOff by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_off) }
|
private val tileOff by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_off) }
|
||||||
private val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) }
|
private val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) }
|
||||||
@@ -44,7 +43,6 @@ class RepeaterTileService : TileService(), ServiceConnection, VpnMonitor.Callbac
|
|||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
|
||||||
binder = service as RepeaterService.RepeaterBinder
|
binder = service as RepeaterService.RepeaterBinder
|
||||||
updateTile()
|
updateTile()
|
||||||
VpnMonitor.registerCallback(this)
|
|
||||||
LocalBroadcastManager.getInstance(this).registerReceiver(statusListener,
|
LocalBroadcastManager.getInstance(this).registerReceiver(statusListener,
|
||||||
intentFilter(RepeaterService.ACTION_STATUS_CHANGED))
|
intentFilter(RepeaterService.ACTION_STATUS_CHANGED))
|
||||||
}
|
}
|
||||||
@@ -52,11 +50,10 @@ class RepeaterTileService : TileService(), ServiceConnection, VpnMonitor.Callbac
|
|||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
binder = null
|
binder = null
|
||||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(statusListener)
|
LocalBroadcastManager.getInstance(this).unregisterReceiver(statusListener)
|
||||||
VpnMonitor.unregisterCallback(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTile() {
|
private fun updateTile() {
|
||||||
when (if (VpnMonitor.available.isEmpty()) null else binder?.service?.status) {
|
when (binder?.service?.status) {
|
||||||
RepeaterService.Status.IDLE -> {
|
RepeaterService.Status.IDLE -> {
|
||||||
qsTile.state = Tile.STATE_INACTIVE
|
qsTile.state = Tile.STATE_INACTIVE
|
||||||
qsTile.icon = tileOff
|
qsTile.icon = tileOff
|
||||||
@@ -80,6 +77,4 @@ class RepeaterTileService : TileService(), ServiceConnection, VpnMonitor.Callbac
|
|||||||
}
|
}
|
||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
}
|
}
|
||||||
override fun onAvailable(ifname: String) = updateTile()
|
|
||||||
override fun onLost(ifname: String) = updateTile()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
|
|||||||
if (upstream != null) {
|
if (upstream != null) {
|
||||||
var failed = false
|
var failed = false
|
||||||
for ((downstream, value) in routings) if (value == null) {
|
for ((downstream, value) in routings) if (value == null) {
|
||||||
val routing = Routing(upstream, downstream).rule().forward().dnsRedirect(app.dns)
|
// system tethering already has working forwarding rules
|
||||||
|
// so it doesn't make sense to add additional forwarding rules
|
||||||
|
val routing = Routing(upstream, downstream).rule().forward(true).dnsRedirect(app.dns)
|
||||||
if (routing.start()) routings[downstream] = routing else {
|
if (routing.start()) routings[downstream] = routing else {
|
||||||
failed = true
|
failed = true
|
||||||
routing.stop()
|
routing.stop()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import java.net.InetAddress
|
|||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class Routing(private val upstream: String, val downstream: String, ownerAddress: InetAddress? = null) {
|
class Routing(val upstream: String?, val downstream: String, ownerAddress: InetAddress? = null) {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* -w <seconds> is not supported on 7.1-.
|
* -w <seconds> is not supported on 7.1-.
|
||||||
@@ -23,6 +23,7 @@ class Routing(private val upstream: String, val downstream: String, ownerAddress
|
|||||||
fun clean() = noisySu(
|
fun clean() = noisySu(
|
||||||
"$IPTABLES -t nat -F PREROUTING",
|
"$IPTABLES -t nat -F PREROUTING",
|
||||||
"quiet while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done",
|
"quiet while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done",
|
||||||
|
"quiet while $IPTABLES -t nat -D POSTROUTING -j MASQUERADE; do done",
|
||||||
"$IPTABLES -F vpnhotspot_fwd",
|
"$IPTABLES -F vpnhotspot_fwd",
|
||||||
"$IPTABLES -X vpnhotspot_fwd",
|
"$IPTABLES -X vpnhotspot_fwd",
|
||||||
"quiet while ip rule del priority 17900; do done")
|
"quiet while ip rule del priority 17900; do done")
|
||||||
@@ -50,19 +51,33 @@ class Routing(private val upstream: String, val downstream: String, ownerAddress
|
|||||||
* Source: https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#65
|
* Source: https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#65
|
||||||
*/
|
*/
|
||||||
fun rule(): Routing {
|
fun rule(): Routing {
|
||||||
|
if (upstream != null) {
|
||||||
startScript.add("ip rule add from all iif $downstream lookup $upstream priority 17900")
|
startScript.add("ip rule add from all iif $downstream lookup $upstream priority 17900")
|
||||||
// by the time stopScript is called, table entry for upstream may already get removed
|
// by the time stopScript is called, table entry for upstream may already get removed
|
||||||
stopScript.addFirst("ip rule del from all iif $downstream priority 17900")
|
stopScript.addFirst("ip rule del from all iif $downstream priority 17900")
|
||||||
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun forward(): Routing {
|
fun forward(strict: Boolean = true): Routing {
|
||||||
startScript.add("quiet $IPTABLES -N vpnhotspot_fwd 2>/dev/null")
|
startScript.add("quiet $IPTABLES -N vpnhotspot_fwd 2>/dev/null")
|
||||||
|
if (strict) {
|
||||||
|
check(upstream != null)
|
||||||
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
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 -A vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT")
|
||||||
startScript.add("$IPTABLES -I FORWARD -j vpnhotspot_fwd")
|
|
||||||
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
||||||
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT")
|
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT")
|
||||||
|
} else {
|
||||||
|
// for not strict mode, allow downstream packets to be redirected to anywhere
|
||||||
|
// also enable unconditional NAT masquerade
|
||||||
|
startScript.add("$IPTABLES -A vpnhotspot_fwd -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
||||||
|
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $downstream -j ACCEPT")
|
||||||
|
startScript.add("$IPTABLES -t nat -A POSTROUTING -j MASQUERADE")
|
||||||
|
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
||||||
|
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $downstream -j ACCEPT")
|
||||||
|
stopScript.addFirst("$IPTABLES -t nat -D POSTROUTING -j MASQUERADE")
|
||||||
|
}
|
||||||
|
startScript.add("$IPTABLES -I FORWARD -j vpnhotspot_fwd")
|
||||||
stopScript.addFirst("$IPTABLES -D FORWARD -j vpnhotspot_fwd")
|
stopScript.addFirst("$IPTABLES -D FORWARD -j vpnhotspot_fwd")
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,27 +46,19 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
|
|||||||
fun registerCallback(callback: Callback, failfast: (() -> Unit)? = null) {
|
fun registerCallback(callback: Callback, failfast: (() -> Unit)? = null) {
|
||||||
if (synchronized(this) {
|
if (synchronized(this) {
|
||||||
if (!callbacks.add(callback)) return
|
if (!callbacks.add(callback)) return
|
||||||
if (registered) {
|
if (!registered) {
|
||||||
if (failfast != null && available.isEmpty()) {
|
manager.registerNetworkCallback(request, this)
|
||||||
callbacks.remove(callback)
|
registered = true
|
||||||
true
|
manager.allNetworks.all {
|
||||||
} else {
|
|
||||||
available.forEach { callback.onAvailable(it.value) }
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else if (failfast != null && manager.allNetworks.all {
|
|
||||||
val cap = manager.getNetworkCapabilities(it)
|
val cap = manager.getNetworkCapabilities(it)
|
||||||
!cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
!cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||||
cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
}) {
|
}
|
||||||
callbacks.remove(callback)
|
} else if (available.isEmpty()) true else {
|
||||||
true
|
available.forEach { callback.onAvailable(it.value) }
|
||||||
} else {
|
|
||||||
manager.registerNetworkCallback(request, this)
|
|
||||||
registered = true
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}) failfast!!()
|
}) failfast?.invoke()
|
||||||
}
|
}
|
||||||
fun unregisterCallback(callback: Callback) = synchronized(this) {
|
fun unregisterCallback(callback: Callback) = synchronized(this) {
|
||||||
if (!callbacks.remove(callback) || callbacks.isNotEmpty() || !registered) return
|
if (!callbacks.remove(callback) || callbacks.isNotEmpty() || !registered) return
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
<string name="repeater_reset_credentials_failure">重置凭据失败(原因:%s)</string>
|
<string name="repeater_reset_credentials_failure">重置凭据失败(原因:%s)</string>
|
||||||
|
|
||||||
<string name="repeater_inactive">未打开</string>
|
<string name="repeater_inactive">未打开</string>
|
||||||
<string name="repeater_vpn_unavailable">无法找到 VPN,请启用任意 VPN</string>
|
|
||||||
<string name="repeater_p2p_unavailable">Wi-Fi 直连不可用</string>
|
<string name="repeater_p2p_unavailable">Wi-Fi 直连不可用</string>
|
||||||
<string name="repeater_create_group_failure">创建 P2P 群组失败(原因:%s)</string>
|
<string name="repeater_create_group_failure">创建 P2P 群组失败(原因:%s)</string>
|
||||||
<string name="repeater_remove_group_failure">关闭已有 P2P 群组失败(原因:%s)</string>
|
<string name="repeater_remove_group_failure">关闭已有 P2P 群组失败(原因:%s)</string>
|
||||||
@@ -42,6 +41,8 @@
|
|||||||
|
|
||||||
<string name="settings_service">服务</string>
|
<string name="settings_service">服务</string>
|
||||||
<string name="settings_service_dns">下游 DNS 服务器[:端口]</string>
|
<string name="settings_service_dns">下游 DNS 服务器[:端口]</string>
|
||||||
|
<string name="settings_service_repeater_strict">严格模式 (仅用于中继)</string>
|
||||||
|
<string name="settings_service_repeater_strict_summary">只允许通过 VPN 隧道的包通过</string>
|
||||||
<string name="settings_service_clean">清理/重新应用路由规则</string>
|
<string name="settings_service_clean">清理/重新应用路由规则</string>
|
||||||
<string name="settings_misc">杂项</string>
|
<string name="settings_misc">杂项</string>
|
||||||
<string name="settings_misc_logcat">导出日志</string>
|
<string name="settings_misc_logcat">导出日志</string>
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
<string name="repeater_reset_credentials_failure">Failed to reset credentials (reason: %s)</string>
|
<string name="repeater_reset_credentials_failure">Failed to reset credentials (reason: %s)</string>
|
||||||
|
|
||||||
<string name="repeater_inactive">Service inactive</string>
|
<string name="repeater_inactive">Service inactive</string>
|
||||||
<string name="repeater_vpn_unavailable">VPN unavailable, please enable any VPN</string>
|
|
||||||
<string name="repeater_p2p_unavailable">Wi-Fi direct unavailable</string>
|
<string name="repeater_p2p_unavailable">Wi-Fi direct unavailable</string>
|
||||||
<string name="repeater_create_group_failure">Failed to create P2P group (reason: %s)</string>
|
<string name="repeater_create_group_failure">Failed to create P2P group (reason: %s)</string>
|
||||||
<string name="repeater_remove_group_failure">Failed to remove P2P group (reason: %s)</string>
|
<string name="repeater_remove_group_failure">Failed to remove P2P group (reason: %s)</string>
|
||||||
@@ -44,6 +43,8 @@
|
|||||||
|
|
||||||
<string name="settings_service">Service</string>
|
<string name="settings_service">Service</string>
|
||||||
<string name="settings_service_dns">Downstream DNS server[:port]</string>
|
<string name="settings_service_dns">Downstream DNS server[:port]</string>
|
||||||
|
<string name="settings_service_repeater_strict">Strict mode (repeater only)</string>
|
||||||
|
<string name="settings_service_repeater_strict_summary">Only allow packets that goes through VPN tunnel</string>
|
||||||
<string name="settings_service_clean">Clean/reapply routing rules</string>
|
<string name="settings_service_clean">Clean/reapply routing rules</string>
|
||||||
<string name="settings_misc">Misc</string>
|
<string name="settings_misc">Misc</string>
|
||||||
<string name="settings_misc_logcat">Export logcat</string>
|
<string name="settings_misc_logcat">Export logcat</string>
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
android:summary="%s"
|
android:summary="%s"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:defaultValue="8.8.8.8"/>
|
android:defaultValue="8.8.8.8"/>
|
||||||
|
<SwitchPreference
|
||||||
|
android:key="service.repeater.strict"
|
||||||
|
android:title="@string/settings_service_repeater_strict"
|
||||||
|
android:summary="@string/settings_service_repeater_strict_summary"/>
|
||||||
<Preference
|
<Preference
|
||||||
android:key="service.clean"
|
android:key="service.clean"
|
||||||
android:title="@string/settings_service_clean"/>
|
android:title="@string/settings_service_clean"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user