Shadowsocks protocol added
This commit is contained in:
parent
59b4bf5267
commit
29656fb9a6
22 changed files with 791 additions and 664 deletions
|
@ -77,7 +77,10 @@
|
|||
<!-- extract android style -->
|
||||
<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splashscreen"/>
|
||||
</activity>
|
||||
<service android:name=".VPNService" android:process=":QtOnlyProcess">
|
||||
<service android:name=".VPNService" android:permission="android.permission.BIND_VPN_SERVICE" android:process=":QtOnlyProcess">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
|
||||
<meta-data android:name="android.app.repository" android:value="default"/>
|
||||
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
|
||||
|
|
|
@ -47,32 +47,10 @@ dependencies {
|
|||
implementation "androidx.security:security-crypto:1.1.0-alpha03"
|
||||
implementation "androidx.security:security-identity-credential:1.0.0-alpha02"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"
|
||||
implementation project(path: ':shadowsocks')
|
||||
//ss
|
||||
// implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0'
|
||||
// implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
// implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
|
||||
// //implementation "androidx.core:core-ktx:1.2.0"
|
||||
// implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
// implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
|
||||
// implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:2.4.0"
|
||||
// implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
|
||||
// implementation "androidx.room:room-runtime:2.2.5" // runtime
|
||||
// implementation "androidx.preference:preference:1.1.0"
|
||||
// implementation "androidx.work:work-runtime-ktx:2.3.4"
|
||||
// implementation "androidx.browser:browser:1.3.0-alpha01"
|
||||
// implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||
// implementation "com.google.android.material:material:1.2.0-alpha05"
|
||||
// implementation "com.google.code.gson:gson:2.8.5"
|
||||
// implementation "dnsjava:dnsjava:2.1.9"
|
||||
// implementation "org.connectbot.jsocks:jsocks:1.0.0"
|
||||
// implementation "com.afollestad.material-dialogs:core:2.6.0"
|
||||
// implementation 'com.takisoft.preferencex:preferencex:1.1.0'
|
||||
// implementation 'com.android.support:multidex:1.0.0'
|
||||
// api 'org.connectbot.jsocks:jsocks:1.0.0'
|
||||
// annotationProcessor "androidx.room:room-compiler:2.2.5"
|
||||
// annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.4.0"
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
|
|
1
client/android/shadowsocks/.gitignore
vendored
1
client/android/shadowsocks/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
/build
|
|
@ -3,6 +3,7 @@ allprojects {
|
|||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,14 +41,6 @@ android {
|
|||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
//publish {
|
||||
// userOrg = 'kyle' //bintray注册的用户名
|
||||
// groupId = 'com.kyle' //compile引用时的第1部分groupId
|
||||
// artifactId = 'shadowsocks' //compile引用时的第2部分项目名
|
||||
// publishVersion = '1.0.1' //compile引用时的第3部分版本号
|
||||
// desc = 'This is a shadowsocks library '
|
||||
// website = 'https://github.com/zhengKyles/shadowsocksDemo'
|
||||
//}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
|
@ -62,6 +55,8 @@ dependencies {
|
|||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.30-M1"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
|
||||
|
||||
|
||||
implementation "androidx.core:core-ktx:1.2.0"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
|
|
|
@ -1,174 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 29,
|
||||
"identityHash": "b60ecca4d684ffe73173478bffd50a17",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Profile",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bypass` INTEGER NOT NULL, `host` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `individual` TEXT NOT NULL, `ipv6` INTEGER NOT NULL, `metered` INTEGER NOT NULL, `method` TEXT NOT NULL, `name` TEXT, `password` TEXT NOT NULL, `plugin` TEXT, `proxyApps` INTEGER NOT NULL, `remoteDns` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `route` TEXT NOT NULL, `rx` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `udpFallback` INTEGER, `udpdns` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "bypass",
|
||||
"columnName": "bypass",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "host",
|
||||
"columnName": "host",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "individual",
|
||||
"columnName": "individual",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ipv6",
|
||||
"columnName": "ipv6",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "metered",
|
||||
"columnName": "metered",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "method",
|
||||
"columnName": "method",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "password",
|
||||
"columnName": "password",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "plugin",
|
||||
"columnName": "plugin",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "proxyApps",
|
||||
"columnName": "proxyApps",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "remoteDns",
|
||||
"columnName": "remoteDns",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "remotePort",
|
||||
"columnName": "remotePort",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "route",
|
||||
"columnName": "route",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "rx",
|
||||
"columnName": "rx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tx",
|
||||
"columnName": "tx",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "udpFallback",
|
||||
"columnName": "udpFallback",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "udpdns",
|
||||
"columnName": "udpdns",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userOrder",
|
||||
"columnName": "userOrder",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "KeyValuePair",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` BLOB NOT NULL, `valueType` INTEGER NOT NULL, PRIMARY KEY(`key`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "valueType",
|
||||
"columnName": "valueType",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"key"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b60ecca4d684ffe73173478bffd50a17')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "f1aab1fb633378621635c344dbc8ac7b",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "KeyValuePair",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` BLOB NOT NULL, `valueType` INTEGER NOT NULL, PRIMARY KEY(`key`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "valueType",
|
||||
"columnName": "valueType",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"key"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f1aab1fb633378621635c344dbc8ac7b')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -30,48 +30,45 @@
|
|||
android:name="com.google.android.backup.api_key"
|
||||
android:value="AEdPqrEAAAAI_zVxZthz2HDuz9toTvkYvL0L5GA-OjeUIfBeXg" />
|
||||
|
||||
<service
|
||||
android:name="org.amnezia.vpn.shadowsocks.core.bg.VpnService"
|
||||
android:directBootAware="true"
|
||||
android:exported="false"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||
android:process=":bg"
|
||||
tools:targetApi="n">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||
android:value="true" />
|
||||
</service>
|
||||
<!-- <service-->
|
||||
<!-- android:name="org.amnezia.vpn.shadowsocks.core.bg.ShadowsocksVpnService"-->
|
||||
<!-- android:directBootAware="true"-->
|
||||
<!-- android:exported="false"-->
|
||||
<!-- android:label="@string/app_name"-->
|
||||
<!-- android:permission="android.permission.BIND_VPN_SERVICE"-->
|
||||
<!-- android:process=":BG"-->
|
||||
<!-- tools:targetApi="n">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.net.VpnService" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </service>-->
|
||||
|
||||
<service
|
||||
android:name="org.amnezia.vpn.shadowsocks.core.bg.TransproxyService"
|
||||
android:directBootAware="true"
|
||||
android:exported="false"
|
||||
android:process=":bg"
|
||||
tools:targetApi="n" />
|
||||
<!-- <service-->
|
||||
<!-- android:name="org.amnezia.vpn.shadowsocks.core.bg.TransproxyService"-->
|
||||
<!-- android:directBootAware="true"-->
|
||||
<!-- android:exported="false"-->
|
||||
<!-- android:process=":QtOnlyProcess"-->
|
||||
<!-- tools:targetApi="n" />-->
|
||||
|
||||
<service
|
||||
android:name="org.amnezia.vpn.shadowsocks.core.bg.ProxyService"
|
||||
android:directBootAware="true"
|
||||
android:exported="false"
|
||||
android:process=":bg"
|
||||
tools:targetApi="n" />
|
||||
<!-- <service-->
|
||||
<!-- android:name="org.amnezia.vpn.shadowsocks.core.bg.ProxyService"-->
|
||||
<!-- android:directBootAware="true"-->
|
||||
<!-- android:exported="false"-->
|
||||
<!-- android:process=":QtOnlyProcess"-->
|
||||
<!-- tools:targetApi="n" />-->
|
||||
|
||||
<activity
|
||||
android:name="org.amnezia.vpn.shadowsocks.core.VpnRequestActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/Theme.AppCompat.Translucent" />
|
||||
<!-- <activity-->
|
||||
<!-- android:name="org.amnezia.vpn.shadowsocks.core.VpnRequestActivity"-->
|
||||
<!-- android:excludeFromRecents="true"-->
|
||||
<!-- android:launchMode="singleTask"-->
|
||||
<!-- android:taskAffinity=""-->
|
||||
<!-- android:theme="@style/Theme.AppCompat.Translucent" />-->
|
||||
|
||||
<receiver
|
||||
android:name="org.amnezia.vpn.shadowsocks.core.BootReceiver"
|
||||
android:directBootAware="true"
|
||||
android:enabled="false"
|
||||
android:process=":bg">
|
||||
android:process=":QtOnlyProcess">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||
|
@ -87,48 +84,48 @@
|
|||
<service
|
||||
android:name="androidx.work.impl.background.systemalarm.SystemAlarmService"
|
||||
android:directBootAware="true"
|
||||
android:process=":bg"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<service
|
||||
android:name="androidx.work.impl.background.systemjob.SystemJobService"
|
||||
android:directBootAware="true"
|
||||
android:process=":bg"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
|
||||
<receiver
|
||||
android:name="androidx.work.impl.utils.ForceStopRunnable$BroadcastReceiver"
|
||||
android:directBootAware="true"
|
||||
android:process=":bg"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryChargingProxy"
|
||||
android:directBootAware="true"
|
||||
android:process=":bg"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$BatteryNotLowProxy"
|
||||
android:directBootAware="true"
|
||||
android:process=":bg"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$StorageNotLowProxy"
|
||||
android:directBootAware="true"
|
||||
android:process=":bg"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxy$NetworkStateProxy"
|
||||
android:directBootAware="true"
|
||||
android:process=":bg"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver"
|
||||
android:directBootAware="true"
|
||||
android:process=":bg"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
<receiver
|
||||
android:name="androidx.work.impl.background.systemalarm.ConstraintProxyUpdateReceiver"
|
||||
android:directBootAware="true"
|
||||
android:process=":bg"
|
||||
android:process=":QtOnlyProcess"
|
||||
tools:replace="android:directBootAware" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -14,12 +14,6 @@ import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
|||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
|
||||
/**
|
||||
* @author : kyle
|
||||
* e-mail : 1239878682@qq.com
|
||||
* @date : 2019/5/14 16:54
|
||||
* 看了我的代码,感动了吗?
|
||||
*/
|
||||
class VpnManager private constructor() {
|
||||
|
||||
var state = BaseService.State.Idle
|
||||
|
@ -80,31 +74,23 @@ class VpnManager private constructor() {
|
|||
connect()
|
||||
}
|
||||
|
||||
/***
|
||||
* 开启或者关闭 自动判断
|
||||
*/
|
||||
fun run(activity:Activity) {
|
||||
fun run() {
|
||||
when {
|
||||
state.canStop -> Core.stopService()
|
||||
DataStore.serviceMode == Key.modeVpn -> {
|
||||
val intent = VpnService.prepare(activity)
|
||||
if (intent != null) activity.startActivityForResult(intent, REQUEST_CONNECT)
|
||||
else onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)
|
||||
}
|
||||
// DataStore.serviceMode == Key.modeVpn -> {
|
||||
// val intent = VpnService.prepare(activity)
|
||||
// if (intent != null) activity.startActivityForResult(intent, REQUEST_CONNECT)
|
||||
// else onActivityResult(REQUEST_CONNECT, Activity.RESULT_OK, null)
|
||||
// }
|
||||
else -> Core.startService()
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* 设置状态监听
|
||||
*/
|
||||
|
||||
fun setOnStatusChangeListener(listener: OnStatusChangeListener) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
/***
|
||||
* application调用stop时调用
|
||||
*/
|
||||
fun onStop() {
|
||||
connection.bandwidthTimeout = 0
|
||||
}
|
||||
|
@ -112,31 +98,23 @@ class VpnManager private constructor() {
|
|||
fun onStart() {
|
||||
connection.bandwidthTimeout = 1000
|
||||
}
|
||||
/***
|
||||
* activity调用onActivityResult时调用
|
||||
*/
|
||||
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when {
|
||||
requestCode != REQUEST_CONNECT -> {
|
||||
}
|
||||
resultCode == Activity.RESULT_OK -> Core.startService()
|
||||
else -> {
|
||||
//无权限
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* 改变当前状态
|
||||
*/
|
||||
private fun changeState(state: BaseService.State) {
|
||||
this.state = state
|
||||
this.listener?.onStatusChanged(state)
|
||||
}
|
||||
|
||||
/***
|
||||
* 状态改变监听器
|
||||
*/
|
||||
interface OnStatusChangeListener {
|
||||
fun onStatusChanged(state: BaseService.State)
|
||||
|
||||
|
@ -144,24 +122,24 @@ class VpnManager private constructor() {
|
|||
}
|
||||
|
||||
enum class Route(name: String) {
|
||||
//全部
|
||||
|
||||
ALL("all")
|
||||
//绕过局域网地址
|
||||
|
||||
,
|
||||
BY_PASS_LAN("bypass-lan")
|
||||
//绕过中国大陆地址
|
||||
|
||||
,
|
||||
BY_PASS_CHINA("bypass-china")
|
||||
//绕过局域网和中国大陆地址
|
||||
|
||||
,
|
||||
BY_PASS_LAN_CHINA("bypass-lan-china")
|
||||
//GFW列表
|
||||
|
||||
,
|
||||
GFW_LIST("gfwlist")
|
||||
//仅代理中国大陆地址
|
||||
|
||||
,
|
||||
CHINA_LIST("china-list")
|
||||
//自定义规则
|
||||
|
||||
,
|
||||
CUSTOM_RULES("custom-rules");
|
||||
|
||||
|
|
|
@ -1,23 +1,3 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.acl
|
||||
|
||||
import android.content.Context
|
||||
|
|
|
@ -1,23 +1,3 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.acl
|
||||
|
||||
import android.content.Context
|
||||
|
|
|
@ -31,7 +31,7 @@ import android.os.RemoteException
|
|||
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.ProxyService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.TransproxyService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.VpnService
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.ShadowsocksVpnService
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key
|
||||
|
@ -45,7 +45,7 @@ class ShadowsocksConnection(private val handler: Handler = Handler(),
|
|||
companion object {
|
||||
val serviceClass get() = when (DataStore.serviceMode) {
|
||||
Key.modeProxy -> ProxyService::class
|
||||
Key.modeVpn -> VpnService::class
|
||||
Key.modeVpn -> ShadowsocksVpnService::class
|
||||
Key.modeTransproxy -> TransproxyService::class
|
||||
else -> throw UnknownError()
|
||||
}.java
|
||||
|
|
|
@ -25,26 +25,25 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.Core.app
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksService
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.plugin.PluginManager
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.readableMessage
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.net.BindException
|
||||
import java.net.InetAddress
|
||||
import java.net.URL
|
||||
import java.net.UnknownHostException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This object uses WeakMap to simulate the effects of multi-inheritance.
|
||||
|
@ -64,13 +63,13 @@ object BaseService {
|
|||
const val CONFIG_FILE = "shadowsocks.conf"
|
||||
const val CONFIG_FILE_UDP = "shadowsocks-udp.conf"
|
||||
|
||||
class Data internal constructor(private val service: Interface) {
|
||||
class Data(private val service: Interface) {
|
||||
var state = State.Stopped
|
||||
var processes: GuardedProcessPool? = null
|
||||
var proxy: ProxyInstance? = null
|
||||
var udpFallback: ProxyInstance? = null
|
||||
|
||||
var notification: ServiceNotification? = null
|
||||
// var notification: ServiceNotification? = null
|
||||
val closeReceiver = broadcastReceiver { _, intent ->
|
||||
when (intent.action) {
|
||||
Action.RELOAD -> service.forceLoad()
|
||||
|
@ -96,7 +95,8 @@ object BaseService {
|
|||
stopListeningForBandwidth(callback ?: return)
|
||||
}
|
||||
}
|
||||
private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier
|
||||
private val bandwidthListeners =
|
||||
mutableMapOf<IBinder, Long>() // the binder is the real identifier
|
||||
private val handler = Handler()
|
||||
|
||||
override fun getState(): Int = (data?.state ?: State.Idle).ordinal
|
||||
|
@ -119,14 +119,15 @@ object BaseService {
|
|||
}
|
||||
|
||||
private fun registerTimeout() {
|
||||
handler.postDelayed(this::onTimeout, bandwidthListeners.values.min() ?: return)
|
||||
handler.postDelayed(this::onTimeout, bandwidthListeners.values.minOrNull() ?: return)
|
||||
}
|
||||
|
||||
private fun onTimeout() {
|
||||
val proxies = listOfNotNull(data?.proxy, data?.udpFallback)
|
||||
val stats = proxies
|
||||
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
|
||||
.filter { it.second != null }
|
||||
.map { Triple(it.first, it.second!!.first, it.second!!.second) }
|
||||
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
|
||||
.filter { it.second != null }
|
||||
.map { Triple(it.first, it.second!!.first, it.second!!.second) }
|
||||
if (stats.any { it.third } && data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {
|
||||
val sum = stats.fold(TrafficStats()) { a, b -> a + b.second }
|
||||
broadcast { item ->
|
||||
|
@ -148,17 +149,21 @@ object BaseService {
|
|||
val data = data
|
||||
val proxy = data?.proxy ?: return
|
||||
proxy.trafficMonitor?.out.also { stats ->
|
||||
cb.trafficUpdated(proxy.profile.id, if (stats == null) sum else {
|
||||
sum += stats
|
||||
stats
|
||||
})
|
||||
cb.trafficUpdated(
|
||||
proxy.profile.id, if (stats == null) sum else {
|
||||
sum += stats
|
||||
stats
|
||||
}
|
||||
)
|
||||
}
|
||||
data.udpFallback?.also { udpFallback ->
|
||||
udpFallback.trafficMonitor?.out.also { stats ->
|
||||
cb.trafficUpdated(udpFallback.profile.id, if (stats == null) TrafficStats() else {
|
||||
sum += stats
|
||||
stats
|
||||
})
|
||||
cb.trafficUpdated(
|
||||
udpFallback.profile.id, if (stats == null) TrafficStats() else {
|
||||
sum += stats
|
||||
stats
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
cb.trafficUpdated(0, sum)
|
||||
|
@ -197,15 +202,17 @@ object BaseService {
|
|||
interface Interface {
|
||||
val data: Data
|
||||
val tag: String
|
||||
fun createNotification(profileName: String): ServiceNotification
|
||||
// fun createNotification(profileName: String): ServiceNotification
|
||||
|
||||
fun onBind(intent: Intent): IBinder? = if (intent.action == Action.SERVICE) data.binder else null
|
||||
fun onBind(intent: Intent): IBinder? =
|
||||
if (intent.action == Action.SERVICE) data.binder else null
|
||||
|
||||
fun forceLoad() {
|
||||
val (profile, fallback) = Core.currentProfile
|
||||
?: return stopRunner(false, (this as Context).getString(R.string.profile_empty))
|
||||
?: return stopRunner(false, (this as Context).getString(R.string.profile_empty))
|
||||
if (profile.host.isEmpty() || profile.password.isEmpty() ||
|
||||
fallback != null && (fallback.host.isEmpty() || fallback.password.isEmpty())) {
|
||||
fallback != null && (fallback.host.isEmpty() || fallback.password.isEmpty())
|
||||
) {
|
||||
stopRunner(false, (this as Context).getString(R.string.proxy_empty))
|
||||
return
|
||||
}
|
||||
|
@ -221,17 +228,22 @@ object BaseService {
|
|||
|
||||
suspend fun startProcesses() {
|
||||
val configRoot = (if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
|
||||
?.isUserUnlocked != false) app else Core.deviceStorage).noBackupFilesDir
|
||||
?.isUserUnlocked != false
|
||||
) app else Core.deviceStorage).noBackupFilesDir
|
||||
val udpFallback = data.udpFallback
|
||||
data.proxy!!.start(this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
|
||||
File(configRoot, CONFIG_FILE),
|
||||
if (udpFallback == null) "-u" else null)
|
||||
data.proxy!!.start(
|
||||
this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
|
||||
File(configRoot, CONFIG_FILE),
|
||||
if (udpFallback == null) "-u" else null
|
||||
)
|
||||
check(udpFallback?.pluginPath == null) { "UDP fallback cannot have plugins" }
|
||||
udpFallback?.start(this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_udp"),
|
||||
File(configRoot, CONFIG_FILE_UDP),
|
||||
"-U")
|
||||
udpFallback?.start(
|
||||
this,
|
||||
File(Core.deviceStorage.noBackupFilesDir, "stat_udp"),
|
||||
File(configRoot, CONFIG_FILE_UDP),
|
||||
"-U"
|
||||
)
|
||||
}
|
||||
|
||||
fun startRunner() {
|
||||
|
@ -264,8 +276,8 @@ object BaseService {
|
|||
data.closeReceiverRegistered = false
|
||||
}
|
||||
|
||||
data.notification?.destroy()
|
||||
data.notification = null
|
||||
// data.notification?.destroy()
|
||||
// data.notification = null
|
||||
|
||||
val ids = listOfNotNull(data.proxy, data.udpFallback).map {
|
||||
it.shutdown(this)
|
||||
|
@ -280,30 +292,36 @@ object BaseService {
|
|||
data.changeState(State.Stopped, msg)
|
||||
|
||||
// stop the service if nothing has bound to it
|
||||
if (restart) startRunner() else stopSelf()
|
||||
if (restart) {
|
||||
startRunner()
|
||||
} else {
|
||||
Log.d("Aman", "Stop Self BaseService-------")
|
||||
// stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun preInit() { }
|
||||
suspend fun preInit() {}
|
||||
suspend fun resolver(host: String) = InetAddress.getAllByName(host)
|
||||
suspend fun openConnection(url: URL) = url.openConnection()
|
||||
|
||||
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val data = data
|
||||
if (data.state != State.Stopped) return Service.START_NOT_STICKY
|
||||
if (data.state != State.Stopped) return Service.START_REDELIVER_INTENT
|
||||
val profilePair = Core.currentProfile
|
||||
this as Context
|
||||
if (profilePair == null) {
|
||||
// gracefully shutdown: https://stackoverflow.com/q/47337857/2245107
|
||||
data.notification = createNotification("")
|
||||
// data.notification = createNotification("")
|
||||
stopRunner(false, getString(R.string.profile_empty))
|
||||
return Service.START_NOT_STICKY
|
||||
return Service.START_REDELIVER_INTENT
|
||||
}
|
||||
val (profile, fallback) = profilePair
|
||||
profile.name = profile.formattedName // save name for later queries
|
||||
val proxy = ProxyInstance(profile)
|
||||
data.proxy = proxy
|
||||
data.udpFallback = if (fallback == null) null else ProxyInstance(fallback, profile.route)
|
||||
data.udpFallback =
|
||||
if (fallback == null) null else ProxyInstance(fallback, profile.route)
|
||||
|
||||
if (!data.closeReceiverRegistered) {
|
||||
registerReceiver(data.closeReceiver, IntentFilter().apply {
|
||||
|
@ -314,7 +332,7 @@ object BaseService {
|
|||
data.closeReceiverRegistered = true
|
||||
}
|
||||
|
||||
data.notification = createNotification(profile.formattedName)
|
||||
// data.notification = createNotification(profile.formattedName)
|
||||
|
||||
data.changeState(State.Connecting)
|
||||
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
|
||||
|
@ -340,16 +358,20 @@ object BaseService {
|
|||
stopRunner(false, getString(R.string.invalid_server))
|
||||
} catch (exc: Throwable) {
|
||||
if (exc !is PluginManager.PluginNotFoundException &&
|
||||
exc !is BindException &&
|
||||
exc !is VpnService.NullConnectionException) {
|
||||
exc !is BindException &&
|
||||
exc !is ShadowsocksVpnService.NullConnectionException
|
||||
) {
|
||||
printLog(exc)
|
||||
}
|
||||
stopRunner(false, "${getString(R.string.service_failed)}: ${exc.readableMessage}")
|
||||
stopRunner(
|
||||
false,
|
||||
"${getString(R.string.service_failed)}: ${exc.readableMessage}"
|
||||
)
|
||||
} finally {
|
||||
data.connectingJob = null
|
||||
}
|
||||
}
|
||||
return Service.START_NOT_STICKY
|
||||
return Service.START_REDELIVER_INTENT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ import android.content.Intent
|
|||
class ProxyService : Service(), BaseService.Interface {
|
||||
override val data = BaseService.Data(this)
|
||||
override val tag: String get() = "ShadowsocksProxyService"
|
||||
override fun createNotification(profileName: String): ServiceNotification =
|
||||
ServiceNotification(this, profileName, "service-proxy", true)
|
||||
// override fun createNotification(profileName: String): ServiceNotification =
|
||||
// ServiceNotification(this, profileName, "service-proxy", true)
|
||||
|
||||
override fun onBind(intent: Intent) = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
|
|
|
@ -1,135 +1,145 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package org.amnezia.vpn.shadowsocks.core.bg
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.text.format.Formatter
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||
|
||||
/**
|
||||
* Android < 8 VPN: always invisible because of VPN notification/icon
|
||||
* Android < 8 other: only invisible in (possibly unsecure) lockscreen
|
||||
* Android 8+: always visible due to system limitations
|
||||
* (user can choose to hide the notification in secure lockscreen or anywhere)
|
||||
*/
|
||||
class ServiceNotification(private val service: BaseService.Interface, profileName: String,
|
||||
channel: String, private val visible: Boolean = false) {
|
||||
private val keyGuard = (service as Context).getSystemService<KeyguardManager>()!!
|
||||
private val nm by lazy { (service as Context).getSystemService<NotificationManager>()!! }
|
||||
private val callback: IShadowsocksServiceCallback by lazy {
|
||||
object : IShadowsocksServiceCallback.Stub() {
|
||||
override fun stateChanged(state: Int, profileName: String?, msg: String?) { } // ignore
|
||||
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||
if (profileId != 0L) return
|
||||
service as Context
|
||||
val txr = service.getString(R.string.speed, Formatter.formatFileSize(service, stats.txRate))
|
||||
val rxr = service.getString(R.string.speed, Formatter.formatFileSize(service, stats.rxRate))
|
||||
builder.setContentText("$txr↑\t$rxr↓")
|
||||
style.bigText(service.getString(R.string.stat_summary, txr, rxr,
|
||||
Formatter.formatFileSize(service, stats.txTotal),
|
||||
Formatter.formatFileSize(service, stats.rxTotal)))
|
||||
show()
|
||||
}
|
||||
override fun trafficPersisted(profileId: Long) { }
|
||||
}
|
||||
}
|
||||
private val lockReceiver = broadcastReceiver { _, intent -> update(intent.action) }
|
||||
private var callbackRegistered = false
|
||||
|
||||
private val builder = NotificationCompat.Builder(service as Context, channel)
|
||||
.setWhen(0)
|
||||
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
||||
.setTicker(service.getString(R.string.forward_success))
|
||||
.setContentTitle(profileName)
|
||||
.setContentIntent(Core.configureIntent(service))
|
||||
.setSmallIcon(R.drawable.ic_service_active)
|
||||
private val style = NotificationCompat.BigTextStyle(builder).bigText("")
|
||||
private var isVisible = true
|
||||
|
||||
init {
|
||||
service as Context
|
||||
if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
|
||||
service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0))
|
||||
update(if (service.getSystemService<PowerManager>()?.isInteractive != false)
|
||||
Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
|
||||
service.registerReceiver(lockReceiver, IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
if (visible && Build.VERSION.SDK_INT < 26) addAction(Intent.ACTION_USER_PRESENT)
|
||||
})
|
||||
}
|
||||
|
||||
private fun update(action: String?, forceShow: Boolean = false) {
|
||||
if (forceShow || service.data.state == BaseService.State.Connected) when (action) {
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
setVisible(false, forceShow)
|
||||
unregisterCallback() // unregister callback to save battery
|
||||
}
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
setVisible(visible && !keyGuard.isKeyguardLocked, forceShow)
|
||||
service.data.binder.registerCallback(callback)
|
||||
service.data.binder.startListeningForBandwidth(callback, 1000)
|
||||
callbackRegistered = true
|
||||
}
|
||||
Intent.ACTION_USER_PRESENT -> setVisible(true, forceShow)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterCallback() {
|
||||
if (callbackRegistered) {
|
||||
service.data.binder.unregisterCallback(callback)
|
||||
callbackRegistered = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setVisible(visible: Boolean, forceShow: Boolean = false) {
|
||||
if (isVisible != visible) {
|
||||
isVisible = visible
|
||||
builder.priority = if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN
|
||||
show()
|
||||
} else if (forceShow) show()
|
||||
}
|
||||
|
||||
private fun show() = (service as Service).startForeground(1, builder.build())
|
||||
|
||||
fun destroy() {
|
||||
(service as Service).unregisterReceiver(lockReceiver)
|
||||
unregisterCallback()
|
||||
service.stopForeground(true)
|
||||
nm.cancel(1)
|
||||
}
|
||||
}
|
||||
///*******************************************************************************
|
||||
// * *
|
||||
// * Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
// * Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
|
||||
// * *
|
||||
// * This program is free software: you can redistribute it and/or modify *
|
||||
// * it under the terms of the GNU General Public License as published by *
|
||||
// * the Free Software Foundation, either version 3 of the License, or *
|
||||
// * (at your option) any later version. *
|
||||
// * *
|
||||
// * This program is distributed in the hope that it will be useful, *
|
||||
// * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
// * GNU General Public License for more details. *
|
||||
// * *
|
||||
// * You should have received a copy of the GNU General Public License *
|
||||
// * along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
// * *
|
||||
// *******************************************************************************/
|
||||
//
|
||||
//package org.amnezia.vpn.shadowsocks.core.bg
|
||||
//
|
||||
//import android.app.KeyguardManager
|
||||
//import android.app.NotificationManager
|
||||
//import android.app.PendingIntent
|
||||
//import android.app.Service
|
||||
//import android.content.Context
|
||||
//import android.content.Intent
|
||||
//import android.content.IntentFilter
|
||||
//import android.os.Build
|
||||
//import android.os.PowerManager
|
||||
//import android.text.format.Formatter
|
||||
//import androidx.core.app.NotificationCompat
|
||||
//import androidx.core.content.ContextCompat
|
||||
//import androidx.core.content.getSystemService
|
||||
//import org.amnezia.vpn.shadowsocks.core.Core
|
||||
//import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
|
||||
//import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
|
||||
//import org.amnezia.vpn.shadowsocks.core.R
|
||||
//import org.amnezia.vpn.shadowsocks.core.utils.Action
|
||||
//import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
|
||||
//
|
||||
///**
|
||||
// * Android < 8 VPN: always invisible because of VPN notification/icon
|
||||
// * Android < 8 other: only invisible in (possibly unsecure) lockscreen
|
||||
// * Android 8+: always visible due to system limitations
|
||||
// * (user can choose to hide the notification in secure lockscreen or anywhere)
|
||||
// */
|
||||
//class ServiceNotification(private val service: BaseService.Interface, profileName: String,
|
||||
// channel: String, private val visible: Boolean = false) {
|
||||
// private val keyGuard = (service as Context).getSystemService<KeyguardManager>()!!
|
||||
// private val nm by lazy { (service as Context).getSystemService<NotificationManager>()!! }
|
||||
// private val callback: IShadowsocksServiceCallback by lazy {
|
||||
// object : IShadowsocksServiceCallback.Stub() {
|
||||
// override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
||||
// when (state) {
|
||||
// BaseService.State.Connected.ordinal -> {
|
||||
// builder.setContentText("VPN Connected")
|
||||
// }
|
||||
// BaseService.State.Stopped.ordinal -> {
|
||||
// builder.setContentText("VPN Disconnected")
|
||||
// }
|
||||
// }
|
||||
// } // ignore
|
||||
// override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||
//// if (profileId != 0L) return
|
||||
//// service as Context
|
||||
//// val txr = service.getString(R.string.speed, Formatter.formatFileSize(service, stats.txRate))
|
||||
//// val rxr = service.getString(R.string.speed, Formatter.formatFileSize(service, stats.rxRate))
|
||||
//// builder.setContentText("$txr↑\t$rxr↓")
|
||||
//// style.bigText(service.getString(R.string.stat_summary, txr, rxr,
|
||||
//// Formatter.formatFileSize(service, stats.txTotal),
|
||||
//// Formatter.formatFileSize(service, stats.rxTotal)))
|
||||
//// show()
|
||||
// }
|
||||
// override fun trafficPersisted(profileId: Long) { }
|
||||
// }
|
||||
// }
|
||||
//// private val lockReceiver = broadcastReceiver { _, intent -> update(intent.action) }
|
||||
// private var callbackRegistered = false
|
||||
//
|
||||
// private val builder = NotificationCompat.Builder(service as Context, channel)
|
||||
// .setWhen(0)
|
||||
// .setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
||||
// .setTicker(service.getString(R.string.forward_success))
|
||||
// .setContentTitle("AmneziaVPN -- testing")
|
||||
// .setContentIntent(Core.configureIntent(service))
|
||||
// .setSmallIcon(R.drawable.ic_amnezia_round)
|
||||
// private val style = NotificationCompat.BigTextStyle(builder).bigText("")
|
||||
// private var isVisible = true
|
||||
//
|
||||
// init {
|
||||
// service as Context
|
||||
//// if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
|
||||
//// service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0))
|
||||
//// update(if (service.getSystemService<PowerManager>()?.isInteractive != false)
|
||||
//// Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
|
||||
//// service.registerReceiver(lockReceiver, IntentFilter().apply {
|
||||
//// addAction(Intent.ACTION_SCREEN_ON)
|
||||
//// addAction(Intent.ACTION_SCREEN_OFF)
|
||||
//// if (visible && Build.VERSION.SDK_INT < 26) addAction(Intent.ACTION_USER_PRESENT)
|
||||
//// })
|
||||
// }
|
||||
//
|
||||
//// private fun update(action: String?, forceShow: Boolean = false) {
|
||||
//// if (forceShow || service.data.state == BaseService.State.Connected) when (action) {
|
||||
//// Intent.ACTION_SCREEN_OFF -> {
|
||||
//// setVisible(false, forceShow)
|
||||
//// unregisterCallback() // unregister callback to save battery
|
||||
//// }
|
||||
//// Intent.ACTION_SCREEN_ON -> {
|
||||
//// setVisible(visible && !keyGuard.isKeyguardLocked, forceShow)
|
||||
//// service.data.binder.registerCallback(callback)
|
||||
//// service.data.binder.startListeningForBandwidth(callback, 1000)
|
||||
//// callbackRegistered = true
|
||||
//// }
|
||||
//// Intent.ACTION_USER_PRESENT -> setVisible(true, forceShow)
|
||||
//// }
|
||||
//// }
|
||||
//
|
||||
// private fun unregisterCallback() {
|
||||
// if (callbackRegistered) {
|
||||
// service.data.binder.unregisterCallback(callback)
|
||||
// callbackRegistered = false
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun setVisible(visible: Boolean, forceShow: Boolean = false) {
|
||||
// if (isVisible != visible) {
|
||||
// isVisible = visible
|
||||
// builder.priority = if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN
|
||||
// show()
|
||||
// } else if (forceShow) show()
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private fun show() = (service as Service).startForeground(1337, builder.build())
|
||||
//
|
||||
// fun destroy() {
|
||||
//// (service as Service).unregisterReceiver(lockReceiver)
|
||||
// unregisterCallback()
|
||||
//// service.stopForeground(true)
|
||||
// nm.cancel(1337)
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -30,6 +30,7 @@ import android.os.Build
|
|||
import android.os.ParcelFileDescriptor
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.util.Log
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.VpnRequestActivity
|
||||
|
@ -51,7 +52,7 @@ import java.net.URL
|
|||
import java.util.*
|
||||
import android.net.VpnService as BaseVpnService
|
||||
|
||||
class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
open class ShadowsocksVpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
companion object {
|
||||
private const val VPN_MTU = 1500
|
||||
private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1"
|
||||
|
@ -95,8 +96,10 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
|||
|
||||
override val data = BaseService.Data(this)
|
||||
override val tag: String get() = "ShadowsocksVpnService"
|
||||
override fun createNotification(profileName: String): ServiceNotification =
|
||||
ServiceNotification(this, profileName, "service-vpn")
|
||||
|
||||
val NOTIFICATION_CHANNEL_ID = "com.amnezia.vpnNotification"
|
||||
// override fun createNotification(profileName: String): ServiceNotification =
|
||||
// ServiceNotification(this, profileName, NOTIFICATION_CHANNEL_ID)
|
||||
|
||||
private var conn: ParcelFileDescriptor? = null
|
||||
private var worker: ProtectWorker? = null
|
||||
|
@ -117,7 +120,9 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
|||
else -> super<LocalDnsService.Interface>.onBind(intent)
|
||||
}
|
||||
|
||||
override fun onRevoke() = stopRunner()
|
||||
override fun onRevoke() {
|
||||
stopRunner()
|
||||
}
|
||||
|
||||
override fun killProcesses(scope: CoroutineScope) {
|
||||
super.killProcesses(scope)
|
||||
|
@ -136,7 +141,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
|||
} else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
stopRunner()
|
||||
return Service.START_NOT_STICKY
|
||||
return Service.START_STICKY
|
||||
}
|
||||
|
||||
override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
|
|
@ -29,15 +29,16 @@ import java.io.File
|
|||
class TransproxyService : Service(), LocalDnsService.Interface {
|
||||
override val data = BaseService.Data(this)
|
||||
override val tag: String get() = "ShadowsocksTransproxyService"
|
||||
override fun createNotification(profileName: String): ServiceNotification =
|
||||
ServiceNotification(this, profileName, "service-transproxy", true)
|
||||
// override fun createNotification(profileName: String): ServiceNotification =
|
||||
// ServiceNotification(this, profileName, "service-transproxy", true)
|
||||
|
||||
override fun onBind(intent: Intent) = super.onBind(intent)
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||
|
||||
private fun startRedsocksDaemon() {
|
||||
File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText("""base {
|
||||
File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText(
|
||||
"""base {
|
||||
log_debug = off;
|
||||
log_info = off;
|
||||
log = stderr;
|
||||
|
@ -51,9 +52,15 @@ redsocks {
|
|||
port = ${DataStore.portProxy};
|
||||
type = socks5;
|
||||
}
|
||||
""")
|
||||
data.processes!!.start(listOf(
|
||||
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath, "-c", "redsocks.conf"))
|
||||
"""
|
||||
)
|
||||
data.processes!!.start(
|
||||
listOf(
|
||||
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath,
|
||||
"-c",
|
||||
"redsocks.conf"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun startProcesses() {
|
||||
|
|
|
@ -75,9 +75,9 @@ object Key {
|
|||
}
|
||||
|
||||
object Action {
|
||||
const val SERVICE = "com.kyle.shadowsocks.SERVICE"
|
||||
const val CLOSE = "com.kyle.shadowsocks.CLOSE"
|
||||
const val RELOAD = "com.kyle.shadowsocks.RELOAD"
|
||||
const val SERVICE = "org.amnezia.vpn.shadowsocks.SERVICE"
|
||||
const val CLOSE = "org.amnezia.vpn.shadowsocks.CLOSE"
|
||||
const val RELOAD = "org.amnezia.vpn.shadowsocks.RELOAD"
|
||||
|
||||
const val EXTRA_PROFILE_ID = "com.kyle.shadowsocks.EXTRA_PROFILE_ID"
|
||||
const val EXTRA_PROFILE_ID = "org.amnezia.vpn.shadowsocks.EXTRA_PROFILE_ID"
|
||||
}
|
||||
|
|
|
@ -38,10 +38,10 @@ import androidx.core.os.bundleOf
|
|||
* ...
|
||||
* <application>
|
||||
* ...
|
||||
* <provider android:name="com.kyle.shadowsocks.$PLUGIN_ID.BinaryProvider"
|
||||
* android:authorities="com.kyle.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider">
|
||||
* <provider android:name="org.amnezia.vpn.shadowsocks.$PLUGIN_ID.BinaryProvider"
|
||||
* android:authorities="org.amnezia.vpn.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider">
|
||||
* <intent-filter>
|
||||
* <category android:name="com.kyle.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" />
|
||||
* <category android:name="org.amnezia.vpn.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" />
|
||||
* </intent-filter>
|
||||
* </provider>
|
||||
* ...
|
||||
|
|
|
@ -29,61 +29,61 @@ object PluginContract {
|
|||
/**
|
||||
* ContentProvider Action: Used for NativePluginProvider.
|
||||
*
|
||||
* Constant Value: "com.kyle.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
|
||||
*/
|
||||
const val ACTION_NATIVE_PLUGIN = "com.kyle.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
|
||||
const val ACTION_NATIVE_PLUGIN = "org.amnezia.vpn.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
|
||||
|
||||
/**
|
||||
* Activity Action: Used for ConfigurationActivity.
|
||||
*
|
||||
* Constant Value: "com.kyle.shadowsocks.plugin.ACTION_CONFIGURE"
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.ACTION_CONFIGURE"
|
||||
*/
|
||||
const val ACTION_CONFIGURE = "com.kyle.shadowsocks.plugin.ACTION_CONFIGURE"
|
||||
const val ACTION_CONFIGURE = "org.amnezia.vpn.shadowsocks.plugin.ACTION_CONFIGURE"
|
||||
/**
|
||||
* Activity Action: Used for HelpActivity or HelpCallback.
|
||||
*
|
||||
* Constant Value: "com.kyle.shadowsocks.plugin.ACTION_HELP"
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.ACTION_HELP"
|
||||
*/
|
||||
const val ACTION_HELP = "com.kyle.shadowsocks.plugin.ACTION_HELP"
|
||||
const val ACTION_HELP = "org.amnezia.vpn.shadowsocks.plugin.ACTION_HELP"
|
||||
|
||||
/**
|
||||
* The lookup key for a string that provides the plugin entry binary.
|
||||
*
|
||||
* Example: "/data/data/com.kyle.shadowsocks.plugin.obfs_local/lib/libobfs-local.so"
|
||||
* Example: "/data/data/org.amnezia.vpn.shadowsocks.plugin.obfs_local/lib/libobfs-local.so"
|
||||
*
|
||||
* Constant Value: "com.kyle.shadowsocks.plugin.EXTRA_ENTRY"
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.EXTRA_ENTRY"
|
||||
*/
|
||||
const val EXTRA_ENTRY = "com.kyle.shadowsocks.plugin.EXTRA_ENTRY"
|
||||
const val EXTRA_ENTRY = "org.amnezia.vpn.shadowsocks.plugin.EXTRA_ENTRY"
|
||||
/**
|
||||
* The lookup key for a string that provides the options as a string.
|
||||
*
|
||||
* Example: "obfs=http;obfs-host=www.baidu.com"
|
||||
*
|
||||
* Constant Value: "com.kyle.shadowsocks.plugin.EXTRA_OPTIONS"
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.EXTRA_OPTIONS"
|
||||
*/
|
||||
const val EXTRA_OPTIONS = "com.kyle.shadowsocks.plugin.EXTRA_OPTIONS"
|
||||
const val EXTRA_OPTIONS = "org.amnezia.vpn.shadowsocks.plugin.EXTRA_OPTIONS"
|
||||
/**
|
||||
* The lookup key for a CharSequence that provides user relevant help message.
|
||||
*
|
||||
* Example: "obfs=<http></http>|tls> Enable obfuscating: HTTP or TLS (Experimental).
|
||||
* obfs-host=<host_name> Hostname for obfuscating (Experimental)."
|
||||
*
|
||||
* Constant Value: "com.kyle.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
|
||||
</host_name> */
|
||||
const val EXTRA_HELP_MESSAGE = "com.kyle.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
|
||||
const val EXTRA_HELP_MESSAGE = "org.amnezia.vpn.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
|
||||
|
||||
/**
|
||||
* The metadata key to retrieve plugin id. Required for plugins.
|
||||
*
|
||||
* Constant Value: "com.kyle.shadowsocks.plugin.id"
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.id"
|
||||
*/
|
||||
const val METADATA_KEY_ID = "com.kyle.shadowsocks.plugin.id"
|
||||
const val METADATA_KEY_ID = "org.amnezia.vpn.shadowsocks.plugin.id"
|
||||
/**
|
||||
* The metadata key to retrieve default configuration. Default value is empty.
|
||||
*
|
||||
* Constant Value: "com.kyle.shadowsocks.plugin.default_config"
|
||||
* Constant Value: "org.amnezia.vpn.shadowsocks.plugin.default_config"
|
||||
*/
|
||||
const val METADATA_KEY_DEFAULT_CONFIG = "com.kyle.shadowsocks.plugin.default_config"
|
||||
const val METADATA_KEY_DEFAULT_CONFIG = "org.amnezia.vpn.shadowsocks.plugin.default_config"
|
||||
|
||||
const val METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable"
|
||||
|
||||
|
@ -114,5 +114,5 @@ object PluginContract {
|
|||
/**
|
||||
* The authority for general plugin actions.
|
||||
*/
|
||||
const val AUTHORITY = "com.kyle.shadowsocks"
|
||||
const val AUTHORITY = "org.amnezia.vpn.shadowsocks"
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,11 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:name="path"
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 21.25 2.28 L 17.55 18.55 L 9.26 15.89 L 16.58 7.16 L 6.83 15.37 L 0 12.8 L 21.25 2.28 ZM 9.45 17.56 L 12.09 18.41 L 9.46 22 L 9.45 17.56 Z" />
|
||||
</vector>
|
|
@ -6,18 +6,137 @@ package org.amnezia.vpn
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.LocalSocket
|
||||
import android.net.LocalSocketAddress
|
||||
import android.net.Network
|
||||
import android.net.ProxyInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.*
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import com.wireguard.android.util.SharedLibraryLoader
|
||||
import com.wireguard.config.*
|
||||
import com.wireguard.crypto.Key
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.vpn.shadowsocks.core.Core
|
||||
import org.amnezia.vpn.shadowsocks.core.R
|
||||
import org.amnezia.vpn.shadowsocks.core.VpnRequestActivity
|
||||
import org.amnezia.vpn.shadowsocks.core.acl.Acl
|
||||
import org.amnezia.vpn.shadowsocks.core.bg.*
|
||||
import org.amnezia.vpn.shadowsocks.core.database.Profile
|
||||
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
|
||||
import org.amnezia.vpn.shadowsocks.core.net.ConcurrentLocalSocketListener
|
||||
import org.amnezia.vpn.shadowsocks.core.net.DefaultNetworkListener
|
||||
import org.amnezia.vpn.shadowsocks.core.net.Subnet
|
||||
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.Key.modeVpn
|
||||
import org.amnezia.vpn.shadowsocks.core.utils.printLog
|
||||
import org.json.JSONObject
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.FileDescriptor
|
||||
import java.io.IOException
|
||||
import android.net.VpnService as BaseVpnService
|
||||
|
||||
|
||||
class VPNService : BaseVpnService(), LocalDnsService.Interface {
|
||||
|
||||
override val data = BaseService.Data(this)
|
||||
override val tag: String get() = "VPNService"
|
||||
// override fun createNotification(profileName: String): ServiceNotification =
|
||||
// ServiceNotification(this, profileName, "service-vpn")
|
||||
|
||||
private var conn: ParcelFileDescriptor? = null
|
||||
private var worker: ProtectWorker? = null
|
||||
private var active = false
|
||||
private var metered = false
|
||||
private var underlyingNetwork: Network? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (active && Build.VERSION.SDK_INT >= 22) setUnderlyingNetworks(underlyingNetworks)
|
||||
}
|
||||
private val underlyingNetworks
|
||||
get() =
|
||||
// clearing underlyingNetworks makes Android 9+ consider the network to be metered
|
||||
if (Build.VERSION.SDK_INT >= 28 && metered) null else underlyingNetwork?.let {
|
||||
arrayOf(
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
var runnable: Runnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (mProtocol.equals("shadowsocks", true)) {
|
||||
Log.e(tag, "run: -----------------: ${data.state}")
|
||||
when (data.state) {
|
||||
BaseService.State.Connected -> {
|
||||
currentTunnelHandle = 1
|
||||
isUp = true
|
||||
}
|
||||
BaseService.State.Stopped -> {
|
||||
currentTunnelHandle = -1
|
||||
isUp = false
|
||||
}
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.postDelayed(this, 1000L) //wait 4 sec and run again
|
||||
}
|
||||
}
|
||||
|
||||
fun stopTest() {
|
||||
handler.removeCallbacks(runnable)
|
||||
}
|
||||
|
||||
fun startTest() {
|
||||
handler.postDelayed(runnable, 0) //wait 0 ms and run
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VPN_MTU = 1500
|
||||
private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1"
|
||||
private const val PRIVATE_VLAN4_ROUTER = "172.19.0.2"
|
||||
private const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1"
|
||||
private const val PRIVATE_VLAN6_ROUTER = "fdfe:dcba:9876::2"
|
||||
|
||||
/**
|
||||
* https://android.googlesource.com/platform/prebuilts/runtime/+/94fec32/appcompat/hiddenapi-light-greylist.txt#9466
|
||||
*/
|
||||
private val getInt = FileDescriptor::class.java.getDeclaredMethod("getInt$")
|
||||
|
||||
@JvmStatic
|
||||
fun startService(c: Context) {
|
||||
c.applicationContext.startService(
|
||||
Intent(c.applicationContext, VPNService::class.java).apply {
|
||||
putExtra("startOnly", true)
|
||||
})
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgGetConfig(handle: Int): String?
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgGetSocketV4(handle: Int): Int
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgGetSocketV6(handle: Int): Int
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgTurnOff(handle: Int)
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgTurnOn(ifName: String, tunFd: Int, settings: String): Int
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgVersion(): String?
|
||||
}
|
||||
|
||||
class VPNService : android.net.VpnService() {
|
||||
private val tag = "VPNService"
|
||||
private var mBinder: VPNServiceBinder = VPNServiceBinder(this)
|
||||
private var mConfig: JSONObject? = null
|
||||
private var mProtocol: String? = null
|
||||
|
@ -26,7 +145,11 @@ class VPNService : android.net.VpnService() {
|
|||
private var mbuilder: Builder = Builder()
|
||||
|
||||
private var mOpenVPNThreadv3: OpenVPNThreadv3? = null
|
||||
private var currentTunnelHandle = -1
|
||||
var currentTunnelHandle = -1
|
||||
|
||||
private var intent: Intent? = null
|
||||
private var flags = 0
|
||||
private var startId = 0
|
||||
|
||||
fun init() {
|
||||
if (mAlreadyInitialised) {
|
||||
|
@ -41,8 +164,16 @@ class VPNService : android.net.VpnService() {
|
|||
mAlreadyInitialised = true
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// Log.v(tag, "Aman: onCreate....................")
|
||||
// Log.v(tag, "Aman: onCreate....................")
|
||||
// Log.v(tag, "Aman: onCreate....................")
|
||||
// NotificationUtil.show(this) // Go foreground
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
Log.v(tag, "Got Unbind request")
|
||||
Log.v(tag, "Aman: onUnbind....................")
|
||||
if (!isUp) {
|
||||
// If the Qt Client got closed while we were not connected
|
||||
// we do not need to stay as a foreground service.
|
||||
|
@ -55,9 +186,21 @@ class VPNService : android.net.VpnService() {
|
|||
* EntryPoint for the Service, gets Called when AndroidController.cpp
|
||||
* calles bindService. Returns the [VPNServiceBinder] so QT can send Requests to it.
|
||||
*/
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
Log.v(tag, "Got Bind request")
|
||||
init()
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
Log.v(tag, "Aman: onBind....................")
|
||||
when (mProtocol) {
|
||||
"shadowsocks" -> {
|
||||
when (intent.action) {
|
||||
SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent)
|
||||
else -> super<LocalDnsService.Interface>.onBind(intent)
|
||||
}
|
||||
startTest()
|
||||
}
|
||||
else -> {
|
||||
init()
|
||||
}
|
||||
}
|
||||
|
||||
return mBinder
|
||||
}
|
||||
|
||||
|
@ -67,11 +210,16 @@ class VPNService : android.net.VpnService() {
|
|||
* or from Booting the device and having "connect on boot" enabled.
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.v(tag, "Aman: onStartCommand....................")
|
||||
this.intent = intent
|
||||
this.flags = flags
|
||||
this.startId = startId
|
||||
init()
|
||||
intent?.let {
|
||||
if (intent.getBooleanExtra("startOnly", false)) {
|
||||
if (!isUp && intent.getBooleanExtra("startOnly", false)) {
|
||||
Log.i(tag, "Start only!")
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
return START_REDELIVER_INTENT
|
||||
// return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
}
|
||||
// This start is from always-on
|
||||
|
@ -81,18 +229,39 @@ class VPNService : android.net.VpnService() {
|
|||
val lastConfString = prefs.getString("lastConf", "")
|
||||
if (lastConfString.isNullOrEmpty()) {
|
||||
// We have nothing to connect to -> Exit
|
||||
Log.e(tag,"VPN service was triggered without defining a Server or having a tunnel")
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
Log.e(tag, "VPN service was triggered without defining a Server or having a tunnel")
|
||||
return super<android.net.VpnService>.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
this.mConfig = JSONObject(lastConfString)
|
||||
}
|
||||
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
mProtocol = mConfig!!.getString("protocol")
|
||||
Log.e(tag, "mProtocol: $mProtocol")
|
||||
if (mProtocol.equals("shadowsocks", true)) {
|
||||
if (DataStore.serviceMode == modeVpn) {
|
||||
if (prepare(this) != null) {
|
||||
startActivity(
|
||||
Intent(
|
||||
this,
|
||||
VpnRequestActivity::class.java
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
} else {
|
||||
Log.e(tag, "Else part enter")
|
||||
// service?.startListeningForBandwidth(serviceCallback, 1000)
|
||||
Log.e(tag, "test")
|
||||
return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
}
|
||||
stopRunner()
|
||||
}
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
// Invoked when the application is revoked.
|
||||
// At this moment, the VPN interface is already deactivated by the system.
|
||||
override fun onRevoke() {
|
||||
Log.v(tag, "Aman: onRevoke....................")
|
||||
this.turnOff()
|
||||
super.onRevoke()
|
||||
}
|
||||
|
@ -145,6 +314,7 @@ class VPNService : android.net.VpnService() {
|
|||
}
|
||||
|
||||
fun turnOn(json: JSONObject?): Int {
|
||||
Log.v(tag, "Aman: turnOn....................")
|
||||
if (!checkPermissions()) {
|
||||
Log.e(tag, "turn on was called without no permissions present!")
|
||||
isUp = false
|
||||
|
@ -152,23 +322,31 @@ class VPNService : android.net.VpnService() {
|
|||
}
|
||||
Log.i(tag, "Permission okay")
|
||||
mConfig = json!!
|
||||
Log.i(tag, "Config: " + mConfig)
|
||||
Log.i(tag, "Config: $mConfig")
|
||||
mProtocol = mConfig!!.getString("protocol")
|
||||
Log.i(tag, "Protocol: " + mProtocol)
|
||||
Log.i(tag, "Protocol: $mProtocol")
|
||||
when (mProtocol) {
|
||||
"openvpn" -> startOpenVpn()
|
||||
"wireguard" -> startWireGuard()
|
||||
"shadowsocks" -> startShadowsocks()
|
||||
"openvpn" -> {
|
||||
startOpenVpn()
|
||||
}
|
||||
"wireguard" -> {
|
||||
startWireGuard()
|
||||
}
|
||||
"shadowsocks" -> {
|
||||
startShadowsocks()
|
||||
startTest()
|
||||
}
|
||||
else -> {
|
||||
Log.e(tag, "No protocol")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
NotificationUtil.show(this) // Go foreground
|
||||
NotificationUtil.show(this)
|
||||
return 1
|
||||
}
|
||||
|
||||
fun establish(): ParcelFileDescriptor? {
|
||||
Log.v(tag, "Aman: establish....................")
|
||||
mbuilder.allowFamily(OsConstants.AF_INET)
|
||||
mbuilder.allowFamily(OsConstants.AF_INET6)
|
||||
|
||||
|
@ -208,7 +386,9 @@ class VPNService : android.net.VpnService() {
|
|||
fun addHttpProxy(host: String, port: Int): Boolean {
|
||||
val proxyInfo = ProxyInfo.buildDirectProxy(host, port)
|
||||
Log.v(tag, "mbuilder.addHttpProxy($host, $port)")
|
||||
mbuilder.setHttpProxy(proxyInfo)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
mbuilder.setHttpProxy(proxyInfo)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -218,19 +398,22 @@ class VPNService : android.net.VpnService() {
|
|||
}
|
||||
|
||||
fun turnOff() {
|
||||
Log.v(tag, "Try to disable tunnel")
|
||||
Log.v(tag, "Aman: turnOff....................")
|
||||
when (mProtocol) {
|
||||
"wireguard" -> wgTurnOff(currentTunnelHandle)
|
||||
"openvpn" -> ovpnTurnOff()
|
||||
"shadowsocks" -> {
|
||||
stopRunner(false)
|
||||
stopTest()
|
||||
}
|
||||
else -> {
|
||||
Log.e(tag, "No protocol")
|
||||
}
|
||||
}
|
||||
currentTunnelHandle = -1
|
||||
stopForeground(true)
|
||||
|
||||
isUp = false
|
||||
stopSelf();
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
|
||||
|
@ -366,11 +549,89 @@ class VPNService : android.net.VpnService() {
|
|||
|
||||
private fun startShadowsocks() {
|
||||
Log.e(tag, "startShadowsocks method enters")
|
||||
if(mConfig != null) {
|
||||
try {
|
||||
if (mConfig != null) {
|
||||
try {
|
||||
Log.e(tag, "Config: $mConfig")
|
||||
|
||||
} catch(e: Exception) {
|
||||
Log.e(tag, "Error in startShadowsocks: $e")
|
||||
ProfileManager.clear()
|
||||
val profile = Profile()
|
||||
// val iter: Iterator<String> = mConfig!!.keys()
|
||||
// while (iter.hasNext()) {
|
||||
// val key = iter.next()
|
||||
// try {
|
||||
// val value: Any = mConfig!!.get(key)
|
||||
// Log.i(tag, "startShadowsocks: $key : $value")
|
||||
// } catch (e: JSONException) {
|
||||
// // Something went wrong!
|
||||
// }
|
||||
// }
|
||||
|
||||
val shadowsocksConfig = mConfig?.getJSONObject("shadowsocks_config_data")
|
||||
|
||||
if (shadowsocksConfig?.has("name") == true) {
|
||||
profile.name = shadowsocksConfig.getString("name")
|
||||
} else {
|
||||
profile.name = "amnezia"
|
||||
}
|
||||
if (shadowsocksConfig?.has("method") == true) {
|
||||
profile.method = shadowsocksConfig.getString("method").toString()
|
||||
}
|
||||
if (shadowsocksConfig?.has("server") == true) {
|
||||
profile.host = shadowsocksConfig.getString("server").toString()
|
||||
}
|
||||
if (shadowsocksConfig?.has("password") == true) {
|
||||
profile.password = shadowsocksConfig.getString("password").toString()
|
||||
}
|
||||
if (shadowsocksConfig?.has("server_port") == true) {
|
||||
profile.remotePort = shadowsocksConfig.getInt("server_port")
|
||||
}
|
||||
// if(mConfig?.has("local_port") == true) {
|
||||
// profile. = mConfig?.getInt("local_port")
|
||||
// }
|
||||
// profile.name = "amnezia"
|
||||
// profile.method = "chacha20-ietf-poly1305"
|
||||
// profile.host = "de01-ss.sshocean.net"
|
||||
// profile.password = "ZTZhN"
|
||||
// profile.remotePort = 8388
|
||||
|
||||
profile.proxyApps = false
|
||||
profile.bypass = false
|
||||
profile.metered = false
|
||||
profile.dirty = false
|
||||
profile.ipv6 = true
|
||||
|
||||
DataStore.profileId = ProfileManager.createProfile(profile).id
|
||||
val switchProfile = Core.switchProfile(DataStore.profileId)
|
||||
Log.i(tag, "startShadowsocks: SwitchProfile: $switchProfile")
|
||||
intent?.putExtra("startOnly", false)
|
||||
onStartCommand(
|
||||
intent,
|
||||
flags,
|
||||
startId
|
||||
)
|
||||
// startRunner()
|
||||
// VpnManager.getInstance().run()
|
||||
// VpnManager.getInstance()
|
||||
// .setOnStatusChangeListener(object : VpnManager.OnStatusChangeListener {
|
||||
// override fun onStatusChanged(state: BaseService.State) {
|
||||
// when (state) {
|
||||
// BaseService.State.Connected -> {
|
||||
// isUp = true
|
||||
// }
|
||||
// BaseService.State.Stopped -> {
|
||||
// isUp = false
|
||||
// }
|
||||
// else -> {}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onTrafficUpdated(profileId: Long, stats: TrafficStats) {
|
||||
//
|
||||
// }
|
||||
// })
|
||||
//// Core.startService()
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Error in startShadowsocks: $e")
|
||||
}
|
||||
} else {
|
||||
Log.e(tag, "Invalid config file!!")
|
||||
|
@ -386,19 +647,20 @@ class VPNService : android.net.VpnService() {
|
|||
|
||||
private fun startWireGuard() {
|
||||
val wireguard_conf = buildWireugardConfig(mConfig!!)
|
||||
Log.i(tag, "startWireGuard: wireguard_conf : $wireguard_conf")
|
||||
if (currentTunnelHandle != -1) {
|
||||
Log.e(tag, "Tunnel already up")
|
||||
// Turn the tunnel down because this might be a switch
|
||||
wgTurnOff(currentTunnelHandle)
|
||||
}
|
||||
val wgConfig: String = wireguard_conf!!.toWgUserspaceString()
|
||||
val wgConfig: String = wireguard_conf.toWgUserspaceString()
|
||||
val builder = Builder()
|
||||
setupBuilder(wireguard_conf, builder)
|
||||
builder.setSession("avpn0")
|
||||
builder.setSession("Amnezia")
|
||||
builder.establish().use { tun ->
|
||||
if (tun == null) return
|
||||
Log.i(tag, "Go backend " + wgVersion())
|
||||
currentTunnelHandle = wgTurnOn("avpn0", tun.detachFd(), wgConfig)
|
||||
currentTunnelHandle = wgTurnOn("Amnezia", tun.detachFd(), wgConfig)
|
||||
}
|
||||
if (currentTunnelHandle < 0) {
|
||||
Log.e(tag, "Activation Error Code -> $currentTunnelHandle")
|
||||
|
@ -417,31 +679,163 @@ class VPNService : android.net.VpnService() {
|
|||
.apply()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun startService(c: Context) {
|
||||
c.applicationContext.startService(
|
||||
Intent(c.applicationContext, VPNService::class.java).apply {
|
||||
putExtra("startOnly", true)
|
||||
})
|
||||
override suspend fun startProcesses() {
|
||||
worker = ProtectWorker().apply { start() }
|
||||
try {
|
||||
Log.i(tag, "startProcesses: ------------------1")
|
||||
super.startProcesses()
|
||||
Log.i(tag, "startProcesses: ------------------2")
|
||||
sendFd(startVpn())
|
||||
Log.i(tag, "startProcesses: ------------------3")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgGetConfig(handle: Int): String?
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgGetSocketV4(handle: Int): Int
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgGetSocketV6(handle: Int): Int
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgTurnOff(handle: Int)
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgTurnOn(ifName: String, tunFd: Int, settings: String): Int
|
||||
|
||||
@JvmStatic
|
||||
private external fun wgVersion(): String?
|
||||
}
|
||||
|
||||
override fun killProcesses(scope: CoroutineScope) {
|
||||
super.killProcesses(scope)
|
||||
active = false
|
||||
scope.launch { DefaultNetworkListener.stop(this) }
|
||||
worker?.shutdown(scope)
|
||||
worker = null
|
||||
conn?.close()
|
||||
conn = null
|
||||
}
|
||||
|
||||
private suspend fun startVpn(): FileDescriptor {
|
||||
val profile = data.proxy!!.profile
|
||||
Log.i(tag, "startVpn: -----------------------1")
|
||||
val builder = Builder()
|
||||
.setConfigureIntent(Core.configureIntent(this))
|
||||
.setSession(profile.formattedName)
|
||||
.setMtu(VPN_MTU)
|
||||
.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
||||
.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||
Log.i(tag, "startVpn: -----------------------2")
|
||||
if (profile.ipv6) {
|
||||
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
||||
builder.addRoute("::", 0)
|
||||
}
|
||||
Log.i(tag, "startVpn: -----------------------3")
|
||||
val me = packageName
|
||||
if (profile.proxyApps) {
|
||||
profile.individual.split('\n')
|
||||
.filter { it != me }
|
||||
.forEach {
|
||||
try {
|
||||
if (profile.bypass) builder.addDisallowedApplication(it)
|
||||
else builder.addAllowedApplication(it)
|
||||
} catch (ex: PackageManager.NameNotFoundException) {
|
||||
printLog(ex)
|
||||
}
|
||||
}
|
||||
if (profile.bypass) {
|
||||
builder.addDisallowedApplication(me)
|
||||
}
|
||||
} else {
|
||||
builder.addDisallowedApplication(me)
|
||||
}
|
||||
Log.i(tag, "startVpn: -----------------------4")
|
||||
when (profile.route) {
|
||||
Acl.ALL, Acl.BYPASS_CHN, Acl.CUSTOM_RULES -> builder.addRoute("0.0.0.0", 0)
|
||||
else -> {
|
||||
resources.getStringArray(R.array.bypass_private_route).forEach {
|
||||
val subnet = Subnet.fromString(it)!!
|
||||
builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
|
||||
}
|
||||
builder.addRoute(PRIVATE_VLAN4_ROUTER, 32)
|
||||
}
|
||||
}
|
||||
Log.i(tag, "startVpn: -----------------------5")
|
||||
metered = profile.metered
|
||||
active = true // possible race condition here?
|
||||
Log.i(tag, "startVpn: -----------------------6")
|
||||
builder.setUnderlyingNetworks(underlyingNetworks)
|
||||
Log.i(tag, "startVpn: -----------------------7")
|
||||
val conn = builder.establish() ?: throw NullConnectionException()
|
||||
Log.i(tag, "startVpn: -----------------------8")
|
||||
this.conn = conn
|
||||
Log.i(tag, "startVpn: -----------------------9")
|
||||
val cmd = arrayListOf(
|
||||
File(applicationInfo.nativeLibraryDir, Executable.TUN2SOCKS).absolutePath,
|
||||
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,
|
||||
"--socks-server-addr", "${DataStore.listenAddress}:${DataStore.portProxy}",
|
||||
"--tunmtu", VPN_MTU.toString(),
|
||||
"--sock-path", "sock_path",
|
||||
"--dnsgw", "127.0.0.1:${DataStore.portLocalDns}",
|
||||
"--loglevel", "warning"
|
||||
)
|
||||
Log.i(tag, "startVpn: -----------------------10")
|
||||
if (profile.ipv6) {
|
||||
cmd += "--netif-ip6addr"
|
||||
cmd += PRIVATE_VLAN6_ROUTER
|
||||
}
|
||||
Log.i(tag, "startVpn: -----------------------11")
|
||||
cmd += "--enable-udprelay"
|
||||
Log.i(tag, "startVpn: -----------------------12")
|
||||
data.processes!!.start(cmd, onRestartCallback = {
|
||||
try {
|
||||
sendFd(conn.fileDescriptor)
|
||||
} catch (e: ErrnoException) {
|
||||
e.printStackTrace()
|
||||
stopRunner(false, e.message)
|
||||
}
|
||||
})
|
||||
Log.i(tag, "startVpn: -----------------------13")
|
||||
return conn.fileDescriptor
|
||||
}
|
||||
|
||||
private suspend fun sendFd(fd: FileDescriptor) {
|
||||
var tries = 0
|
||||
val path = File(Core.deviceStorage.noBackupFilesDir, "sock_path").absolutePath
|
||||
while (true) try {
|
||||
delay(50L shl tries)
|
||||
LocalSocket().use { localSocket ->
|
||||
localSocket.connect(
|
||||
LocalSocketAddress(
|
||||
path,
|
||||
LocalSocketAddress.Namespace.FILESYSTEM
|
||||
)
|
||||
)
|
||||
localSocket.setFileDescriptorsForSend(arrayOf(fd))
|
||||
localSocket.outputStream.write(42)
|
||||
}
|
||||
return
|
||||
} catch (e: IOException) {
|
||||
if (tries > 5) throw e
|
||||
tries += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private inner class ProtectWorker : ConcurrentLocalSocketListener(
|
||||
"ShadowsocksVpnThread",
|
||||
File(Core.deviceStorage.noBackupFilesDir, "protect_path")
|
||||
) {
|
||||
override fun acceptInternal(socket: LocalSocket) {
|
||||
socket.inputStream.read()
|
||||
val fd = socket.ancillaryFileDescriptors!!.single()!!
|
||||
CloseableFd(fd).use {
|
||||
socket.outputStream.write(if (underlyingNetwork.let { network ->
|
||||
if (network != null && Build.VERSION.SDK_INT >= 23) try {
|
||||
network.bindSocket(fd)
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
// suppress ENONET (Machine is not on the network)
|
||||
if ((e.cause as? ErrnoException)?.errno != 64) printLog(e)
|
||||
false
|
||||
} else protect(getInt.invoke(fd) as Int)
|
||||
}) 0 else 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class NullConnectionException : NullPointerException() {
|
||||
override fun getLocalizedMessage() = getString(R.string.reboot_required)
|
||||
}
|
||||
|
||||
class CloseableFd(val fd: FileDescriptor) : Closeable {
|
||||
override fun close() = Os.close(fd)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue