Migrate to singbox

This commit is contained in:
problematicconsumer
2023-08-19 22:27:23 +03:30
parent 14369d0a03
commit 684acc555d
124 changed files with 3408 additions and 2047 deletions

View File

@@ -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'

View File

View 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

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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>()!! }
}
}

View File

@@ -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)

View File

@@ -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
}
}
}

View File

@@ -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) {
}
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}
}

View 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()
}

View File

@@ -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
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}
}

View 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)
}
}
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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"))
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View 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)
}

View File

@@ -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"
}

View File

@@ -0,0 +1,10 @@
package com.hiddify.hiddify.constant
enum class Alert {
RequestVPNPermission,
RequestNotificationPermission,
EmptyConfiguration,
StartCommandServer,
CreateService,
StartService
}

View File

@@ -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"
}

View File

@@ -0,0 +1,8 @@
package com.hiddify.hiddify.constant
enum class Status {
Stopped,
Starting,
Started,
Stopping,
}

View File

@@ -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) {
}
}

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="stop">Stop</string>
</resources>