Migrate to singbox
This commit is contained in:
@@ -60,6 +60,11 @@ android {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
aidl true
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
@@ -67,7 +72,11 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
implementation 'androidx.window:window:1.0.0'
|
||||
implementation 'androidx.window:window-java:1.0.0'
|
||||
|
||||
0
android/app/libs/.gitkeep
Normal file
0
android/app/libs/.gitkeep
Normal file
@@ -1,14 +1,24 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:label="hiddify"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round">
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -39,15 +49,14 @@
|
||||
<data android:host="install-config"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".HiddifyVpnService"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:stopWithTask="false"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".bg.VPNService"
|
||||
android:exported="false"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.hiddify.hiddify;
|
||||
|
||||
import com.hiddify.hiddify.IServiceCallback;
|
||||
|
||||
interface IService {
|
||||
int getStatus();
|
||||
void registerCallback(in IServiceCallback callback);
|
||||
oneway void unregisterCallback(in IServiceCallback callback);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.hiddify.hiddify;
|
||||
|
||||
interface IServiceCallback {
|
||||
void onServiceStatusChanged(int status);
|
||||
void onServiceAlert(int type, String message);
|
||||
void onServiceWriteLog(String message);
|
||||
void onServiceResetLogs(in List<String> messages);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,42 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
import com.hiddify.hiddify.bg.AppChangeReceiver
|
||||
import go.Seq
|
||||
import com.hiddify.hiddify.Application as BoxApplication
|
||||
|
||||
class Application : Application() {
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
|
||||
application = this
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
Seq.setContext(this)
|
||||
|
||||
registerReceiver(AppChangeReceiver(), IntentFilter().apply {
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addDataScheme("package")
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var application: BoxApplication
|
||||
val notification by lazy { application.getSystemService<NotificationManager>()!! }
|
||||
val connectivity by lazy { application.getSystemService<ConnectivityManager>()!! }
|
||||
val packageManager by lazy { application.packageManager }
|
||||
val powerManager by lazy { application.getSystemService<PowerManager>()!! }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Observer
|
||||
import com.hiddify.hiddify.constant.Alert
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
|
||||
class EventHandler : FlutterPlugin {
|
||||
|
||||
companion object {
|
||||
const val TAG = "A/EventHandler"
|
||||
const val SERVICE_STATUS = "com.hiddify.app/service.status"
|
||||
const val SERVICE_ALERTS = "com.hiddify.app/service.alerts"
|
||||
}
|
||||
|
||||
private lateinit var statusChannel: EventChannel
|
||||
private lateinit var alertsChannel: EventChannel
|
||||
|
||||
private lateinit var statusObserver: Observer<Status>
|
||||
private lateinit var alertsObserver: Observer<ServiceEvent?>
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
statusChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_STATUS)
|
||||
alertsChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_ALERTS)
|
||||
|
||||
statusChannel.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
statusObserver = Observer {
|
||||
Log.d(TAG, "new status: $it")
|
||||
val map = listOf(
|
||||
Pair("status", it.name)
|
||||
)
|
||||
.toMap()
|
||||
events?.success(map)
|
||||
}
|
||||
MainActivity.instance.serviceStatus.observeForever(statusObserver)
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
MainActivity.instance.serviceStatus.removeObserver(statusObserver)
|
||||
}
|
||||
})
|
||||
|
||||
alertsChannel.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
alertsObserver = Observer {
|
||||
if (it == null) return@Observer
|
||||
Log.d(TAG, "new alert: $it")
|
||||
val map = listOf(
|
||||
Pair("status", it.status.name),
|
||||
Pair("failure", it.alert?.name),
|
||||
Pair("message", it.message)
|
||||
)
|
||||
.mapNotNull { p -> p.second?.let { Pair(p.first, p.second) } }
|
||||
.toMap()
|
||||
events?.success(map)
|
||||
}
|
||||
MainActivity.instance.serviceAlerts.observeForever(alertsObserver)
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
MainActivity.instance.serviceAlerts.removeObserver(alertsObserver)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
MainActivity.instance.serviceStatus.removeObserver(statusObserver)
|
||||
statusChannel.setStreamHandler(null)
|
||||
MainActivity.instance.serviceAlerts.removeObserver(alertsObserver)
|
||||
alertsChannel.setStreamHandler(null)
|
||||
}
|
||||
}
|
||||
|
||||
data class ServiceEvent(val status: Status, val alert: Alert? = null, val message: String? = null)
|
||||
@@ -1,317 +0,0 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.StrictMode
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
class HiddifyVpnService : VpnService() {
|
||||
companion object {
|
||||
const val TAG = "Hiddify/VpnService"
|
||||
const val EVENT_TAG = "Hiddify/VpnServiceEvents"
|
||||
private const val TUN2SOCKS = "libtun2socks.so"
|
||||
|
||||
private const val TUN_MTU = 9000
|
||||
private const val TUN_GATEWAY = "172.19.0.1"
|
||||
private const val TUN_ROUTER = "172.19.0.2"
|
||||
private const val TUN_SUBNET_PREFIX = 30
|
||||
private const val NET_ANY = "0.0.0.0"
|
||||
private val HTTP_PROXY_LOCAL_LIST = listOf(
|
||||
"localhost",
|
||||
"*.local",
|
||||
"127.*",
|
||||
"10.*",
|
||||
"172.16.*",
|
||||
"172.17.*",
|
||||
"172.18.*",
|
||||
"172.19.*",
|
||||
"172.2*",
|
||||
"172.30.*",
|
||||
"172.31.*",
|
||||
"192.168.*"
|
||||
)
|
||||
}
|
||||
|
||||
private var vpnBroadcastReceiver: VpnState? = null
|
||||
private var conn: ParcelFileDescriptor? = null
|
||||
private lateinit var process: Process
|
||||
private var isRunning = false
|
||||
|
||||
// prefs
|
||||
private var includeAppPackages: Set<String> = HashSet()
|
||||
|
||||
fun getService(): Service {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
startVpnService()
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "creating vpn service")
|
||||
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
|
||||
StrictMode.setThreadPolicy(policy)
|
||||
registerBroadcastReceiver()
|
||||
VpnServiceManager.vpnService = SoftReference(this)
|
||||
}
|
||||
|
||||
override fun onRevoke() {
|
||||
Log.d(TAG, "vpn service revoked")
|
||||
super.onRevoke()
|
||||
stopVpnService()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "vpn service destroyed")
|
||||
super.onDestroy()
|
||||
broadcastVpnStatus(false)
|
||||
VpnServiceManager.cancelNotification()
|
||||
unregisterBroadcastReceiver()
|
||||
}
|
||||
|
||||
private fun registerBroadcastReceiver() {
|
||||
Log.d(TAG, "registering receiver in service")
|
||||
vpnBroadcastReceiver = VpnState()
|
||||
val intentFilter = IntentFilter(VpnState.ACTION_VPN_STATUS)
|
||||
registerReceiver(vpnBroadcastReceiver, intentFilter)
|
||||
}
|
||||
|
||||
private fun unregisterBroadcastReceiver() {
|
||||
Log.d(TAG, "unregistering receiver in service")
|
||||
if (vpnBroadcastReceiver != null) {
|
||||
unregisterReceiver(vpnBroadcastReceiver)
|
||||
vpnBroadcastReceiver = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastVpnStatus(isVpnActive: Boolean) {
|
||||
Log.d(TAG, "broadcasting status= $isVpnActive")
|
||||
val intent = Intent(VpnState.ACTION_VPN_STATUS)
|
||||
intent.putExtra(VpnState.IS_VPN_ACTIVE, isVpnActive)
|
||||
sendBroadcast(intent)
|
||||
}
|
||||
|
||||
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
||||
private val defaultNetworkRequest by lazy {
|
||||
NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val connectivity by lazy { getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
|
||||
|
||||
@delegate:RequiresApi(Build.VERSION_CODES.P)
|
||||
private val defaultNetworkCallback by lazy {
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
setUnderlyingNetworks(arrayOf(network))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
// it's a good idea to refresh capabilities
|
||||
setUnderlyingNetworks(arrayOf(network))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
setUnderlyingNetworks(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startVpnService() {
|
||||
val prepare = prepare(this)
|
||||
if (prepare != null) {
|
||||
return
|
||||
}
|
||||
|
||||
with(Builder()) {
|
||||
addAddress(TUN_GATEWAY, TUN_SUBNET_PREFIX)
|
||||
setMtu(TUN_MTU)
|
||||
addRoute(NET_ANY, 0)
|
||||
addDnsServer(TUN_ROUTER)
|
||||
allowBypass()
|
||||
setBlocking(true)
|
||||
setSession("Hiddify")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
setMetered(false)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && VpnServiceManager.prefs.systemProxy) {
|
||||
setHttpProxy(
|
||||
ProxyInfo.buildDirectProxy(
|
||||
"127.0.0.1",
|
||||
VpnServiceManager.prefs.httpPort,
|
||||
HTTP_PROXY_LOCAL_LIST,
|
||||
)
|
||||
)
|
||||
}
|
||||
if (includeAppPackages.isEmpty()) {
|
||||
addDisallowedApplication(packageName)
|
||||
} else {
|
||||
includeAppPackages.forEach {
|
||||
addAllowedApplication(it)
|
||||
}
|
||||
}
|
||||
setConfigureIntent(
|
||||
PendingIntent.getActivity(
|
||||
this@HiddifyVpnService,
|
||||
0,
|
||||
Intent().setComponent(ComponentName(packageName, "$packageName.MainActivity")),
|
||||
pendingIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
conn?.close()
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
try {
|
||||
connectivity.requestNetwork(defaultNetworkRequest, defaultNetworkCallback)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
try {
|
||||
conn = establish()
|
||||
isRunning = true
|
||||
runTun2socks()
|
||||
VpnServiceManager.showNotification()
|
||||
Log.d(TAG, "vpn connection established")
|
||||
broadcastVpnStatus(true)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "failed to start vpn service: $e")
|
||||
e.printStackTrace()
|
||||
stopVpnService()
|
||||
broadcastVpnStatus(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopVpnService(isForced: Boolean = true) {
|
||||
Log.d(TAG, "stopping vpn service, forced: [$isForced]")
|
||||
isRunning = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
try {
|
||||
connectivity.unregisterNetworkCallback(defaultNetworkCallback)
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Log.d(TAG, "destroying tun2socks")
|
||||
process.destroy()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.toString())
|
||||
}
|
||||
|
||||
if(isForced) {
|
||||
stopSelf()
|
||||
try {
|
||||
conn?.close()
|
||||
conn = null
|
||||
} catch (ignored: Exception) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "vpn service stopped")
|
||||
}
|
||||
|
||||
private fun runTun2socks() {
|
||||
val cmd = arrayListOf(
|
||||
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
|
||||
"--netif-ipaddr", TUN_ROUTER,
|
||||
"--netif-netmask", "255.255.255.252",
|
||||
"--socks-server-addr", "127.0.0.1:${VpnServiceManager.prefs.socksPort}",
|
||||
"--tunmtu", TUN_MTU.toString(),
|
||||
"--sock-path", "sock_path",//File(applicationContext.filesDir, "sock_path").absolutePath,
|
||||
"--enable-udprelay",
|
||||
"--loglevel", "notice")
|
||||
|
||||
Log.d(TAG, cmd.toString())
|
||||
protect(conn!!.fd) // not sure
|
||||
|
||||
try {
|
||||
val proBuilder = ProcessBuilder(cmd)
|
||||
proBuilder.redirectErrorStream(true)
|
||||
process = proBuilder
|
||||
.directory(applicationContext.filesDir)
|
||||
.start()
|
||||
Thread(Runnable {
|
||||
Log.d(TAG,"$TUN2SOCKS check")
|
||||
process.waitFor()
|
||||
Log.d(TAG,"$TUN2SOCKS exited")
|
||||
if (isRunning) {
|
||||
Log.d(packageName,"$TUN2SOCKS restart")
|
||||
runTun2socks()
|
||||
}
|
||||
}).start()
|
||||
Log.d(TAG, process.toString())
|
||||
|
||||
sendFd()
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendFd() {
|
||||
val fd = conn!!.fileDescriptor
|
||||
val path = File(applicationContext.filesDir, "sock_path").absolutePath
|
||||
Log.d(TAG, path)
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
var tries = 0
|
||||
while (true) try {
|
||||
Thread.sleep(50L shl tries)
|
||||
Log.d(TAG, "sendFd tries: $tries")
|
||||
LocalSocket().use { localSocket ->
|
||||
localSocket.connect(LocalSocketAddress(path, LocalSocketAddress.Namespace.FILESYSTEM))
|
||||
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
||||
localSocket.outputStream.write(42)
|
||||
}
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, e.toString())
|
||||
if (tries > 5) break
|
||||
tries += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pendingIntentFlags(flags: Int, mutable: Boolean = false): Int {
|
||||
return if (Build.VERSION.SDK_INT >= 24) {
|
||||
if (Build.VERSION.SDK_INT > 30 && mutable) {
|
||||
flags or PendingIntent.FLAG_MUTABLE
|
||||
} else {
|
||||
flags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
} else {
|
||||
flags
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import androidx.annotation.NonNull
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
|
||||
|
||||
class LogHandler : FlutterPlugin {
|
||||
|
||||
companion object {
|
||||
const val TAG = "A/LogHandler"
|
||||
const val SERVICE_LOGS = "com.hiddify.app/service.logs"
|
||||
}
|
||||
|
||||
private lateinit var logsChannel: EventChannel
|
||||
|
||||
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
logsChannel = EventChannel(flutterPluginBinding.binaryMessenger, SERVICE_LOGS)
|
||||
|
||||
logsChannel.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
MainActivity.instance.serviceLogs.observeForever { it ->
|
||||
if (it == null) return@observeForever
|
||||
events?.success(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
}
|
||||
}
|
||||
@@ -1,116 +1,148 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.VpnService
|
||||
import android.util.Log
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.hiddify.hiddify.bg.ServiceConnection
|
||||
import com.hiddify.hiddify.bg.ServiceNotification
|
||||
import com.hiddify.hiddify.bg.VPNService
|
||||
import com.hiddify.hiddify.constant.Alert
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity(), MethodChannel.MethodCallHandler {
|
||||
private lateinit var methodChannel: MethodChannel
|
||||
private lateinit var eventChannel: EventChannel
|
||||
private lateinit var methodResult: MethodChannel.Result
|
||||
private var vpnBroadcastReceiver: VpnState? = null
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.LinkedList
|
||||
|
||||
class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback {
|
||||
companion object {
|
||||
private const val TAG = "ANDROID/MyActivity"
|
||||
lateinit var instance: MainActivity
|
||||
|
||||
const val VPN_PERMISSION_REQUEST_CODE = 1001
|
||||
|
||||
enum class Action(val method: String) {
|
||||
GrantPermission("grant_permission"),
|
||||
StartProxy("start"),
|
||||
StopProxy("stop"),
|
||||
RefreshStatus("refresh_status"),
|
||||
SetPrefs("set_prefs")
|
||||
}
|
||||
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1010
|
||||
}
|
||||
|
||||
private fun registerBroadcastReceiver() {
|
||||
Log.d(HiddifyVpnService.TAG, "registering broadcast receiver")
|
||||
vpnBroadcastReceiver = VpnState()
|
||||
val intentFilter = IntentFilter(VpnState.ACTION_VPN_STATUS)
|
||||
registerReceiver(vpnBroadcastReceiver, intentFilter)
|
||||
}
|
||||
private val connection = ServiceConnection(this, this)
|
||||
|
||||
private fun unregisterBroadcastReceiver() {
|
||||
Log.d(HiddifyVpnService.TAG, "unregistering broadcast receiver")
|
||||
if (vpnBroadcastReceiver != null) {
|
||||
unregisterReceiver(vpnBroadcastReceiver)
|
||||
vpnBroadcastReceiver = null
|
||||
}
|
||||
}
|
||||
val logList = LinkedList<String>()
|
||||
var logCallback: ((Boolean) -> Unit)? = null
|
||||
val serviceStatus = MutableLiveData(Status.Stopped)
|
||||
val serviceAlerts = MutableLiveData<ServiceEvent?>(null)
|
||||
val serviceLogs = MutableLiveData<String?>(null)
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
methodChannel =
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, HiddifyVpnService.TAG)
|
||||
methodChannel.setMethodCallHandler(this)
|
||||
|
||||
eventChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, HiddifyVpnService.EVENT_TAG)
|
||||
registerBroadcastReceiver()
|
||||
eventChannel.setStreamHandler(vpnBroadcastReceiver)
|
||||
instance = this
|
||||
reconnect()
|
||||
flutterEngine.plugins.add(MethodHandler())
|
||||
flutterEngine.plugins.add(EventHandler())
|
||||
flutterEngine.plugins.add(LogHandler())
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
methodResult = result
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when (call.method) {
|
||||
Action.GrantPermission.method -> {
|
||||
grantVpnPermission()
|
||||
fun reconnect() {
|
||||
connection.reconnect()
|
||||
}
|
||||
|
||||
fun startService() {
|
||||
if (!ServiceNotification.checkPermission()) {
|
||||
Log.d(TAG, "missing notification permission")
|
||||
return
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
// if (Settings.rebuildServiceMode()) {
|
||||
// reconnect()
|
||||
// }
|
||||
if (prepare()) {
|
||||
Log.d(TAG, "VPN permission required")
|
||||
return@launch
|
||||
}
|
||||
|
||||
Action.StartProxy.method -> {
|
||||
VpnServiceManager.startVpnService(this)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
Action.StopProxy.method -> {
|
||||
VpnServiceManager.stopVpnService()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
Action.RefreshStatus.method -> {
|
||||
val statusIntent = Intent(VpnState.ACTION_VPN_STATUS)
|
||||
statusIntent.putExtra(VpnState.IS_VPN_ACTIVE, VpnServiceManager.isRunning)
|
||||
sendBroadcast(statusIntent)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
Action.SetPrefs.method -> {
|
||||
val args = call.arguments as Map<String, Any>
|
||||
VpnServiceManager.setPrefs(context, args)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
val intent = Intent(Application.application, VPNService::class.java)
|
||||
withContext(Dispatchers.Main) {
|
||||
ContextCompat.startForegroundService(Application.application, intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterBroadcastReceiver()
|
||||
|
||||
override fun onServiceStatusChanged(status: Status) {
|
||||
Log.d(TAG, "service status changed: $status")
|
||||
serviceStatus.postValue(status)
|
||||
}
|
||||
|
||||
private fun grantVpnPermission() {
|
||||
val vpnPermissionIntent = VpnService.prepare(this)
|
||||
if (vpnPermissionIntent == null) {
|
||||
onActivityResult(VPN_PERMISSION_REQUEST_CODE, RESULT_OK, null)
|
||||
} else {
|
||||
startActivityForResult(vpnPermissionIntent, VPN_PERMISSION_REQUEST_CODE)
|
||||
|
||||
override fun onServiceAlert(type: Alert, message: String?) {
|
||||
Log.d(TAG, "service alert: $type")
|
||||
serviceAlerts.postValue(ServiceEvent(Status.Stopped, type, message))
|
||||
}
|
||||
|
||||
private var paused = false
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
paused = true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
paused = false
|
||||
logCallback?.invoke(true)
|
||||
}
|
||||
|
||||
override fun onServiceWriteLog(message: String?) {
|
||||
if (paused) {
|
||||
if (logList.size > 300) {
|
||||
logList.removeFirst()
|
||||
}
|
||||
}
|
||||
logList.addLast(message)
|
||||
if (!paused) {
|
||||
logCallback?.invoke(false)
|
||||
serviceLogs.postValue(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceResetLogs(messages: MutableList<String>) {
|
||||
logList.clear()
|
||||
logList.addAll(messages)
|
||||
if (!paused) logCallback?.invoke(true)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
connection.disconnect()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private suspend fun prepare() = withContext(Dispatchers.Main) {
|
||||
try {
|
||||
val intent = VpnService.prepare(this@MainActivity)
|
||||
if (intent != null) {
|
||||
// prepareLauncher.launch(intent)
|
||||
startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onServiceAlert(Alert.RequestVPNPermission, e.message)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
|
||||
methodResult.success(resultCode == RESULT_OK)
|
||||
} else if (requestCode == 101010) {
|
||||
methodResult.success(resultCode == RESULT_OK)
|
||||
if (resultCode == RESULT_OK) startService()
|
||||
else onServiceAlert(Alert.RequestVPNPermission, null)
|
||||
} else if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode == RESULT_OK) startService()
|
||||
else onServiceAlert(Alert.RequestNotificationPermission, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import androidx.annotation.NonNull
|
||||
import com.hiddify.hiddify.bg.BoxService
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.StandardMethodCodec
|
||||
|
||||
class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
companion object {
|
||||
const val channelName = "com.hiddify.app/method"
|
||||
|
||||
enum class Trigger(val method: String) {
|
||||
ParseConfig("parse_config"),
|
||||
SetActiveConfigPath("set_active_config_path"),
|
||||
Start("start"),
|
||||
Stop("stop"),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
val taskQueue = flutterPluginBinding.binaryMessenger.makeBackgroundTaskQueue()
|
||||
channel = MethodChannel(
|
||||
flutterPluginBinding.binaryMessenger,
|
||||
channelName,
|
||||
StandardMethodCodec.INSTANCE,
|
||||
taskQueue
|
||||
)
|
||||
channel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
Trigger.ParseConfig.method -> {
|
||||
val args = call.arguments as Map<*, *>
|
||||
val path = args["path"] as String? ?: ""
|
||||
val msg = BoxService.parseConfig(path)
|
||||
result.success(msg)
|
||||
}
|
||||
|
||||
Trigger.SetActiveConfigPath.method -> {
|
||||
val args = call.arguments as Map<*, *>
|
||||
Settings.selectedConfigPath = args["path"] as String? ?: ""
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
Trigger.Start.method -> {
|
||||
MainActivity.instance.startService()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
Trigger.Stop.method -> {
|
||||
BoxService.stop()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
33
android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt
Normal file
33
android/app/src/main/kotlin/com/hiddify/hiddify/Settings.kt
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.content.Context
|
||||
import com.hiddify.hiddify.constant.SettingsKey
|
||||
|
||||
object Settings {
|
||||
|
||||
const val PER_APP_PROXY_DISABLED = 0
|
||||
const val PER_APP_PROXY_EXCLUDE = 1
|
||||
const val PER_APP_PROXY_INCLUDE = 2
|
||||
|
||||
private val preferences by lazy {
|
||||
val context = Application.application.applicationContext
|
||||
context.getSharedPreferences("preferences", Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
var disableMemoryLimit = preferences.getBoolean(SettingsKey.DISABLE_MEMORY_LIMIT, false)
|
||||
|
||||
var perAppProxyEnabled = preferences.getBoolean(SettingsKey.PER_APP_PROXY_ENABLED, false)
|
||||
var perAppProxyMode = preferences.getInt(SettingsKey.PER_APP_PROXY_MODE, PER_APP_PROXY_EXCLUDE)
|
||||
var perAppProxyList = preferences.getStringSet(SettingsKey.PER_APP_PROXY_LIST, emptySet())!!
|
||||
var perAppProxyUpdateOnChange =
|
||||
preferences.getInt(SettingsKey.PER_APP_PROXY_UPDATE_ON_CHANGE, PER_APP_PROXY_DISABLED)
|
||||
|
||||
var selectedConfigPath: String
|
||||
get() = preferences.getString(SettingsKey.SELECTED_CONFIG_PATH, "") ?: ""
|
||||
set(value) = preferences.edit().putString(SettingsKey.SELECTED_CONFIG_PATH, value).apply()
|
||||
|
||||
var startedByUser: Boolean
|
||||
get() = preferences.getBoolean(SettingsKey.STARTED_BY_USER, false)
|
||||
set(value) = preferences.edit().putBoolean(SettingsKey.STARTED_BY_USER, value).apply()
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Context.NOTIFICATION_SERVICE
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import java.lang.ref.SoftReference
|
||||
|
||||
data class VpnServiceConfigs(val httpPort: Int = 12346, val socksPort: Int = 12347, val systemProxy: Boolean = true)
|
||||
|
||||
object VpnServiceManager {
|
||||
private const val NOTIFICATION_ID = 1
|
||||
private const val NOTIFICATION_CHANNEL_ID = "hiddify_vpn"
|
||||
private const val NOTIFICATION_CHANNEL_NAME = "Hiddify VPN"
|
||||
|
||||
var vpnService: SoftReference<HiddifyVpnService>? = null
|
||||
var prefs = VpnServiceConfigs()
|
||||
var isRunning = false
|
||||
|
||||
private var mBuilder: NotificationCompat.Builder? = null
|
||||
private var mNotificationManager: NotificationManager? = null
|
||||
|
||||
fun startVpnService(context: Context) {
|
||||
val intent = Intent(context.applicationContext, HiddifyVpnService::class.java)
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopVpnService() {
|
||||
val service = vpnService?.get() ?: return
|
||||
service.stopVpnService()
|
||||
}
|
||||
|
||||
fun setPrefs(context: Context,args: Map<String, Any>) {
|
||||
prefs = prefs.copy(
|
||||
httpPort = args["httpPort"] as Int? ?: prefs.httpPort,
|
||||
socksPort = args["socksPort"] as Int? ?: prefs.socksPort,
|
||||
systemProxy = args["systemProxy"] as Boolean? ?: prefs.systemProxy,
|
||||
)
|
||||
if(isRunning) {
|
||||
stopVpnService()
|
||||
startVpnService(context)
|
||||
}
|
||||
}
|
||||
|
||||
fun showNotification() {
|
||||
val service = vpnService?.get()?.getService() ?: return
|
||||
val channelId = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createNotificationChannel()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
mBuilder = NotificationCompat.Builder(service, channelId)
|
||||
.setSmallIcon(R.drawable.ic_stat_logo)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentTitle("Hiddify")
|
||||
.setContentText("Connected")
|
||||
|
||||
service.startForeground(NOTIFICATION_ID, mBuilder?.build())
|
||||
}
|
||||
|
||||
fun cancelNotification() {
|
||||
val service = vpnService?.get()?.getService() ?: return
|
||||
service.stopForeground(true)
|
||||
mBuilder = null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(): String {
|
||||
val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH)
|
||||
channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||
getNotificationManager()?.createNotificationChannel(
|
||||
channel
|
||||
)
|
||||
return NOTIFICATION_CHANNEL_ID
|
||||
}
|
||||
|
||||
private fun getNotificationManager(): NotificationManager? {
|
||||
if (mNotificationManager == null) {
|
||||
val service = vpnService?.get()?.getService() ?: return null
|
||||
mNotificationManager = service.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
||||
return mNotificationManager
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.hiddify.hiddify
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
|
||||
class VpnState : BroadcastReceiver(), EventChannel.StreamHandler{
|
||||
companion object {
|
||||
const val ACTION_VPN_STATUS = "Hiddify.VpnState.ACTION_VPN_STATUS"
|
||||
const val IS_VPN_ACTIVE = "isVpnActive"
|
||||
}
|
||||
|
||||
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
eventSink = events
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
eventSink = null
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == ACTION_VPN_STATUS) {
|
||||
val isVpnActive = intent.getBooleanExtra(IS_VPN_ACTIVE, false)
|
||||
Log.d(HiddifyVpnService.TAG, "send to flutter: status= $isVpnActive")
|
||||
VpnServiceManager.isRunning = isVpnActive
|
||||
eventSink?.success(isVpnActive)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.hiddify.hiddify.Settings
|
||||
|
||||
class AppChangeReceiver : BroadcastReceiver() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "A/AppChangeReceiver"
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
checkUpdate(context, intent)
|
||||
}
|
||||
|
||||
private fun checkUpdate(context: Context, intent: Intent) {
|
||||
if (!Settings.perAppProxyEnabled) {
|
||||
return
|
||||
}
|
||||
val perAppProxyUpdateOnChange = Settings.perAppProxyUpdateOnChange
|
||||
if (perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_DISABLED) {
|
||||
return
|
||||
}
|
||||
val packageName = intent.dataString?.substringAfter("package:")
|
||||
if (packageName.isNullOrBlank()) {
|
||||
return
|
||||
}
|
||||
if ((perAppProxyUpdateOnChange == Settings.PER_APP_PROXY_INCLUDE)) {
|
||||
Settings.perAppProxyList = Settings.perAppProxyList + packageName
|
||||
} else {
|
||||
Settings.perAppProxyList = Settings.perAppProxyList - packageName
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
291
android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt
Normal file
291
android/app/src/main/kotlin/com/hiddify/hiddify/bg/BoxService.kt
Normal file
@@ -0,0 +1,291 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.IBinder
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.hiddify.hiddify.Application
|
||||
import com.hiddify.hiddify.Settings
|
||||
import com.hiddify.hiddify.constant.Action
|
||||
import com.hiddify.hiddify.constant.Alert
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import go.Seq
|
||||
import io.nekohasekai.libbox.BoxService
|
||||
import io.nekohasekai.libbox.CommandServer
|
||||
import io.nekohasekai.libbox.CommandServerHandler
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.libbox.PProfServer
|
||||
import io.nekohasekai.libbox.PlatformInterface
|
||||
import io.nekohasekai.mobile.Mobile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class BoxService(
|
||||
private val service: Service,
|
||||
private val platformInterface: PlatformInterface
|
||||
) : CommandServerHandler {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "A/BoxService"
|
||||
|
||||
private var initializeOnce = false
|
||||
private fun initialize() {
|
||||
if (initializeOnce) return
|
||||
val baseDir = Application.application.filesDir
|
||||
baseDir.mkdirs()
|
||||
val workingDir = Application.application.getExternalFilesDir(null) ?: return
|
||||
workingDir.mkdirs()
|
||||
val tempDir = Application.application.cacheDir
|
||||
tempDir.mkdirs()
|
||||
Log.d(TAG, "base dir: ${baseDir.path}")
|
||||
Log.d(TAG, "working dir: ${workingDir.path}")
|
||||
Log.d(TAG, "temp dir: ${tempDir.path}")
|
||||
Libbox.setup(baseDir.path, workingDir.path, tempDir.path, false)
|
||||
Libbox.redirectStderr(File(workingDir, "stderr.log").path)
|
||||
initializeOnce = true
|
||||
return
|
||||
}
|
||||
|
||||
fun parseConfig(path: String): String {
|
||||
return try {
|
||||
Mobile.parse(path)
|
||||
""
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
e.message ?: "invalid config"
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
val intent = runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
Intent(Application.application, VPNService::class.java)
|
||||
}
|
||||
}
|
||||
ContextCompat.startForegroundService(Application.application, intent)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
Application.application.sendBroadcast(
|
||||
Intent(Action.SERVICE_CLOSE).setPackage(
|
||||
Application.application.packageName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
Application.application.sendBroadcast(
|
||||
Intent(Action.SERVICE_RELOAD).setPackage(
|
||||
Application.application.packageName
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var fileDescriptor: ParcelFileDescriptor? = null
|
||||
|
||||
private val status = MutableLiveData(Status.Stopped)
|
||||
private val binder = ServiceBinder(status)
|
||||
private val notification = ServiceNotification(service)
|
||||
private var boxService: BoxService? = null
|
||||
private var commandServer: CommandServer? = null
|
||||
private var pprofServer: PProfServer? = null
|
||||
private var receiverRegistered = false
|
||||
private val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
Action.SERVICE_CLOSE -> {
|
||||
stopService()
|
||||
}
|
||||
|
||||
Action.SERVICE_RELOAD -> {
|
||||
serviceReload()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCommandServer() {
|
||||
val commandServer =
|
||||
CommandServer(this, 300)
|
||||
commandServer.start()
|
||||
this.commandServer = commandServer
|
||||
}
|
||||
|
||||
private suspend fun startService() {
|
||||
try {
|
||||
Log.d(TAG, "starting service")
|
||||
|
||||
val selectedConfigPath = Settings.selectedConfigPath
|
||||
if (selectedConfigPath.isBlank()) {
|
||||
stopAndAlert(Alert.EmptyConfiguration)
|
||||
return
|
||||
}
|
||||
|
||||
val content = try {
|
||||
Mobile.applyOverrides(selectedConfigPath)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
stopAndAlert(Alert.EmptyConfiguration)
|
||||
return
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
binder.broadcast {
|
||||
it.onServiceResetLogs(listOf())
|
||||
}
|
||||
}
|
||||
|
||||
DefaultNetworkMonitor.start()
|
||||
Libbox.registerLocalDNSTransport(LocalResolver)
|
||||
Libbox.setMemoryLimit(!Settings.disableMemoryLimit)
|
||||
|
||||
val newService = try {
|
||||
Libbox.newService(content, platformInterface)
|
||||
} catch (e: Exception) {
|
||||
stopAndAlert(Alert.CreateService, e.message)
|
||||
return
|
||||
}
|
||||
|
||||
newService.start()
|
||||
boxService = newService
|
||||
commandServer?.setService(boxService)
|
||||
status.postValue(Status.Started)
|
||||
} catch (e: Exception) {
|
||||
stopAndAlert(Alert.StartService, e.message)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
override fun serviceReload() {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val pfd = fileDescriptor
|
||||
if (pfd != null) {
|
||||
pfd.close()
|
||||
fileDescriptor = null
|
||||
}
|
||||
commandServer?.setService(null)
|
||||
boxService?.apply {
|
||||
runCatching {
|
||||
close()
|
||||
}.onFailure {
|
||||
writeLog("service: error when closing: $it")
|
||||
}
|
||||
Seq.destroyRef(refnum)
|
||||
}
|
||||
boxService = null
|
||||
startService()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopService() {
|
||||
if (status.value != Status.Started) return
|
||||
status.value = Status.Stopping
|
||||
if (receiverRegistered) {
|
||||
service.unregisterReceiver(receiver)
|
||||
receiverRegistered = false
|
||||
}
|
||||
notification.close()
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val pfd = fileDescriptor
|
||||
if (pfd != null) {
|
||||
pfd.close()
|
||||
fileDescriptor = null
|
||||
}
|
||||
commandServer?.setService(null)
|
||||
boxService?.apply {
|
||||
runCatching {
|
||||
close()
|
||||
}.onFailure {
|
||||
writeLog("service: error when closing: $it")
|
||||
}
|
||||
Seq.destroyRef(refnum)
|
||||
}
|
||||
boxService = null
|
||||
Libbox.registerLocalDNSTransport(null)
|
||||
DefaultNetworkMonitor.stop()
|
||||
|
||||
commandServer?.apply {
|
||||
close()
|
||||
Seq.destroyRef(refnum)
|
||||
}
|
||||
commandServer = null
|
||||
Settings.startedByUser = false
|
||||
withContext(Dispatchers.Main) {
|
||||
status.value = Status.Stopped
|
||||
service.stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun stopAndAlert(type: Alert, message: String? = null) {
|
||||
Settings.startedByUser = false
|
||||
withContext(Dispatchers.Main) {
|
||||
if (receiverRegistered) {
|
||||
service.unregisterReceiver(receiver)
|
||||
receiverRegistered = false
|
||||
}
|
||||
notification.close()
|
||||
binder.broadcast { callback ->
|
||||
callback.onServiceAlert(type.ordinal, message)
|
||||
}
|
||||
status.value = Status.Stopped
|
||||
}
|
||||
}
|
||||
|
||||
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (status.value != Status.Stopped) return Service.START_NOT_STICKY
|
||||
status.value = Status.Starting
|
||||
|
||||
if (!receiverRegistered) {
|
||||
ContextCompat.registerReceiver(service, receiver, IntentFilter().apply {
|
||||
addAction(Action.SERVICE_CLOSE)
|
||||
addAction(Action.SERVICE_RELOAD)
|
||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
receiverRegistered = true
|
||||
}
|
||||
|
||||
notification.show()
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
Settings.startedByUser = true
|
||||
initialize()
|
||||
try {
|
||||
startCommandServer()
|
||||
} catch (e: Exception) {
|
||||
stopAndAlert(Alert.StartCommandServer, e.message)
|
||||
return@launch
|
||||
}
|
||||
startService()
|
||||
}
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
binder.close()
|
||||
}
|
||||
|
||||
fun onRevoke() {
|
||||
stopService()
|
||||
}
|
||||
|
||||
fun writeLog(message: String) {
|
||||
binder.broadcast {
|
||||
it.onServiceWriteLog(message)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.hiddify.hiddify.Application
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.actor
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.net.UnknownHostException
|
||||
|
||||
object DefaultNetworkListener {
|
||||
private sealed class NetworkMessage {
|
||||
class Start(val key: Any, val listener: (Network?) -> Unit) : NetworkMessage()
|
||||
class Get : NetworkMessage() {
|
||||
val response = CompletableDeferred<Network>()
|
||||
}
|
||||
|
||||
class Stop(val key: Any) : NetworkMessage()
|
||||
|
||||
class Put(val network: Network) : NetworkMessage()
|
||||
class Update(val network: Network) : NetworkMessage()
|
||||
class Lost(val network: Network) : NetworkMessage()
|
||||
}
|
||||
|
||||
private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) {
|
||||
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
|
||||
var network: Network? = null
|
||||
val pendingRequests = arrayListOf<NetworkMessage.Get>()
|
||||
for (message in channel) when (message) {
|
||||
is NetworkMessage.Start -> {
|
||||
if (listeners.isEmpty()) register()
|
||||
listeners[message.key] = message.listener
|
||||
if (network != null) message.listener(network)
|
||||
}
|
||||
|
||||
is NetworkMessage.Get -> {
|
||||
check(listeners.isNotEmpty()) { "Getting network without any listeners is not supported" }
|
||||
if (network == null) pendingRequests += message else message.response.complete(
|
||||
network
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkMessage.Stop -> if (listeners.isNotEmpty() && // was not empty
|
||||
listeners.remove(message.key) != null && listeners.isEmpty()
|
||||
) {
|
||||
network = null
|
||||
unregister()
|
||||
}
|
||||
|
||||
is NetworkMessage.Put -> {
|
||||
network = message.network
|
||||
pendingRequests.forEach { it.response.complete(message.network) }
|
||||
pendingRequests.clear()
|
||||
listeners.values.forEach { it(network) }
|
||||
}
|
||||
|
||||
is NetworkMessage.Update -> if (network == message.network) listeners.values.forEach {
|
||||
it(
|
||||
network
|
||||
)
|
||||
}
|
||||
|
||||
is NetworkMessage.Lost -> if (network == message.network) {
|
||||
network = null
|
||||
listeners.values.forEach { it(null) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun start(key: Any, listener: (Network?) -> Unit) = networkActor.send(
|
||||
NetworkMessage.Start(
|
||||
key,
|
||||
listener
|
||||
)
|
||||
)
|
||||
|
||||
suspend fun get() = if (fallback) @TargetApi(23) {
|
||||
Application.connectivity.activeNetwork
|
||||
?: throw UnknownHostException() // failed to listen, return current if available
|
||||
} else NetworkMessage.Get().run {
|
||||
networkActor.send(this)
|
||||
response.await()
|
||||
}
|
||||
|
||||
suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key))
|
||||
|
||||
// NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26
|
||||
private object Callback : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) = runBlocking {
|
||||
networkActor.send(
|
||||
NetworkMessage.Put(
|
||||
network
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
// it's a good idea to refresh capabilities
|
||||
runBlocking { networkActor.send(NetworkMessage.Update(network)) }
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) = runBlocking {
|
||||
networkActor.send(
|
||||
NetworkMessage.Lost(
|
||||
network
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var fallback = false
|
||||
private val request = NetworkRequest.Builder().apply {
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
if (Build.VERSION.SDK_INT == 23) { // workarounds for OEM bugs
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)
|
||||
}
|
||||
}.build()
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
/**
|
||||
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||
*
|
||||
* This makes doing a requestNetwork with REQUEST necessary so that we don't get ALL possible networks that
|
||||
* satisfies default network capabilities but only THE default network. Unfortunately, we need to have
|
||||
* android.permission.CHANGE_NETWORK_STATE to be able to call requestNetwork.
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
|
||||
*/
|
||||
private fun register() {
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
in 31..Int.MAX_VALUE -> @TargetApi(31) {
|
||||
Application.connectivity.registerBestMatchingNetworkCallback(
|
||||
request,
|
||||
Callback,
|
||||
mainHandler
|
||||
)
|
||||
}
|
||||
|
||||
in 28 until 31 -> @TargetApi(28) { // we want REQUEST here instead of LISTEN
|
||||
Application.connectivity.requestNetwork(request, Callback, mainHandler)
|
||||
}
|
||||
|
||||
in 26 until 28 -> @TargetApi(26) {
|
||||
Application.connectivity.registerDefaultNetworkCallback(Callback, mainHandler)
|
||||
}
|
||||
|
||||
in 24 until 26 -> @TargetApi(24) {
|
||||
Application.connectivity.registerDefaultNetworkCallback(Callback)
|
||||
}
|
||||
|
||||
else -> try {
|
||||
fallback = false
|
||||
Application.connectivity.requestNetwork(request, Callback)
|
||||
} catch (e: RuntimeException) {
|
||||
fallback =
|
||||
true // known bug on API 23: https://stackoverflow.com/a/33509180/2245107
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregister() = Application.connectivity.unregisterNetworkCallback(Callback)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.net.Network
|
||||
import android.os.Build
|
||||
import com.hiddify.hiddify.Application
|
||||
import io.nekohasekai.libbox.InterfaceUpdateListener
|
||||
|
||||
object DefaultNetworkMonitor {
|
||||
|
||||
var defaultNetwork: Network? = null
|
||||
private var listener: InterfaceUpdateListener? = null
|
||||
|
||||
suspend fun start() {
|
||||
DefaultNetworkListener.start(this) {
|
||||
defaultNetwork = it
|
||||
checkDefaultInterfaceUpdate(it)
|
||||
}
|
||||
defaultNetwork = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Application.connectivity.activeNetwork
|
||||
} else {
|
||||
DefaultNetworkListener.get()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stop() {
|
||||
DefaultNetworkListener.stop(this)
|
||||
}
|
||||
|
||||
fun setListener(listener: InterfaceUpdateListener?) {
|
||||
this.listener = listener
|
||||
checkDefaultInterfaceUpdate(defaultNetwork)
|
||||
}
|
||||
|
||||
private fun checkDefaultInterfaceUpdate(
|
||||
newNetwork: Network?
|
||||
) {
|
||||
val listener = listener ?: return
|
||||
val link = Application.connectivity.getLinkProperties(newNetwork ?: return) ?: return
|
||||
listener.updateDefaultInterface(link.interfaceName, -1)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.net.DnsResolver
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.system.ErrnoException
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.hiddify.hiddify.ktx.tryResumeWithException
|
||||
import io.nekohasekai.libbox.ExchangeContext
|
||||
import io.nekohasekai.libbox.LocalDNSTransport
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.net.InetAddress
|
||||
import java.net.UnknownHostException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
object LocalResolver : LocalDNSTransport {
|
||||
|
||||
private const val RCODE_NXDOMAIN = 3
|
||||
|
||||
override fun raw(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
override fun exchange(ctx: ExchangeContext, message: ByteArray) {
|
||||
return runBlocking {
|
||||
suspendCoroutine { continuation ->
|
||||
val signal = CancellationSignal()
|
||||
ctx.onCancel(signal::cancel)
|
||||
val callback = object : DnsResolver.Callback<ByteArray> {
|
||||
override fun onAnswer(answer: ByteArray, rcode: Int) {
|
||||
if (rcode == 0) {
|
||||
ctx.rawSuccess(answer)
|
||||
} else {
|
||||
ctx.errorCode(rcode)
|
||||
}
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
|
||||
override fun onError(error: DnsResolver.DnsException) {
|
||||
when (val cause = error.cause) {
|
||||
is ErrnoException -> {
|
||||
ctx.errnoCode(cause.errno)
|
||||
continuation.resume(Unit)
|
||||
return
|
||||
}
|
||||
}
|
||||
continuation.tryResumeWithException(error)
|
||||
}
|
||||
}
|
||||
DnsResolver.getInstance().rawQuery(
|
||||
DefaultNetworkMonitor.defaultNetwork,
|
||||
message,
|
||||
DnsResolver.FLAG_NO_RETRY,
|
||||
Dispatchers.IO.asExecutor(),
|
||||
signal,
|
||||
callback
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun lookup(ctx: ExchangeContext, network: String, domain: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
return runBlocking {
|
||||
suspendCoroutine { continuation ->
|
||||
val signal = CancellationSignal()
|
||||
ctx.onCancel(signal::cancel)
|
||||
val callback = object : DnsResolver.Callback<Collection<InetAddress>> {
|
||||
@Suppress("ThrowableNotThrown")
|
||||
override fun onAnswer(answer: Collection<InetAddress>, rcode: Int) {
|
||||
if (rcode == 0) {
|
||||
ctx.success((answer as Collection<InetAddress?>).mapNotNull { it?.hostAddress }
|
||||
.joinToString("\n"))
|
||||
} else {
|
||||
ctx.errorCode(rcode)
|
||||
}
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
|
||||
override fun onError(error: DnsResolver.DnsException) {
|
||||
when (val cause = error.cause) {
|
||||
is ErrnoException -> {
|
||||
ctx.errnoCode(cause.errno)
|
||||
continuation.resume(Unit)
|
||||
return
|
||||
}
|
||||
}
|
||||
continuation.tryResumeWithException(error)
|
||||
}
|
||||
}
|
||||
val type = when {
|
||||
network.endsWith("4") -> DnsResolver.TYPE_A
|
||||
network.endsWith("6") -> DnsResolver.TYPE_AAAA
|
||||
else -> null
|
||||
}
|
||||
if (type != null) {
|
||||
DnsResolver.getInstance().query(
|
||||
DefaultNetworkMonitor.defaultNetwork,
|
||||
domain,
|
||||
type,
|
||||
DnsResolver.FLAG_NO_RETRY,
|
||||
Dispatchers.IO.asExecutor(),
|
||||
signal,
|
||||
callback
|
||||
)
|
||||
} else {
|
||||
DnsResolver.getInstance().query(
|
||||
DefaultNetworkMonitor.defaultNetwork,
|
||||
domain,
|
||||
DnsResolver.FLAG_NO_RETRY,
|
||||
Dispatchers.IO.asExecutor(),
|
||||
signal,
|
||||
callback
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val underlyingNetwork =
|
||||
DefaultNetworkMonitor.defaultNetwork ?: error("upstream network not found")
|
||||
val answer = try {
|
||||
underlyingNetwork.getAllByName(domain)
|
||||
} catch (e: UnknownHostException) {
|
||||
ctx.errorCode(RCODE_NXDOMAIN)
|
||||
return
|
||||
}
|
||||
ctx.success(answer.mapNotNull { it.hostAddress }.joinToString("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.hiddify.hiddify.Application
|
||||
import io.nekohasekai.libbox.InterfaceUpdateListener
|
||||
import io.nekohasekai.libbox.NetworkInterfaceIterator
|
||||
import io.nekohasekai.libbox.PlatformInterface
|
||||
import io.nekohasekai.libbox.StringIterator
|
||||
import io.nekohasekai.libbox.TunOptions
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.InterfaceAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.util.Enumeration
|
||||
import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface
|
||||
|
||||
interface PlatformInterfaceWrapper : PlatformInterface {
|
||||
|
||||
override fun usePlatformAutoDetectInterfaceControl(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun autoDetectInterfaceControl(fd: Int) {
|
||||
}
|
||||
|
||||
override fun openTun(options: TunOptions): Int {
|
||||
error("invalid argument")
|
||||
}
|
||||
|
||||
override fun useProcFS(): Boolean {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
override fun findConnectionOwner(
|
||||
ipProtocol: Int,
|
||||
sourceAddress: String,
|
||||
sourcePort: Int,
|
||||
destinationAddress: String,
|
||||
destinationPort: Int
|
||||
): Int {
|
||||
val uid = Application.connectivity.getConnectionOwnerUid(
|
||||
ipProtocol,
|
||||
InetSocketAddress(sourceAddress, sourcePort),
|
||||
InetSocketAddress(destinationAddress, destinationPort)
|
||||
)
|
||||
if (uid == Process.INVALID_UID) error("android: connection owner not found")
|
||||
return uid
|
||||
}
|
||||
|
||||
override fun packageNameByUid(uid: Int): String {
|
||||
val packages = Application.packageManager.getPackagesForUid(uid)
|
||||
if (packages.isNullOrEmpty()) error("android: package not found")
|
||||
return packages[0]
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun uidByPackageName(packageName: String): Int {
|
||||
return try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Application.packageManager.getPackageUid(
|
||||
packageName, PackageManager.PackageInfoFlags.of(0)
|
||||
)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
Application.packageManager.getPackageUid(packageName, 0)
|
||||
} else {
|
||||
Application.packageManager.getApplicationInfo(packageName, 0).uid
|
||||
}
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
error("android: package not found")
|
||||
}
|
||||
}
|
||||
|
||||
override fun usePlatformDefaultInterfaceMonitor(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun startDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
|
||||
DefaultNetworkMonitor.setListener(listener)
|
||||
}
|
||||
|
||||
override fun closeDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
|
||||
DefaultNetworkMonitor.setListener(null)
|
||||
}
|
||||
|
||||
override fun usePlatformInterfaceGetter(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
}
|
||||
|
||||
override fun getInterfaces(): NetworkInterfaceIterator {
|
||||
return InterfaceArray(NetworkInterface.getNetworkInterfaces())
|
||||
}
|
||||
|
||||
override fun underNetworkExtension(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
private class InterfaceArray(private val iterator: Enumeration<NetworkInterface>) :
|
||||
NetworkInterfaceIterator {
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return iterator.hasMoreElements()
|
||||
}
|
||||
|
||||
override fun next(): LibboxNetworkInterface {
|
||||
val element = iterator.nextElement()
|
||||
return LibboxNetworkInterface().apply {
|
||||
name = element.name
|
||||
index = element.index
|
||||
runCatching {
|
||||
mtu = element.mtu
|
||||
}
|
||||
addresses =
|
||||
StringArray(
|
||||
element.interfaceAddresses.mapTo(mutableListOf()) { it.toPrefix() }
|
||||
.iterator()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun InterfaceAddress.toPrefix(): String {
|
||||
return if (address is Inet6Address) {
|
||||
"${Inet6Address.getByAddress(address.address).hostAddress}/${networkPrefixLength}"
|
||||
} else {
|
||||
"${address.hostAddress}/${networkPrefixLength}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class StringArray(private val iterator: Iterator<String>) : StringIterator {
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return iterator.hasNext()
|
||||
}
|
||||
|
||||
override fun next(): String {
|
||||
return iterator.next()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.os.RemoteCallbackList
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.hiddify.hiddify.IService
|
||||
import com.hiddify.hiddify.IServiceCallback
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class ServiceBinder(private val status: MutableLiveData<Status>) : IService.Stub() {
|
||||
private val callbacks = RemoteCallbackList<IServiceCallback>()
|
||||
private val broadcastLock = Mutex()
|
||||
|
||||
init {
|
||||
status.observeForever {
|
||||
broadcast { callback ->
|
||||
callback.onServiceStatusChanged(it.ordinal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun broadcast(work: (IServiceCallback) -> Unit) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
broadcastLock.withLock {
|
||||
val count = callbacks.beginBroadcast()
|
||||
try {
|
||||
repeat(count) {
|
||||
try {
|
||||
work(callbacks.getBroadcastItem(it))
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStatus(): Int {
|
||||
return (status.value ?: Status.Stopped).ordinal
|
||||
}
|
||||
|
||||
override fun registerCallback(callback: IServiceCallback) {
|
||||
callbacks.register(callback)
|
||||
}
|
||||
|
||||
override fun unregisterCallback(callback: IServiceCallback?) {
|
||||
callbacks.unregister(callback)
|
||||
}
|
||||
|
||||
fun close() {
|
||||
callbacks.kill()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import com.hiddify.hiddify.IService
|
||||
import com.hiddify.hiddify.IServiceCallback
|
||||
import com.hiddify.hiddify.Settings
|
||||
import com.hiddify.hiddify.constant.Action
|
||||
import com.hiddify.hiddify.constant.Alert
|
||||
import com.hiddify.hiddify.constant.Status
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ServiceConnection(
|
||||
private val context: Context,
|
||||
callback: Callback,
|
||||
private val register: Boolean = true,
|
||||
) : ServiceConnection {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ServiceConnection"
|
||||
}
|
||||
|
||||
private val callback = ServiceCallback(callback)
|
||||
private var service: IService? = null
|
||||
|
||||
val status get() = service?.status?.let { Status.values()[it] } ?: Status.Stopped
|
||||
|
||||
fun connect() {
|
||||
val intent = runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
Intent(context, VPNService::class.java).setAction(Action.SERVICE)
|
||||
}
|
||||
}
|
||||
context.bindService(intent, this, AppCompatActivity.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
try {
|
||||
context.unbindService(this)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun reconnect() {
|
||||
try {
|
||||
context.unbindService(this)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
val intent = runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
Intent(context, VPNService::class.java).setAction(Action.SERVICE)
|
||||
}
|
||||
}
|
||||
context.bindService(intent, this, AppCompatActivity.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
||||
val service = IService.Stub.asInterface(binder)
|
||||
this.service = service
|
||||
try {
|
||||
if (register) service.registerCallback(callback)
|
||||
callback.onServiceStatusChanged(service.status)
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "initialize service connection", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
try {
|
||||
service?.unregisterCallback(callback)
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "cleanup service connection", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindingDied(name: ComponentName?) {
|
||||
reconnect()
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onServiceStatusChanged(status: Status)
|
||||
fun onServiceAlert(type: Alert, message: String?) {}
|
||||
fun onServiceWriteLog(message: String?) {}
|
||||
fun onServiceResetLogs(messages: MutableList<String>) {}
|
||||
}
|
||||
|
||||
class ServiceCallback(private val callback: Callback) : IServiceCallback.Stub() {
|
||||
override fun onServiceStatusChanged(status: Int) {
|
||||
callback.onServiceStatusChanged(Status.values()[status])
|
||||
}
|
||||
|
||||
override fun onServiceAlert(type: Int, message: String?) {
|
||||
callback.onServiceAlert(Alert.values()[type], message)
|
||||
}
|
||||
|
||||
override fun onServiceWriteLog(message: String?) = callback.onServiceWriteLog(message)
|
||||
|
||||
override fun onServiceResetLogs(messages: MutableList<String>) =
|
||||
callback.onServiceResetLogs(messages)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import com.hiddify.hiddify.Application
|
||||
import com.hiddify.hiddify.MainActivity
|
||||
import com.hiddify.hiddify.R
|
||||
import com.hiddify.hiddify.constant.Action
|
||||
|
||||
class ServiceNotification(private val service: Service) {
|
||||
companion object {
|
||||
private const val notificationId = 1
|
||||
private const val notificationChannel = "service"
|
||||
private val flags =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
||||
|
||||
fun checkPermission(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
return true
|
||||
}
|
||||
if (Application.notification.areNotificationsEnabled()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val notification by lazy {
|
||||
NotificationCompat.Builder(service, notificationChannel).setWhen(0)
|
||||
.setContentTitle("hiddify next")
|
||||
.setContentText("service running").setOnlyAlertOnce(true)
|
||||
.setSmallIcon(R.drawable.ic_stat_logo)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
service,
|
||||
0,
|
||||
Intent(
|
||||
service,
|
||||
MainActivity::class.java
|
||||
).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
|
||||
flags
|
||||
)
|
||||
)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW).apply {
|
||||
addAction(
|
||||
NotificationCompat.Action.Builder(
|
||||
0, service.getText(R.string.stop), PendingIntent.getBroadcast(
|
||||
service,
|
||||
0,
|
||||
Intent(Action.SERVICE_CLOSE).setPackage(service.packageName),
|
||||
flags
|
||||
)
|
||||
).build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun show() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Application.notification.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
notificationChannel, "hiddify service", NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
)
|
||||
}
|
||||
service.startForeground(notificationId, notification.build())
|
||||
}
|
||||
|
||||
fun close() {
|
||||
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
147
android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt
Normal file
147
android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt
Normal file
@@ -0,0 +1,147 @@
|
||||
package com.hiddify.hiddify.bg
|
||||
|
||||
import com.hiddify.hiddify.Settings
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import io.nekohasekai.libbox.TunOptions
|
||||
|
||||
class VPNService : VpnService(), PlatformInterfaceWrapper {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "A/VPNService"
|
||||
}
|
||||
|
||||
private val service = BoxService(this, this)
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) =
|
||||
service.onStartCommand(intent, flags, startId)
|
||||
|
||||
override fun onBind(intent: Intent) = service.onBind(intent)
|
||||
override fun onDestroy() {
|
||||
service.onDestroy()
|
||||
}
|
||||
|
||||
override fun onRevoke() {
|
||||
service.onRevoke()
|
||||
}
|
||||
|
||||
override fun autoDetectInterfaceControl(fd: Int) {
|
||||
protect(fd)
|
||||
}
|
||||
|
||||
override fun openTun(options: TunOptions): Int {
|
||||
if (prepare(this) != null) error("android: missing vpn permission")
|
||||
|
||||
val builder = Builder()
|
||||
.setSession("sing-box")
|
||||
.setMtu(options.mtu)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
builder.setMetered(false)
|
||||
}
|
||||
|
||||
val inet4Address = options.inet4Address
|
||||
if (inet4Address.hasNext()) {
|
||||
while (inet4Address.hasNext()) {
|
||||
val address = inet4Address.next()
|
||||
builder.addAddress(address.address, address.prefix)
|
||||
}
|
||||
}
|
||||
|
||||
val inet6Address = options.inet6Address
|
||||
if (inet6Address.hasNext()) {
|
||||
while (inet6Address.hasNext()) {
|
||||
val address = inet6Address.next()
|
||||
builder.addAddress(address.address, address.prefix)
|
||||
}
|
||||
}
|
||||
|
||||
if (options.autoRoute) {
|
||||
builder.addDnsServer(options.dnsServerAddress)
|
||||
|
||||
val inet4RouteAddress = options.inet4RouteAddress
|
||||
if (inet4RouteAddress.hasNext()) {
|
||||
while (inet4RouteAddress.hasNext()) {
|
||||
val address = inet4RouteAddress.next()
|
||||
builder.addRoute(address.address, address.prefix)
|
||||
}
|
||||
} else {
|
||||
builder.addRoute("0.0.0.0", 0)
|
||||
}
|
||||
|
||||
val inet6RouteAddress = options.inet6RouteAddress
|
||||
if (inet6RouteAddress.hasNext()) {
|
||||
while (inet6RouteAddress.hasNext()) {
|
||||
val address = inet6RouteAddress.next()
|
||||
builder.addRoute(address.address, address.prefix)
|
||||
}
|
||||
} else {
|
||||
builder.addRoute("::", 0)
|
||||
}
|
||||
|
||||
if (Settings.perAppProxyEnabled) {
|
||||
val appList = Settings.perAppProxyList
|
||||
if (Settings.perAppProxyMode == Settings.PER_APP_PROXY_INCLUDE) {
|
||||
appList.forEach {
|
||||
try {
|
||||
builder.addAllowedApplication(it)
|
||||
} catch (_: NameNotFoundException) {
|
||||
}
|
||||
}
|
||||
builder.addAllowedApplication(packageName)
|
||||
} else {
|
||||
appList.forEach {
|
||||
try {
|
||||
builder.addDisallowedApplication(it)
|
||||
} catch (_: NameNotFoundException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val includePackage = options.includePackage
|
||||
if (includePackage.hasNext()) {
|
||||
while (includePackage.hasNext()) {
|
||||
try {
|
||||
builder.addAllowedApplication(includePackage.next())
|
||||
} catch (_: NameNotFoundException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val excludePackage = options.excludePackage
|
||||
if (excludePackage.hasNext()) {
|
||||
while (excludePackage.hasNext()) {
|
||||
try {
|
||||
builder.addDisallowedApplication(excludePackage.next())
|
||||
} catch (_: NameNotFoundException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.isHTTPProxyEnabled) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
builder.setHttpProxy(
|
||||
ProxyInfo.buildDirectProxy(
|
||||
options.httpProxyServer,
|
||||
options.httpProxyServerPort
|
||||
)
|
||||
)
|
||||
} else {
|
||||
error("android: tun.platform.http_proxy requires android 10 or higher")
|
||||
}
|
||||
}
|
||||
|
||||
val pfd =
|
||||
builder.establish() ?: error("android: the application is not prepared or is revoked")
|
||||
service.fileDescriptor = pfd
|
||||
return pfd.fd
|
||||
}
|
||||
|
||||
override fun writeLog(message: String) = service.writeLog(message)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
object Action {
|
||||
const val SERVICE = "com.hiddify.app.SERVICE"
|
||||
const val SERVICE_CLOSE = "com.hiddify.app.SERVICE_CLOSE"
|
||||
const val SERVICE_RELOAD = "com.hiddify.app.sfa.SERVICE_RELOAD"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
enum class Alert {
|
||||
RequestVPNPermission,
|
||||
RequestNotificationPermission,
|
||||
EmptyConfiguration,
|
||||
StartCommandServer,
|
||||
CreateService,
|
||||
StartService
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
object SettingsKey {
|
||||
const val SELECTED_CONFIG_PATH = "selected_config_path"
|
||||
const val DISABLE_MEMORY_LIMIT = "disable_memory_limit"
|
||||
|
||||
const val PER_APP_PROXY_ENABLED = "per_app_proxy_enabled"
|
||||
const val PER_APP_PROXY_MODE = "per_app_proxy_mode"
|
||||
const val PER_APP_PROXY_LIST = "per_app_proxy_list"
|
||||
const val PER_APP_PROXY_UPDATE_ON_CHANGE = "per_app_proxy_update_on_change"
|
||||
|
||||
// cache
|
||||
|
||||
const val STARTED_BY_USER = "started_by_user"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.hiddify.hiddify.constant
|
||||
|
||||
enum class Status {
|
||||
Stopped,
|
||||
Starting,
|
||||
Started,
|
||||
Stopping,
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.hiddify.hiddify.ktx
|
||||
|
||||
import kotlin.coroutines.Continuation
|
||||
|
||||
|
||||
fun <T> Continuation<T>.tryResume(value: T) {
|
||||
try {
|
||||
resumeWith(Result.success(value))
|
||||
} catch (ignored: IllegalStateException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Continuation<T>.tryResumeWithException(exception: Throwable) {
|
||||
try {
|
||||
resumeWith(Result.failure(exception))
|
||||
} catch (ignored: IllegalStateException) {
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- exclude 127.0.0.0/8 169.254.0.0/16 10.0.0.0/8 192.168.0.0/16 172.16.0.0/12 -->
|
||||
<string-array name="bypass_private_route" translatable="false">
|
||||
<item>1.0.0.0/8</item>
|
||||
<item>2.0.0.0/7</item>
|
||||
<item>4.0.0.0/6</item>
|
||||
<item>8.0.0.0/7</item>
|
||||
<item>11.0.0.0/8</item>
|
||||
<item>12.0.0.0/6</item>
|
||||
<item>16.0.0.0/4</item>
|
||||
<item>32.0.0.0/3</item>
|
||||
<item>64.0.0.0/3</item>
|
||||
<item>96.0.0.0/4</item>
|
||||
<item>112.0.0.0/5</item>
|
||||
<item>120.0.0.0/6</item>
|
||||
<item>124.0.0.0/7</item>
|
||||
<item>126.0.0.0/8</item>
|
||||
<item>128.0.0.0/3</item>
|
||||
<item>160.0.0.0/5</item>
|
||||
<item>168.0.0.0/8</item>
|
||||
<item>169.0.0.0/9</item>
|
||||
<item>169.128.0.0/10</item>
|
||||
<item>169.192.0.0/11</item>
|
||||
<item>169.224.0.0/12</item>
|
||||
<item>169.240.0.0/13</item>
|
||||
<item>169.248.0.0/14</item>
|
||||
<item>169.252.0.0/15</item>
|
||||
<item>169.255.0.0/16</item>
|
||||
<item>170.0.0.0/7</item>
|
||||
<item>172.0.0.0/12</item>
|
||||
<item>172.32.0.0/11</item>
|
||||
<item>172.64.0.0/10</item>
|
||||
<item>172.128.0.0/9</item>
|
||||
<item>173.0.0.0/8</item>
|
||||
<item>174.0.0.0/7</item>
|
||||
<item>176.0.0.0/4</item>
|
||||
<item>192.0.0.0/9</item>
|
||||
<item>192.128.0.0/11</item>
|
||||
<item>192.160.0.0/13</item>
|
||||
<item>192.169.0.0/16</item>
|
||||
<item>192.170.0.0/15</item>
|
||||
<item>192.172.0.0/14</item>
|
||||
<item>192.176.0.0/12</item>
|
||||
<item>192.192.0.0/10</item>
|
||||
<item>193.0.0.0/8</item>
|
||||
<item>194.0.0.0/7</item>
|
||||
<item>196.0.0.0/6</item>
|
||||
<item>200.0.0.0/5</item>
|
||||
<item>208.0.0.0/4</item>
|
||||
<item>240.0.0.0/5</item>
|
||||
<item>248.0.0.0/6</item>
|
||||
<item>252.0.0.0/7</item>
|
||||
<item>254.0.0.0/8</item>
|
||||
<item>255.0.0.0/9</item>
|
||||
<item>255.128.0.0/10</item>
|
||||
<item>255.192.0.0/11</item>
|
||||
<item>255.224.0.0/12</item>
|
||||
<item>255.240.0.0/13</item>
|
||||
<item>255.248.0.0/14</item>
|
||||
<item>255.252.0.0/15</item>
|
||||
<item>255.254.0.0/16</item>
|
||||
<item>255.255.0.0/17</item>
|
||||
<item>255.255.128.0/18</item>
|
||||
<item>255.255.192.0/19</item>
|
||||
<item>255.255.224.0/20</item>
|
||||
<item>255.255.240.0/21</item>
|
||||
<item>255.255.248.0/22</item>
|
||||
<item>255.255.252.0/23</item>
|
||||
<item>255.255.254.0/24</item>
|
||||
<item>255.255.255.0/25</item>
|
||||
<item>255.255.255.128/26</item>
|
||||
<item>255.255.255.192/27</item>
|
||||
<item>255.255.255.224/28</item>
|
||||
<item>255.255.255.240/29</item>
|
||||
<item>255.255.255.248/30</item>
|
||||
<item>255.255.255.252/31</item>
|
||||
<item>255.255.255.254/32</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
4
android/app/src/main/res/values/strings.xml
Normal file
4
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="stop">Stop</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user