Android shadowsocks code added

This commit is contained in:
aman 2022-04-01 10:05:58 +05:30
parent ccdd433e35
commit 929bcf03a0
92 changed files with 39982 additions and 1702 deletions

View file

@ -15,7 +15,8 @@
<!-- %%INSERT_FEATURES -->
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
<application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="-- %%INSERT_APP_NAME%% --" android:extractNativeLibs="true" android:icon="@drawable/icon">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:hardwareAccelerated="true" android:label="-- %%INSERT_APP_NAME%% --" android:extractNativeLibs="true" android:icon="@drawable/icon">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="-- %%INSERT_APP_NAME%% --" android:screenOrientation="unspecified" android:launchMode="singleTop" android:theme="@style/splashScreenTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@ -76,17 +77,21 @@
<!-- extract android style -->
<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splashscreen"/>
</activity>
<service android:name=".VPNService"
android:process=":QtOnlyProcess"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService"/>
</intent-filter>
<service android:name=".VPNService" android:process=":QtOnlyProcess">
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
<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"/>
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
<meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
<meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
<meta-data android:name="android.app.load_local_libs_resource_id" android:resource="@array/load_local_libs"/>
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
</service>
<service android:name="org.amnezia.vpn.qt.VPNPermissionHelper"
android:permission="android.permission.BIND_VPN_SERVICE">
<service android:name="org.amnezia.vpn.qt.VPNPermissionHelper">
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
</service>
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
</application>

View file

@ -1,11 +0,0 @@
package com.github.shadowsocks.aidl;
import com.github.shadowsocks.aidl.TrafficStats;
//"oneway" unexpected. xinlake
interface IShadowsocksServiceCallback {
oneway void stateChanged(int state, String profileName, String msg);
oneway void trafficUpdated(long profileId, in TrafficStats stats);
// Traffic data has persisted to database, listener should refetch their data from database
oneway void trafficPersisted(long profileId);
}

View file

@ -1,3 +0,0 @@
package com.github.shadowsocks.aidl;
parcelable TrafficStats;

View file

@ -1,6 +1,6 @@
package com.github.shadowsocks.aidl;
package org.amnezia.vpn.shadowsocks.core.aidl;
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback;
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback;
interface IShadowsocksService {
int getState();

View file

@ -0,0 +1,18 @@
package org.amnezia.vpn.shadowsocks.core.aidl;
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats;
//"oneway" unexpected. xinlake
interface IShadowsocksServiceCallback {
oneway void stateChanged(int state, String profileName, String msg);
oneway void trafficUpdated(long profileId, in TrafficStats stats);
// Traffic data has persisted to database, listener should refetch their data from database
oneway void trafficPersisted(long profileId);
}
//oneway interface IShadowsocksServiceCallback {
// void stateChanged(int state, String profileName, String msg);
// void trafficUpdated(long profileId, in TrafficStats stats);
// // Traffic data has persisted to database, listener should refetch their data from database
// void trafficPersisted(long profileId);
//}

View file

@ -0,0 +1,3 @@
package org.amnezia.vpn.shadowsocks.core.aidl;
parcelable TrafficStats;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -7,13 +7,10 @@
127.0.0.0/8
169.254.0.0/16
172.16.0.0/12
192.0.0.0/24
192.0.0.0/29
192.0.2.0/24
192.31.196.0/24
192.52.193.0/24
192.88.99.0/24
192.168.0.0/16
192.175.48.0/24
198.18.0.0/15
198.51.100.0/24
203.0.113.0/24

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,8 @@
apply plugin: 'com.github.ben-manes.versions'
buildscript {
ext{
kotlin_version = "1.5.0"
kotlin_version = "1.4.30-M1"
// for libwg
appcompatVersion = '1.1.0'
annotationsVersion = '1.0.1'
@ -19,6 +21,8 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
@ -34,24 +38,44 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization'
apply plugin: 'kotlin-kapt'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'androidx.core:core-ktx:1.1.0'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-alpha02"
//implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-alpha02"
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"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"
//ss
implementation "androidx.preference:preference:1.1.0"
implementation "androidx.work:work-runtime-ktx:2.3.4"
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 "dnsjava:dnsjava:2.1.9"
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"
annotationProcessor "androidx.room:room-compiler:2.3.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 {
experimental = true
}
android {
@ -86,6 +110,7 @@ android {
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
androidTest.assets.srcDirs += files("${qt5AndroidDir}/schemas".toString())
}
}
@ -114,6 +139,10 @@ android {
targetSdkVersion = 30
versionCode 8 // Change to a higher number
versionName "2.0.8" // Change to a higher number
javaCompileOptions.annotationProcessorOptions.arguments = [
"room.schemaLocation": "${qt5AndroidDir}/schemas".toString()
]
}
buildTypes {
@ -136,18 +165,6 @@ android {
}
}
}
// externalNativeBuild {
// cmake {
// path 'wireguard/CMakeLists.txt'
// }
// }
// externalNativeBuild {
// cmake {
// path 'openvpn/src/main/cpp/CMakeLists.txt'
// }
// }
}

View file

@ -3,7 +3,8 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m
#org.gradle.jvmargs=-Xmx2048m
org.gradle.jvmargs=-Xmx1536m
# Gradle caching allows reusing the build artifacts from a previous
# build with the same inputs. However, over time, the cache size will
@ -21,3 +22,7 @@ androidBuildToolsVersion=30.0.2
androidCompileSdkVersion=30
org.gradle.caching=true
org.gradle.parallel=true
android.enableJetifier=true
android.injected.testOnly=false
kapt.use.worker.api=false
kapt.incremental.apt=false

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -1,113 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Shadowsocks</string>
<string name="send_email">Send email</string>
<string name="app_name">shadowsocks</string>
<!-- ssplugin -->
<string name="proxy_cat">Server Settings</string>
<string name="feature_cat">Feature Settings</string>
<string name="unsaved_changes_prompt">Changes not saved. Do you want to save?</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="apply">Apply</string>
<string name="file_manager_missing">File Explorer Missing</string>
<string name="browse">Browse…</string>
<string name="service_mode_vpn">VPN</string>
<string name="speed">%s/s</string>
<string name="quick_toggle">"Switch"</string>
<string name="remote_dns">"Remote DNS"</string>
<string name="stat_summary">"Upload: \t%3$s\t↑\t%1$s
Download: \t%4$s\t↓\t%2$s"</string>
<string name="connection_test_testing">"Testing…"</string>
<string name="connection_test_available">"Connection successful: HTTPS handshake delay %d milliseconds"</string>
<string name="connection_test_error">"Failed: %s"</string>
<string name="connection_test_fail">"No Internet Connection"</string>
<string name="connection_test_error_status_code">"Invalid status code (#%d) "</string>
<!-- proxy category -->
<string name="profile_name">Profile Name</string>
<string name="proxy">Server</string>
<string name="remote_port">Remote Port</string>
<string name="sitekey">Password</string>
<string name="enc_method">Encrypt Method</string>
<string name="profile_name">"Profile name"</string>
<string name="proxy">"Server"</string>
<string name="remote_port">"Remote Port"</string>
<string name="sitekey">"Password"</string>
<string name="enc_method">"Encryption"</string>
<!-- feature category -->
<string name="ipv6">IPv6 Route</string>
<string name="ipv6_summary">Redirect IPv6 traffic to remote</string>
<string name="on">On</string>
<string name="off">Off</string>
<string name="tcp_fastopen_summary">Toggling might require ROOT permission</string>
<string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s &lt; 3.7.1</string>
<string name="tcp_fastopen_failure">Toggle failed</string>
<string name="udp_dns">Send DNS over UDP</string>
<string name="udp_dns_summary">Requires UDP forwarding on server side</string>
<string name="ipv6">"IPv6 routing"</string>
<string name="ipv6_summary">"Forward IPv6 traffic to remote server"</string>
<string name="route_list">"Routing"</string>
<string name="route_entry_gfwlist">"GFW List"</string>
<string name="proxied_apps">"Proxied VPN"</string>
<string name="proxied_apps_summary">"Allow some apps to bypass VPN"</string>
<string name="on">"On"</string>
<string name="bypass_apps">"Bypass"</string>
<string name="bypass_apps_summary">"Bypass selected apps"</string>
<string name="auto_connect">"Auto connect"</string>
<string name="auto_connect_summary">"Allow Shadowsocks to start with the system"</string>
<string name="tcp_fastopen_summary">"Switching may require ROOT permissions"</string>
<string name="tcp_fastopen_summary_unsupported">"Unsupported kernel version: %s &lt; 3.7.1"</string>
<string name="udp_dns">"Using UDP DNS"</string>
<string name="udp_dns_summary">"Requires remote server to support UDP forwarding"</string>
<!-- notification category -->
<string name="service_vpn">VPN Service</string>
<string name="forward_success">Shadowsocks started.</string>
<string name="invalid_server">Invalid server name</string>
<string name="service_failed">Failed to connect the remote server</string>
<string name="stop">Stop</string>
<string name="stopping">Shutting down…</string>
<string name="vpn_error">%s</string>
<string name="vpn_permission_denied">Permission denied to create a VPN service</string>
<string name="reboot_required">Failed to start VPN service. You might need to reboot your device.</string>
<string name="profile_invalid_input">No valid profile data found.</string>
<string name="forward_success">"Background service has started running. "</string>
<string name="invalid_server">"Invalid server name"</string>
<string name="service_failed">"Unable to connect to remote server"</string>
<string name="stop">"Stop"</string>
<string name="stopping">"stopping…"</string>
<string name="vpn_error">"Background service failed to start: %s"</string>
<string name="reboot_required">"VPN service failed to start. You may need to restart your device."</string>
<string name="profile_invalid_input">"No valid configuration file found."</string>
<!-- alert category -->
<string name="profile_empty">Please select a profile</string>
<string name="proxy_empty">Proxy/Password should not be empty</string>
<string name="connect">Connect</string>
<string name="profile_empty">"Please select a profile"</string>
<string name="proxy_empty">"The proxy server address and password cannot be empty"</string>
<string name="connect">"Connect"</string>
<!-- menu category -->
<string name="profiles">Profiles</string>
<string name="settings">Settings</string>
<string name="about">About</string>
<string name="about_title">Shadowsocks %s</string>
<string name="edit">Edit</string>
<string name="share">Share</string>
<string name="add_profile">Add Profile</string>
<string name="action_apply_all">Apply Settings to All Profiles</string>
<string name="action_export_more">Export…</string>
<string name="action_export_file">Export to file…</string>
<string name="action_export">Export to Clipboard</string>
<string name="action_import">Import from Clipboard</string>
<string name="action_import_file">Import from file…</string>
<string name="action_replace_file">Replace from file…</string>
<string name="action_export_msg">Successfully export!</string>
<string name="action_export_err">Failed to export.</string>
<string name="action_import_msg">Successfully import!</string>
<string name="action_import_err">Failed to import.</string>
<string name="action_fetch_location">Fetch location</string>
<string name="profiles">"Profiles"</string>
<string name="settings">"Settings"</string>
<string name="faq">"FAQ"</string>
<string name="about">"About"</string>
<string name="about_title">"Shadowsocks %s"</string>
<string name="edit">"Edit"</string>
<string name="share">"Share"</string>
<string name="add_profile">"Add Profile"</string>
<string name="action_apply_all">"Apply settings to all profiles"</string>
<string name="action_export">"Export to clipboard"</string>
<string name="action_import">"Import from clipboard"</string>
<string name="action_export_msg">"Export to clipboard succeeded"</string>
<string name="action_export_err">"Export to clipboard failed"</string>
<string name="action_import_msg">"Import successful"</string>
<string name="action_import_err">"Import failed"</string>
<!-- profile -->
<string name="profile_config">Profile config</string>
<string name="delete">Remove</string>
<string name="delete_confirm_prompt">Are you sure you want to remove this profile?</string>
<string name="share_qr_nfc">QR code</string>
<string name="add_profile_dialog">Add this Shadowsocks Profile?</string>
<string name="add_profile_methods_scan_qr_code">Scan QR code</string>
<string name="add_profile_methods_manual_settings">Manual Settings</string>
<string name="add_profile_scanner_permission_required">Camera permission is required for scanning QR code.</string>
<string name="undo">Undo</string>
<string name="profile_config">"Profile Config"</string>
<string name="delete">"Delete"</string>
<string name="delete_confirm_prompt">"Are you sure you want to delete this profile?"</string>
<string name="share_qr_nfc">"QR code / NFC"</string>
<string name="add_profile_dialog">"Add this profile for Shadowsock?"</string>
<string name="add_profile_methods_scan_qr_code">"Scan QR code"</string>
<plurals name="removed">
<item quantity="other">"%d items deleted"</item>
</plurals>
<string name="undo">"Undo"</string>
<!-- tasker -->
<string name="toggle_service_state">"Start service"</string>
<string name="start_service_default">"Connect to the current server"</string>
<string name="start_service">"Connect to %s"</string>
<string name="stop_service">"Switch to %s"</string>
<string name="profile_default">"Use current profile"</string>
<!-- status -->
<string name="connecting">Connecting…</string>
<string name="vpn_connected">Connected, tap to check connection</string>
<string name="not_connected">Not connected</string>
<string name="sent">"Send: "</string>
<string name="received">"Received"</string>
<string name="sent">Sent</string>
<string name="received">Received</string>
<!-- status -->
<string name="connecting">"connecting…"</string>
<string name="vpn_connected">"Connected, click Test Connection"</string>
<string name="not_connected">"Not connected"</string>
<!-- acl -->
<string name="custom_rules">"Custom rules"</string>
<string name="action_add_rule">"Add rule…"</string>
<string name="edit_rule">"Edit rules"</string>
<string name="route_entry_all">"Global"</string>
<string name="route_entry_bypass_lan">"Bypass LAN addresses"</string>
<string name="route_entry_bypass_chn">"Bypass mainland China addresses"</string>
<string name="route_entry_bypass_lan_chn">"Bypass LAN and Mainland China addresses"</string>
<string name="route_entry_chinalist">"Proxy only for mainland China addresses"</string>
<string name="acl_rule_templates_generic">"Subnet/Domain PCRE Regular Expression"</string>
<string name="acl_rule_templates_domain">"Domain names and their subdomains"</string>
<!-- plugin -->
<string name="plugin">"Plugin"</string>
<string name="plugin_configure">"Configure…"</string>
<string name="plugin_disabled">"Disabled"</string>
<string name="plugin_unknown">"Unknown plugin %s"</string>
<string name="plugin_untrusted">"Warning: This plugin does not appear to be from a known trusted source."</string>
<string name="profile_plugin">"Plugin: %s"</string>
<string name="add_profile_scanner_permission_required">"Scanning the QR code requires permission to use the camera."</string>
<!-- notification category -->
<string name="service_vpn">"VPN service"</string>
<string name="add_profile_methods_manual_settings">"Manual setting"</string>
<!-- misc -->
<string name="add_first_profile">There is no profile currently, would you like to add it now?</string>
<string name="port_proxy">SOCKS5 proxy port</string>
<string name="port_local_dns">Local DNS port</string>
<string name="advanced">"Advanced options"</string>
<string name="quick_toggle">Toggle</string>
<string name="remote_dns">Remote DNS</string>
<string name="stat_summary">Sent: \t\t\t\t\t%3$s\t↑\t%1$s\nReceived: \t%4$s\t↓\t%2$s</string>
<string name="connection_test_pending">Check Connectivity</string>
<string name="connection_test_testing">Testing…</string>
<string name="connection_test_available">Success: HTTPS handshake took %dms</string>
<string name="connection_test_error">Fail to detect internet connection: %s</string>
<string name="connection_test_fail">Internet Unavailable</string>
<string name="connection_test_error_status_code">Error code: #%d</string>
<string name="speed" translatable="false">%s/s</string>
<string name="traffic" translatable="false">%1$s↑\t%2$s↓</string>
<plurals name="removed">
<item quantity="one">Removed</item>
<item quantity="other">%d items removed</item>
</plurals>
<!-- misc -->
<string name="service_mode">"Service mode"</string>
<string name="service_mode_proxy">"Proxy only"</string>
<string name="service_mode_transproxy">"Transparent proxy"</string>
<string name="port_proxy">"SOCKS5 proxy port"</string>
<string name="port_local_dns">"local DNS port"</string>
<string name="port_transproxy">"Transparent proxy port"</string>
<string name="service_proxy">"Proxy mode"</string>
<string name="service_transproxy">"Transparent proxy mode"</string>
<string name="vpn_permission_denied">"Insufficient permission to create VPN service"</string>
<string name="auto_connect_summary_v24">"Allow Shadowsocks to start with the system, an always-on VPN is recommended"</string>
<string name="direct_boot_aware">"Allow toggle on lock screen"</string>
<string name="direct_boot_aware_summary">"The selected configuration information will be less secure"</string>
<string name="acl_rule_online_config">"Online Rules File URL"</string>
<string name="action_import_file">"Import from file…"</string>
<string name="night_mode">"Night Mode"</string>
<string name="night_mode_system">"System"</string>
<string name="night_mode_auto">"Auto"</string>
<string name="night_mode_on">"On"</string>
<string name="night_mode_off">"Off"</string>
<string name="send_email">"Send email"</string>
<string name="action_export_more">"Export…"</string>
<string name="action_export_file">"Export to file…"</string>
<string name="cleartext_http_warning">"HTTP clear text traffic is not secure"</string>
<string name="share_over_lan">"Share via LAN"</string>
<string name="connection_test_pending">"Check connection"</string>
<string name="file_manager_missing">"Please install a file manager such as MiXplorer"</string>
<string name="tcp_fastopen_failure">"Failed to switch"</string>
<string name="udp_fallback">"UDP configuration"</string>
<string name="action_replace_file">"Replace from file…"</string>
<string name="off">"Off"</string>
<string name="proxied_apps_mode">"model"</string>
<string name="proxy_cat">"Server settings"</string>
<string name="feature_cat">"Function settings"</string>
<string name="unsaved_changes_prompt">"Do you want to save the changes?"</string>
<string name="yes">"Yes"</string>
<string name="no">"No"</string>
<string name="apply">"Apply"</string>
</resources>

View file

@ -0,0 +1,132 @@
{
"formatVersion": 1,
"database": {
"version": 1000,
"identityHash": "14b379f7776710b79b9d617090efe40e",
"entities": [
{
"tableName": "Profile",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remotePort",
"columnName": "remotePort",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "method",
"columnName": "method",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remoteDns",
"columnName": "remoteDns",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "udpdns",
"columnName": "udpdns",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ipv6",
"columnName": "ipv6",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "tx",
"columnName": "tx",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "rx",
"columnName": "rx",
"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, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "valueType",
"columnName": "valueType",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "BLOB",
"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, '14b379f7776710b79b9d617090efe40e')"
]
}
}

View file

@ -0,0 +1,46 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "f1aab1fb633378621635c344dbc8ac7b",
"entities": [
{
"tableName": "KeyValuePair",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "valueType",
"columnName": "valueType",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "BLOB",
"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')"
]
}
}

View file

@ -1,215 +0,0 @@
package com.github.shadowsocks
import android.app.Service
import android.content.Intent
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.net.Network
import android.net.VpnService
import android.os.Build
import android.os.ParcelFileDescriptor
import android.system.ErrnoException
import android.system.Os
import com.github.shadowsocks.bg.*
import com.github.shadowsocks.net.ConcurrentLocalSocketListener
import com.github.shadowsocks.net.DefaultNetworkListener
import com.github.shadowsocks.net.HostsFile
import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.printLog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.Closeable
import java.io.File
import java.io.FileDescriptor
import java.io.IOException
import java.net.URL
import org.amnezia.vpn.R
class LocalVpnService : VpnService(), LocalDnsService.Interface {
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$")
}
class CloseableFd(val fd: FileDescriptor) : Closeable {
override fun close() = Os.close(fd)
}
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(), BaseService.ExpectedException {
override fun getLocalizedMessage() = getString(R.string.reboot_required)
}
override val data = BaseService.Data(this)
override val tag: String get() = "ShadowsocksVpnService"
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
// metered = false. xinlake
private var underlyingNetwork: Network? = null
set(value) {
field = value
if (active) setUnderlyingNetworks(underlyingNetworks)
}
private val underlyingNetworks
get() = // clearing underlyingNetworks makes Android 9 consider the network to be metered
underlyingNetwork?.let { arrayOf(it) }
override fun onBind(intent: Intent) = when (intent.action) {
SERVICE_INTERFACE -> super<VpnService>.onBind(intent)
else -> super<LocalDnsService.Interface>.onBind(intent)
}
override fun onRevoke() = stopRunner()
override fun killProcesses(scope: CoroutineScope) {
super.killProcesses(scope)
active = false
scope.launch { DefaultNetworkListener.stop(this) }
worker?.shutdown(scope)
worker = null
conn?.close()
conn = null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (prepare(this) != null) {
// startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
} else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
stopRunner()
return Service.START_NOT_STICKY
}
override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
override suspend fun resolver(host: String) =
DnsResolverCompat.resolve(DefaultNetworkListener.get(), host)
override suspend fun openConnection(url: URL) = DefaultNetworkListener.get().openConnection(url)
override suspend fun startProcesses(hosts: HostsFile) {
worker = ProtectWorker().apply { start() }
super.startProcesses(hosts)
sendFd(startVpn())
}
override fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> {
cmd += "-V"
return cmd
}
private suspend fun startVpn(): FileDescriptor {
val profile = data.proxy!!.profile
val builder = Builder()
.setConfigureIntent(Core.configureIntent(this))
.setSession(profile.formattedName)
.setMtu(VPN_MTU)
.addAddress(PRIVATE_VLAN4_CLIENT, 30)
.addDnsServer(PRIVATE_VLAN4_ROUTER)
if (profile.ipv6) {
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
builder.addRoute("::", 0)
}
// XinLake. bypass lan
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)
active = true // possible race condition here?
if (Build.VERSION.SDK_INT >= 22) {
builder.setUnderlyingNetworks(underlyingNetworks)
}
val conn = builder.establish() ?: throw NullConnectionException()
this.conn = conn
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"
)
if (profile.ipv6) {
cmd += "--netif-ip6addr"
cmd += PRIVATE_VLAN6_ROUTER
}
cmd += "--enable-udprelay"
data.processes!!.start(cmd, onRestartCallback = {
try {
sendFd(conn.fileDescriptor)
} catch (e: ErrnoException) {
stopRunner(false, e.message)
}
})
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
}
}
override fun onDestroy() {
super.onDestroy()
data.binder.close()
}
}

View file

@ -1,99 +0,0 @@
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 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 com.github.shadowsocks.bg
import android.annotation.TargetApi
import android.app.ActivityManager
import android.net.DnsResolver
import android.net.Network
import android.os.Build
import android.os.CancellationSignal
import androidx.core.content.getSystemService
import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import kotlinx.coroutines.*
import java.io.IOException
import java.net.InetAddress
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
sealed class DnsResolverCompat {
companion object : DnsResolverCompat() {
private val instance by lazy { if (Build.VERSION.SDK_INT >= 29) DnsResolverCompat29 else DnsResolverCompat21 }
override suspend fun resolve(network: Network, host: String) =
instance.resolve(network, host)
override suspend fun resolveOnActiveNetwork(host: String) =
instance.resolveOnActiveNetwork(host)
}
abstract suspend fun resolve(network: Network, host: String): Array<InetAddress>
abstract suspend fun resolveOnActiveNetwork(host: String): Array<InetAddress>
private object DnsResolverCompat21 : DnsResolverCompat() {
/**
* This dispatcher is used for noncancellable possibly-forever-blocking operations in network IO.
*
* See also: https://issuetracker.google.com/issues/133874590
*/
private val unboundedIO by lazy {
if (app.getSystemService<ActivityManager>()!!.isLowRamDevice) Dispatchers.IO
else Executors.newCachedThreadPool().asCoroutineDispatcher()
}
override suspend fun resolve(network: Network, host: String) =
GlobalScope.async(unboundedIO) { network.getAllByName(host) }.await()
override suspend fun resolveOnActiveNetwork(host: String) =
GlobalScope.async(unboundedIO) { InetAddress.getAllByName(host) }.await()
}
@TargetApi(29)
private object DnsResolverCompat29 : DnsResolverCompat(), Executor {
/**
* This executor will run on its caller directly. On Q beta 3 thru 4, this results in calling in main thread.
*/
override fun execute(command: Runnable) = command.run()
override suspend fun resolve(network: Network, host: String): Array<InetAddress> {
return suspendCancellableCoroutine { cont ->
val signal = CancellationSignal()
cont.invokeOnCancellation { signal.cancel() }
// retry should be handled by client instead
DnsResolver.getInstance().query(network, host, DnsResolver.FLAG_NO_RETRY, this, signal,
object : DnsResolver.Callback<Collection<InetAddress>> {
override fun onAnswer(answer: Collection<InetAddress>, rcode: Int) =
cont.resume(answer.toTypedArray())
override fun onError(error: DnsResolver.DnsException) =
cont.resumeWithException(IOException(error))
})
}
}
override suspend fun resolveOnActiveNetwork(host: String): Array<InetAddress> {
return resolve(Core.connectivity.activeNetwork ?: return emptyArray(), host)
}
}
}

View file

@ -1,95 +0,0 @@
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 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 com.github.shadowsocks.bg
import android.content.Context
import com.github.shadowsocks.Core
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.net.HostsFile
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.parseNumericAddress
import kotlinx.coroutines.CoroutineScope
import java.io.File
import java.io.IOException
import java.net.UnknownHostException
/**
* This class sets up environment for ss-local.
*/
class ProxyInstance(val profile: Profile) {
private var configFile: File? = null
var trafficMonitor: TrafficMonitor? = null
fun getFile(context: Context = Core.deviceStorage) =
File(context.noBackupFilesDir, "bypass-lan.acl")
suspend fun init(service: BaseService.Interface, hosts: HostsFile) {
// it's hard to resolve DNS on a specific interface so we'll do it here
if (profile.host.parseNumericAddress() == null) {
profile.host = (hosts.resolve(profile.host).firstOrNull() ?: try {
service.resolver(profile.host).firstOrNull()
} catch (_: IOException) {
null
})?.hostAddress ?: throw UnknownHostException()
}
}
/**
* Sensitive shadowsocks configuration file requires extra protection. It may be stored in encrypted storage or
* device storage, depending on which is currently available.
*/
fun start(service: BaseService.Interface, stat: File, configFile: File, extraFlag: String? = null) {
trafficMonitor = TrafficMonitor(stat)
this.configFile = configFile
val config = profile.toJson()
configFile.writeText(config.toString())
val cmd = service.buildAdditionalArguments(arrayListOf(
File((service as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
"-b", DataStore.listenAddress,
"-l", DataStore.portProxy.toString(),
"-t", "600",
"-S", stat.absolutePath,
"-c", configFile.absolutePath))
if (extraFlag != null) cmd.add(extraFlag)
cmd += "--acl"
cmd += getFile().absolutePath
// for UDP profile, it's only going to operate in UDP relay mode-only so this flag has no effect
cmd += "-D"
if (DataStore.tcpFastOpen) cmd += "--fast-open"
service.data.processes!!.start(cmd)
}
fun shutdown(scope: CoroutineScope) {
trafficMonitor?.apply {
thread.shutdown(scope)
persistStats(profile.id) // Make sure update total traffic when stopping the runner
}
trafficMonitor = null
configFile?.delete() // remove old config possibly in device storage
configFile = null
}
}

View file

@ -1,123 +0,0 @@
/*******************************************************************************
* *
* 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 com.github.shadowsocks.bg
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
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 com.github.shadowsocks.Core
import org.amnezia.vpn.R
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.utils.Action
/**
* User can customize visibility of notification since Android 8.
* The default visibility:
*
* Android 8.x: always visible due to system limitations
* VPN: always invisible because of VPN notification/icon
* Other: always visible
*
* See also: https://github.com/aosp-mirror/platform_frameworks_base/commit/070d142993403cc2c42eca808ff3fafcee220ac4
*/
class ServiceNotification(private val service: BaseService.Interface, profileName: String, channel: String, visible: Boolean = false)
: BroadcastReceiver() {
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
builder.apply {
setContentText((service as Context).getString(R.string.traffic,
service.getString(R.string.speed, Formatter.formatFileSize(service, stats.txRate)),
service.getString(R.string.speed, Formatter.formatFileSize(service, stats.rxRate))))
setSubText(service.getString(R.string.traffic,
Formatter.formatFileSize(service, stats.txTotal),
Formatter.formatFileSize(service, stats.rxTotal)))
}
show()
}
override fun trafficPersisted(profileId: Long) {}
}
}
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)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setPriority(if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN)
init {
service as Context
val closeAction = NotificationCompat.Action.Builder(
R.drawable.ic_navigation_close,
service.getString(R.string.stop),
PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0)).apply {
setShowsUserInterface(false)
}.build()
if (Build.VERSION.SDK_INT < 24) builder.addAction(closeAction) else builder.addInvisibleAction(closeAction)
updateCallback(service.getSystemService<PowerManager>()?.isInteractive != false)
service.registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
})
show()
}
override fun onReceive(context: Context, intent: Intent) {
if (service.data.state == BaseService.State.Connected) updateCallback(intent.action == Intent.ACTION_SCREEN_ON)
}
private fun updateCallback(screenOn: Boolean) {
if (screenOn) {
service.data.binder.registerCallback(callback)
service.data.binder.startListeningForBandwidth(callback, 1000)
callbackRegistered = true
} else if (callbackRegistered) { // unregister callback to save battery
service.data.binder.unregisterCallback(callback)
callbackRegistered = false
}
}
private fun show() = (service as Service).startForeground(1, builder.build())
fun destroy() {
(service as Service).unregisterReceiver(this)
updateCallback(false)
service.stopForeground(true)
}
}

View file

@ -1,266 +0,0 @@
/*******************************************************************************
* *
* 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 com.github.shadowsocks.database
import android.net.Uri
import android.os.Parcelable
import android.util.Base64
import android.util.Log
import android.util.LongSparseArray
import androidx.core.net.toUri
import androidx.room.*
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.parsePort
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import kotlinx.android.parcel.Parcelize
import org.json.JSONObject
import java.io.Serializable
import java.net.URI
import java.net.URISyntaxException
import java.util.*
@Entity
@Parcelize
data class Profile(
// XinLake. route mode is bypass-lan
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
var name: String? = "",
var host: String = "0.0.0.0",
var remotePort: Int = 0,
var password: String = "0000",
var method: String = "aes-256-cfb",
var remoteDns: String = "8.8.8.8",
var udpdns: Boolean = false,
var ipv6: Boolean = false,
//@TargetApi(28)
var tx: Long = 0,
var rx: Long = 0,
var userOrder: Long = 0,
@Ignore // not persisted in db, only used by direct boot
var dirty: Boolean = false) : Parcelable, Serializable {
companion object {
private const val TAG = "ShadowParser"
private const val serialVersionUID = 1L
private val pattern =
"""(?i)ss://[-a-zA-Z0-9+&@#/%?=.~*'()|!:,;\[\]]*[-a-zA-Z0-9+&@#/%=.~*'()|\[\]]""".toRegex()
private val userInfoPattern = "^(.+?):(.*)$".toRegex()
private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex()
fun findAllUrls(data: CharSequence?, feature: Profile? = null) =
pattern.findAll(data ?: "").map {
val uri = it.value.toUri()
try {
if (uri.userInfo == null) {
val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING)))
if (match != null) {
val profile = Profile()
feature?.copyFeatureSettingsTo(profile)
profile.method = match.groupValues[1].toLowerCase(Locale.ENGLISH)
profile.password = match.groupValues[2]
profile.host = match.groupValues[3]
profile.remotePort = match.groupValues[4].toInt()
profile.name = uri.fragment
profile
} else {
Log.e(TAG, "Unrecognized URI: ${it.value}")
null
}
} else {
val match = userInfoPattern.matchEntire(String(Base64.decode(uri.userInfo,
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)))
if (match != null) {
val profile = Profile()
feature?.copyFeatureSettingsTo(profile)
profile.method = match.groupValues[1]
profile.password = match.groupValues[2]
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855
try {
val javaURI = URI(it.value)
profile.host = javaURI.host ?: ""
if (profile.host.firstOrNull() == '[' && profile.host.lastOrNull() == ']') {
profile.host = profile.host.substring(1, profile.host.length - 1)
}
profile.remotePort = javaURI.port
profile.name = uri.fragment ?: ""
profile
} catch (e: URISyntaxException) {
Log.e(TAG, "Invalid URI: ${it.value}")
null
}
} else {
Log.e(TAG, "Unknown user info: ${it.value}")
null
}
}
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Invalid base64 detected: ${it.value}")
null
}
}.filterNotNull()
private class JsonParser(private val feature: Profile? = null) : ArrayList<Profile>() {
private val JsonElement?.optString get() = (this as? JsonPrimitive)?.asString
private val JsonElement?.optBoolean
get() = // asBoolean attempts to cast everything to boolean
(this as? JsonPrimitive)?.run { if (isBoolean) asBoolean else null }
private val JsonElement?.optInt
get() = try {
(this as? JsonPrimitive)?.asInt
} catch (_: NumberFormatException) {
null
}
private fun tryParse(json: JsonObject, fallback: Boolean = false): Profile? {
val host = json["server"].optString
if (host.isNullOrEmpty()) return null
val remotePort = json["server_port"]?.optInt
if (remotePort == null || remotePort <= 0) return null
val password = json["password"].optString
if (password.isNullOrEmpty()) return null
val method = json["method"].optString
if (method.isNullOrEmpty()) return null
return Profile().also {
it.host = host
it.remotePort = remotePort
it.password = password
it.method = method
}.apply {
feature?.copyFeatureSettingsTo(this)
name = json["remarks"].optString
if (fallback) return@apply
remoteDns = json["remote_dns"].optString ?: remoteDns
ipv6 = json["ipv6"].optBoolean ?: ipv6
udpdns = json["udpdns"].optBoolean ?: udpdns
}
}
fun process(json: JsonElement?) {
when (json) {
is JsonObject -> {
val profile = tryParse(json)
if (profile != null) add(profile) else for ((_, value) in json.entrySet()) process(value)
}
is JsonArray -> json.asIterable().forEach(this::process)
// ignore other types
}
}
}
fun parseJson(json: JsonElement, feature: Profile? = null, create: (Profile) -> Unit) {
JsonParser(feature).run {
process(json)
for (profile in this) create(profile)
}
}
}
@androidx.room.Dao
interface Dao {
@Query("SELECT * FROM `Profile` WHERE `id` = :id")
operator fun get(id: Long): Profile?
@Query("SELECT * FROM `Profile` ORDER BY `userOrder`")
fun list(): List<Profile>
@Query("SELECT MAX(`userOrder`) + 1 FROM `Profile`")
fun nextOrder(): Long?
@Query("SELECT 1 FROM `Profile` LIMIT 1")
fun isNotEmpty(): Boolean
@Insert
fun create(value: Profile): Long
@Update
fun update(value: Profile): Int
@Query("DELETE FROM `Profile` WHERE `id` = :id")
fun delete(id: Long): Int
@Query("DELETE FROM `Profile`")
fun deleteAll(): Int
}
val formattedAddress get() = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort)
val formattedName get() = if (name.isNullOrEmpty()) formattedAddress else name!!
fun copyFeatureSettingsTo(profile: Profile) {
profile.ipv6 = ipv6
profile.udpdns = udpdns
}
fun toUri(): Uri {
val auth = Base64.encodeToString("$method:$password".toByteArray(),
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)
val wrappedHost = if (host.contains(':')) "[$host]" else host
val builder = Uri.Builder().scheme("ss").encodedAuthority("$auth@$wrappedHost:$remotePort")
if (!name.isNullOrEmpty()) builder.fragment(name)
return builder.build()
}
override fun toString() = toUri().toString()
fun toJson(profiles: LongSparseArray<Profile>? = null): JSONObject = JSONObject().apply {
put("server", host)
put("server_port", remotePort)
put("password", password)
put("method", method)
if (profiles == null) return@apply
put("remarks", name)
put("remote_dns", remoteDns)
put("ipv6", ipv6)
put("udpdns", udpdns)
}
fun serialize() {
DataStore.editingId = id
DataStore.privateStore.putString(Key.name, name)
DataStore.privateStore.putString(Key.host, host)
DataStore.privateStore.putString(Key.remotePort, remotePort.toString())
DataStore.privateStore.putString(Key.password, password)
DataStore.privateStore.putString(Key.remoteDns, remoteDns)
DataStore.privateStore.putString(Key.method, method)
DataStore.privateStore.putBoolean(Key.udpdns, udpdns)
DataStore.privateStore.putBoolean(Key.ipv6, ipv6)
DataStore.privateStore.remove(Key.dirty)
}
fun deserialize() {
check(id == 0L || DataStore.editingId == id)
DataStore.editingId = null
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
name = DataStore.privateStore.getString(Key.name) ?: ""
// It's safe to trim the hostname, as we expect no leading or trailing whitespaces here
host = (DataStore.privateStore.getString(Key.host) ?: "").trim()
remotePort = parsePort(DataStore.privateStore.getString(Key.remotePort), 8388, 1)
password = DataStore.privateStore.getString(Key.password) ?: ""
method = DataStore.privateStore.getString(Key.method) ?: ""
remoteDns = DataStore.privateStore.getString(Key.remoteDns) ?: ""
udpdns = DataStore.privateStore.getBoolean(Key.udpdns, false)
ipv6 = DataStore.privateStore.getBoolean(Key.ipv6, false)
}
}

View file

@ -39,7 +39,6 @@ class VPNService : android.net.VpnService() {
Log.e(tag, "Wireguard Version ${wgVersion()}")
mOpenVPNThreadv3 = OpenVPNThreadv3(this)
mAlreadyInitialised = true
}
override fun onUnbind(intent: Intent?): Boolean {
@ -82,10 +81,7 @@ 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"
)
Log.e(tag,"VPN service was triggered without defining a Server or having a tunnel")
return super.onStartCommand(intent, flags, startId)
}
this.mConfig = JSONObject(lastConfString)
@ -156,10 +152,13 @@ class VPNService : android.net.VpnService() {
}
Log.i(tag, "Permission okay")
mConfig = json!!
Log.i(tag, "Config: " + mConfig)
mProtocol = mConfig!!.getString("protocol")
Log.i(tag, "Protocol: " + mProtocol)
when (mProtocol) {
"openvpn" -> startOpenVpn()
"wireguard" -> startWireGuard()
"shadowsocks" -> startShadowsocks()
else -> {
Log.e(tag, "No protocol")
return 0
@ -365,6 +364,19 @@ class VPNService : android.net.VpnService() {
return mConfig!!
}
private fun startShadowsocks() {
Log.e(tag, "startShadowsocks method enters")
if(mConfig != null) {
try {
} catch(e: Exception) {
Log.e(tag, "Error in startShadowsocks: $e")
}
} else {
Log.e(tag, "Invalid config file!!")
}
}
private fun startOpenVpn() {
mOpenVPNThreadv3 = OpenVPNThreadv3(this)
Thread({

View file

@ -54,7 +54,7 @@ class VPNServiceBinder(service: VPNService) : Binder() {
val json = buffer?.let { String(it) }
val config = JSONObject(json)
Log.v(tag, "Stored new Tunnel config in Service")
Log.i(tag, "Config: $config")
if (!mService.checkPermissions()) {
mResumeConfig = config
// The Permission prompt was already

View file

@ -0,0 +1,22 @@
package org.amnezia.vpn.qt
import android.content.res.Configuration
import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.shadowsocks.core.VpnManager
import org.qtproject.qt5.android.bindings.QtActivity
import org.qtproject.qt5.android.bindings.QtApplication
import android.app.Application
class AmneziaApp: Application() {
override fun onCreate() {
super.onCreate()
Core.init(this, QtActivity::class)
VpnManager.getInstance().init(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
Core.updateNotificationChannels()
}
}

View file

@ -1,18 +1,24 @@
package org.amnezia.vpn.qt;
import android.app.Activity;
import android.os.Bundle;
import org.amnezia.vpn.BuildConfig;
import android.content.res.Configuration;
import androidx.annotation.NonNull;
import org.amnezia.vpn.shadowsocks.core.Core;
import org.amnezia.vpn.shadowsocks.core.VpnManager;
public class VPNApplication extends org.qtproject.qt5.android.bindings.QtApplication {
private static VPNApplication instance;
@Override
public void onCreate() {
super.onCreate();
VPNApplication.instance = this;
// Core.INSTANCE.init(this, VPNActivity.class);
// VpnManager.Companion.getInstance().init(this);
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Core.INSTANCE.updateNotificationChannels();
}
}

View file

@ -0,0 +1,51 @@
/*******************************************************************************
* *
* 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
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import org.amnezia.vpn.shadowsocks.core.Core.app
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
class BootReceiver : BroadcastReceiver() {
companion object {
private val componentName by lazy { ComponentName(app, org.amnezia.vpn.shadowsocks.core.BootReceiver::class.java) }
var enabled: Boolean
get() = app.packageManager.getComponentEnabledSetting(org.amnezia.vpn.shadowsocks.core.BootReceiver.Companion.componentName) ==
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
set(value) = app.packageManager.setComponentEnabledSetting(
org.amnezia.vpn.shadowsocks.core.BootReceiver.Companion.componentName,
if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
}
override fun onReceive(context: Context, intent: Intent) {
val locked = when (intent.action) {
Intent.ACTION_BOOT_COMPLETED -> false
Intent.ACTION_LOCKED_BOOT_COMPLETED -> true // constant will be folded so no need to do version checks
else -> return
}
if (DataStore.directBootAware == locked) org.amnezia.vpn.shadowsocks.core.Core.startService()
}
}

View file

@ -18,7 +18,7 @@
* *
*******************************************************************************/
package com.github.shadowsocks
package org.amnezia.vpn.shadowsocks.core
import android.app.Application
import android.app.NotificationChannel
@ -31,7 +31,6 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.os.Build
import android.os.UserManager
import androidx.annotation.RequiresApi
@ -39,17 +38,17 @@ import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.work.Configuration
import androidx.work.WorkManager
import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.*
import org.amnezia.vpn.R
import org.amnezia.vpn.shadowsocks.core.acl.Acl
import org.amnezia.vpn.shadowsocks.core.aidl.ShadowsocksConnection
import org.amnezia.vpn.shadowsocks.core.database.Profile
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
import org.amnezia.vpn.shadowsocks.core.net.TcpFastOpen
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
import org.amnezia.vpn.shadowsocks.core.utils.*
import kotlinx.coroutines.DEBUG_PROPERTY_NAME
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.amnezia.vpn.R
import java.io.File
import java.io.IOException
import kotlin.reflect.KClass
@ -59,16 +58,16 @@ object Core {
lateinit var app: Application
lateinit var configureIntent: (Context) -> PendingIntent
val connectivity by lazy { app.getSystemService<ConnectivityManager>()!! }
val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) }
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) }
val directBootSupported by lazy {
Build.VERSION.SDK_INT >= 24 && app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
Build.VERSION.SDK_INT >= 24 && app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus ==
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
}
val activeProfileIds
get() = ProfileManager.getProfile(DataStore.profileId).let {
if (it == null) emptyList() else listOfNotNull(it.id)
if (it == null) emptyList() else listOfNotNull(it.id, it.udpFallback)
}
val currentProfile: Pair<Profile, Profile?>?
get() {
@ -84,37 +83,34 @@ object Core {
}
fun init(app: Application, configureClass: KClass<out Any>) {
this.app = app
this.configureIntent = {
PendingIntent.getActivity(it,
0,
Intent(it,
configureClass.java).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
0)
Core.app = app
configureIntent = {
PendingIntent.getActivity(it, 0, Intent(it, configureClass.java)
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)
}
if (Build.VERSION.SDK_INT >= 24) { // migrate old files
deviceStorage.moveDatabaseFrom(app, Key.DB_PUBLIC)
val old = Acl.getFile(Acl.CUSTOM_RULES, app)
if (old.canRead()) {
Acl.getFile(Acl.CUSTOM_RULES).writeText(old.readText())
old.delete()
}
}
// overhead of debug mode is minimal: https://github.com/Kotlin/kotlinx.coroutines/blob/f528898/docs/debugging.md#debug-mode
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
// Fabric.with(deviceStorage, Crashlytics()) // multiple processes needs manual set-up
// FirebaseApp.initializeApp(deviceStorage)
WorkManager.initialize(deviceStorage, Configuration.Builder().apply {
setExecutor { GlobalScope.launch { it.run() } }
setTaskExecutor { GlobalScope.launch { it.run() } }
}.build())
WorkManager.initialize(deviceStorage, Configuration.Builder().build())
// handle data restored/crash
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware && app.getSystemService<UserManager>()?.isUserUnlocked == true) DirectBoot.flushTrafficStats()
if (Build.VERSION.SDK_INT >= 24 && DataStore.directBootAware &&
app.getSystemService<UserManager>()?.isUserUnlocked == true) DirectBoot.flushTrafficStats()
if (DataStore.tcpFastOpen && !TcpFastOpen.sendEnabled) TcpFastOpen.enableTimeout()
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) {
val assetManager = app.assets
try {
for (file in assetManager.list("acl")!!) assetManager.open("acl/$file").use { input ->
File(deviceStorage.noBackupFilesDir, file).outputStream()
.use { output -> input.copyTo(output) }
File(ContextCompat.getNoBackupFilesDir(deviceStorage), file).outputStream().use { output -> input.copyTo(output) }
}
} catch (e: IOException) {
printLog(e)
@ -127,11 +123,13 @@ object Core {
fun updateNotificationChannels() {
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
val nm = app.getSystemService<NotificationManager>()!!
nm.createNotificationChannels(listOf(NotificationChannel("service-vpn",
app.getText(R.string.service_vpn),
if (Build.VERSION.SDK_INT >= 28) NotificationManager.IMPORTANCE_MIN
else NotificationManager.IMPORTANCE_LOW) // #1355
))
nm.createNotificationChannels(listOf(
NotificationChannel("service-vpn", app.getText(R.string.service_vpn),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel("service-proxy", app.getText(R.string.service_proxy),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel("service-transproxy", app.getText(R.string.service_transproxy),
NotificationManager.IMPORTANCE_LOW)))
nm.deleteNotificationChannel("service-nat") // NAT mode is gone for good
}
}
@ -140,14 +138,11 @@ object Core {
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
fun startService() =
ContextCompat.startForegroundService(app, Intent(app, ShadowsocksConnection.serviceClass))
fun startService() = ContextCompat.startForegroundService(app, Intent(app, ShadowsocksConnection.serviceClass))
fun reloadService() = app.sendBroadcast(Intent(Action.RELOAD))
fun stopService() = app.sendBroadcast(Intent(Action.CLOSE))
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
object : BroadcastReceiver() {
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) = object : BroadcastReceiver() {
init {
app.registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)

View file

@ -0,0 +1,170 @@
package org.amnezia.vpn.shadowsocks.core
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.VpnService
import android.os.DeadObjectException
import android.os.Handler
import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksService
import org.amnezia.vpn.shadowsocks.core.aidl.ShadowsocksConnection
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
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
private var context: Context? = null
private val handler = Handler()
private val connection = ShadowsocksConnection(handler, true)
private var listener: OnStatusChangeListener? = null
private val callback: ShadowsocksConnection.Callback = object : ShadowsocksConnection.Callback {
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {
changeState(state)
}
override fun onServiceDisconnected() = changeState(BaseService.State.Idle)
override fun onServiceConnected(service: IShadowsocksService) {
changeState(try {
BaseService.State.values()[service.state]
} catch (_: DeadObjectException) {
BaseService.State.Idle
})
}
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
super.trafficUpdated(profileId, stats)
listener?.onTrafficUpdated(profileId, stats)
}
override fun onBinderDied() {
disconnect()
connect()
}
}
private fun connect() {
context?.let {
connection.connect(it, callback)
}
}
private fun disconnect() {
context?.let { connection.disconnect(it) }
}
companion object {
private const val REQUEST_CONNECT = 1
@SuppressLint("StaticFieldLeak")
private var instance: VpnManager? = null
fun getInstance(): VpnManager {
if (instance == null) {
instance = VpnManager()
}
return instance as VpnManager
}
}
fun init(context: Context){
this.context=context
connect()
}
/***
* 开启或者关闭 自动判断
*/
fun run(activity:Activity) {
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)
}
else -> Core.startService()
}
}
/***
* 设置状态监听
*/
fun setOnStatusChangeListener(listener: OnStatusChangeListener) {
this.listener = listener
}
/***
* application调用stop时调用
*/
fun onStop() {
connection.bandwidthTimeout = 0
}
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)
fun onTrafficUpdated(profileId: Long, stats: TrafficStats)
}
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");
var route = name
}
}

View file

@ -0,0 +1,75 @@
/*******************************************************************************
* *
* 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
import android.app.KeyguardManager
import android.content.BroadcastReceiver
import android.content.Intent
import android.content.IntentFilter
import android.net.VpnService
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import org.amnezia.vpn.R
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
import org.amnezia.vpn.shadowsocks.core.utils.Key
import org.amnezia.vpn.shadowsocks.core.utils.broadcastReceiver
class VpnRequestActivity : AppCompatActivity() {
companion object {
private const val TAG = "VpnRequestActivity"
private const val REQUEST_CONNECT = 1
}
private var receiver: BroadcastReceiver? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (DataStore.serviceMode != Key.modeVpn) {
finish()
return
}
if (getSystemService<KeyguardManager>()!!.isKeyguardLocked) {
receiver = broadcastReceiver { _, _ -> request() }
registerReceiver(receiver, IntentFilter(Intent.ACTION_USER_PRESENT))
} else request()
}
private fun request() {
val intent = VpnService.prepare(this)
if (intent == null) onActivityResult(REQUEST_CONNECT, RESULT_OK, null)
else startActivityForResult(intent, REQUEST_CONNECT)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == RESULT_OK) Core.startService() else {
Toast.makeText(this, R.string.vpn_permission_denied, Toast.LENGTH_LONG).show()
}
finish()
}
override fun onDestroy() {
super.onDestroy()
if (receiver != null) unregisterReceiver(receiver)
}
}

View file

@ -0,0 +1,200 @@
/*******************************************************************************
* *
* 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
import androidx.recyclerview.widget.SortedList
import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.shadowsocks.core.net.Subnet
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
import org.amnezia.vpn.shadowsocks.core.utils.asIterable
import java.io.File
import java.io.IOException
import java.io.Reader
import java.net.URL
import java.net.URLConnection
class Acl {
companion object {
const val TAG = "Acl"
const val ALL = "all"
const val BYPASS_LAN = "bypass-lan"
const val BYPASS_CHN = "bypass-china"
const val BYPASS_LAN_CHN = "bypass-lan-china"
const val GFWLIST = "gfwlist"
const val CHINALIST = "china-list"
const val CUSTOM_RULES = "custom-rules"
val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex()
fun getFile(id: String, context: Context = Core.deviceStorage) = File(context.noBackupFilesDir, "$id.acl")
var customRules: Acl
get() {
val acl = Acl()
val str = DataStore.publicStore.getString(CUSTOM_RULES)
if (str != null) acl.fromReader(str.reader(), true)
if (!acl.bypass) {
acl.bypass = true
acl.subnets.clear()
}
return acl
}
set(value) = DataStore.publicStore.putString(CUSTOM_RULES,
if ((!value.bypass || value.subnets.size() == 0) && value.bypassHostnames.size() == 0 &&
value.proxyHostnames.size() == 0 && value.urls.size() == 0) null else value.toString())
fun save(id: String, acl: Acl) = getFile(id).writeText(acl.toString())
}
private abstract class BaseSorter<T> : SortedList.Callback<T>() {
override fun onInserted(position: Int, count: Int) { }
override fun areContentsTheSame(oldItem: T?, newItem: T?): Boolean = oldItem == newItem
override fun onMoved(fromPosition: Int, toPosition: Int) { }
override fun onChanged(position: Int, count: Int) { }
override fun onRemoved(position: Int, count: Int) { }
override fun areItemsTheSame(item1: T?, item2: T?): Boolean = item1 == item2
override fun compare(o1: T?, o2: T?): Int =
if (o1 == null) if (o2 == null) 0 else 1 else if (o2 == null) -1 else compareNonNull(o1, o2)
abstract fun compareNonNull(o1: T, o2: T): Int
}
private open class DefaultSorter<T : Comparable<T>> : BaseSorter<T>() {
override fun compareNonNull(o1: T, o2: T): Int = o1.compareTo(o2)
}
private object StringSorter : DefaultSorter<String>()
private object SubnetSorter : DefaultSorter<Subnet>()
private object URLSorter : BaseSorter<URL>() {
private val ordering = compareBy<URL>({ it.host }, { it.port }, { it.file }, { it.protocol })
override fun compareNonNull(o1: URL, o2: URL): Int = ordering.compare(o1, o2)
}
val bypassHostnames = SortedList(String::class.java, StringSorter)
val proxyHostnames = SortedList(String::class.java, StringSorter)
val subnets = SortedList(Subnet::class.java, SubnetSorter)
val urls = SortedList(URL::class.java, URLSorter)
var bypass = false
fun fromAcl(other: Acl): Acl {
bypassHostnames.clear()
for (item in other.bypassHostnames.asIterable()) bypassHostnames.add(item)
proxyHostnames.clear()
for (item in other.proxyHostnames.asIterable()) proxyHostnames.add(item)
subnets.clear()
for (item in other.subnets.asIterable()) subnets.add(item)
urls.clear()
for (item in other.urls.asIterable()) urls.add(item)
bypass = other.bypass
return this
}
fun fromReader(reader: Reader, defaultBypass: Boolean = false): Acl {
bypassHostnames.clear()
proxyHostnames.clear()
subnets.clear()
urls.clear()
bypass = defaultBypass
val bypassSubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) }
val proxySubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) }
var hostnames: SortedList<String>? = if (defaultBypass) proxyHostnames else bypassHostnames
var subnets: SortedList<Subnet>? = if (defaultBypass) proxySubnets else bypassSubnets
reader.useLines {
for (line in it) {
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
val blocks = (line as java.lang.String).split("#", 2)
val url = networkAclParser.matchEntire(blocks.getOrElse(1) { "" })?.groupValues?.getOrNull(1)
if (url != null) urls.add(URL(url))
when (val input = blocks[0].trim()) {
"[outbound_block_list]" -> {
hostnames = null
subnets = null
}
"[black_list]", "[bypass_list]" -> {
hostnames = bypassHostnames
subnets = bypassSubnets
}
"[white_list]", "[proxy_list]" -> {
hostnames = proxyHostnames
subnets = proxySubnets
}
"[reject_all]", "[bypass_all]" -> bypass = true
"[accept_all]", "[proxy_all]" -> bypass = false
else -> if (subnets != null && input.isNotEmpty()) {
val subnet = Subnet.fromString(input)
if (subnet == null) hostnames!!.add(input) else subnets!!.add(subnet)
}
}
}
}
for (item in (if (bypass) proxySubnets else bypassSubnets).asIterable()) this.subnets.add(item)
return this
}
fun fromId(id: String): Acl = try {
fromReader(getFile(id).bufferedReader())
} catch (_: IOException) { this }
suspend fun flatten(depth: Int, connect: suspend (URL) -> URLConnection): Acl {
if (depth > 0) for (url in urls.asIterable()) {
val child = Acl()
try {
child.fromReader(connect(url).getInputStream().bufferedReader(), bypass).flatten(depth - 1, connect)
} catch (e: IOException) {
e.printStackTrace()
continue
}
if (bypass != child.bypass) {
child.subnets.clear() // subnets for the different mode are discarded
child.bypass = bypass
}
for (item in child.bypassHostnames.asIterable()) bypassHostnames.add(item)
for (item in child.proxyHostnames.asIterable()) proxyHostnames.add(item)
for (item in child.subnets.asIterable()) subnets.add(item)
}
urls.clear()
return this
}
override fun toString(): String {
val result = StringBuilder()
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
val bypassList = (if (bypass) {
bypassHostnames.asIterable().asSequence()
} else {
subnets.asIterable().asSequence().map(Subnet::toString) + bypassHostnames.asIterable().asSequence()
}).toList()
val proxyList = (if (bypass) {
subnets.asIterable().asSequence().map(Subnet::toString) + proxyHostnames.asIterable().asSequence()
} else {
proxyHostnames.asIterable().asSequence()
}).toList()
if (bypassList.isNotEmpty()) {
result.append("[bypass_list]\n")
result.append(bypassList.joinToString("\n"))
result.append('\n')
}
if (proxyList.isNotEmpty()) {
result.append("[proxy_list]\n")
result.append(proxyList.joinToString("\n"))
result.append('\n')
}
result.append(urls.asIterable().joinToString("") { "#IMPORT_URL <$it>\n" })
return result.toString()
}
}

View file

@ -0,0 +1,58 @@
/*******************************************************************************
* *
* 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
import androidx.work.*
import kotlinx.coroutines.Dispatchers
import java.io.IOException
import java.net.URL
import java.util.concurrent.TimeUnit
class AclSyncer(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
companion object {
private const val KEY_ROUTE = "route"
fun schedule(route: String) = WorkManager.getInstance().enqueueUniqueWork(route, ExistingWorkPolicy.REPLACE,
OneTimeWorkRequestBuilder<AclSyncer>().run {
setInputData(Data.Builder().putString(KEY_ROUTE, route).build())
setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build())
setInitialDelay(10, TimeUnit.SECONDS)
build()
})
}
override val coroutineContext get() = Dispatchers.IO
override suspend fun doWork(): Result = try {
val route = inputData.getString(KEY_ROUTE)!!
val acl = URL("https://shadowsocks.org/acl/android/v1/$route.acl").openStream().bufferedReader()
.use { it.readText() }
Acl.getFile(route).printWriter().use { it.write(acl) }
Result.success()
} catch (e: IOException) {
e.printStackTrace()
Result.retry()
}
}

View file

@ -18,44 +18,50 @@
* *
*******************************************************************************/
package com.github.shadowsocks.aidl
package org.amnezia.vpn.shadowsocks.core.aidl
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.DeadObjectException
import android.os.Handler
import android.os.IBinder
import android.os.RemoteException
import com.github.shadowsocks.LocalVpnService
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.utils.Action
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.preference.DataStore
import org.amnezia.vpn.shadowsocks.core.utils.Action
import org.amnezia.vpn.shadowsocks.core.utils.Key
/**
* This object should be compact as it will not get GC-ed.
*/
class ShadowsocksConnection(
private val handler: Handler = Handler(),
private var listenForDeath: Boolean = false
) : ServiceConnection,
IBinder.DeathRecipient {
class ShadowsocksConnection(private val handler: Handler = Handler(),
private var listenForDeath: Boolean = false) :
ServiceConnection, IBinder.DeathRecipient {
companion object {
val serviceClass = LocalVpnService::class.java
val serviceClass get() = when (DataStore.serviceMode) {
Key.modeProxy -> ProxyService::class
Key.modeVpn -> VpnService::class
Key.modeTransproxy -> TransproxyService::class
else -> throw UnknownError()
}.java
}
interface Callback {
fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)
fun trafficUpdated(profileId: Long, stats: TrafficStats) {}
fun trafficPersisted(profileId: Long) {}
fun trafficUpdated(profileId: Long, stats: TrafficStats) { }
fun trafficPersisted(profileId: Long) { }
fun onServiceConnected(service: IShadowsocksService)
/**
* Different from Android framework, this method will be called even when you call `detachService`.
*/
fun onServiceDisconnected() {}
fun onBinderDied() {}
fun onServiceDisconnected() { }
fun onBinderDied() { }
}
private var connectionActive = false
@ -64,16 +70,14 @@ class ShadowsocksConnection(
private val serviceCallback = object : IShadowsocksServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
val callback = callback ?: return
handler.post {
callback.stateChanged(BaseService.State.values()[state], profileName, msg)
handler.post { callback.stateChanged(BaseService.State.values()[state], profileName, msg) }
}
}
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
val callback = callback ?: return
handler.post { callback.trafficUpdated(profileId, stats) }
handler.post {
callback.trafficUpdated(profileId, stats)
}
}
override fun trafficPersisted(profileId: Long) {
val callback = callback ?: return
handler.post { callback.trafficPersisted(profileId) }
@ -83,30 +87,25 @@ class ShadowsocksConnection(
var bandwidthTimeout = 0L
set(value) {
try {
if (value > 0) service?.startListeningForBandwidth(serviceCallback, value)
else service?.stopListeningForBandwidth(serviceCallback)
} catch (_: RemoteException) {
}
val service = service
if (bandwidthTimeout != value && service != null)
if (value > 0) service.startListeningForBandwidth(serviceCallback, value) else try {
service.stopListeningForBandwidth(serviceCallback)
} catch (_: DeadObjectException) { }
field = value
}
var service: IShadowsocksService? = null
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
this.binder = binder
if (listenForDeath) binder.linkToDeath(this, 0)
val service = IShadowsocksService.Stub.asInterface(binder)!!
this.service = service
try {
if (listenForDeath) binder.linkToDeath(this, 0)
check(!callbackRegistered)
if (!callbackRegistered) try {
service.registerCallback(serviceCallback)
callbackRegistered = true
if (bandwidthTimeout > 0) service.startListeningForBandwidth(
serviceCallback,
bandwidthTimeout
)
} catch (_: RemoteException) {
}
if (bandwidthTimeout > 0) service.startListeningForBandwidth(serviceCallback, bandwidthTimeout)
} catch (_: RemoteException) { }
callback!!.onServiceConnected(service)
}
@ -119,7 +118,6 @@ class ShadowsocksConnection(
override fun binderDied() {
service = null
callbackRegistered = false
callback?.also { handler.post(it::onBinderDied) }
}
@ -127,8 +125,7 @@ class ShadowsocksConnection(
val service = service
if (service != null && callbackRegistered) try {
service.unregisterCallback(serviceCallback)
} catch (_: RemoteException) {
}
} catch (_: RemoteException) { }
callbackRegistered = false
}
@ -145,15 +142,11 @@ class ShadowsocksConnection(
unregisterCallback()
if (connectionActive) try {
context.unbindService(this)
} catch (_: IllegalArgumentException) {
} // ignore
} catch (_: IllegalArgumentException) { } // ignore
connectionActive = false
if (listenForDeath) binder?.unlinkToDeath(this, 0)
binder = null
try {
service?.stopListeningForBandwidth(serviceCallback)
} catch (_: RemoteException) {
}
service = null
callback = null
}

View file

@ -18,34 +18,31 @@
* *
*******************************************************************************/
package com.github.shadowsocks.aidl
package org.amnezia.vpn.shadowsocks.core.aidl
import android.os.Parcel
import android.os.Parcelable
data class TrafficStats(
// Bytes per second
var txRate: Long = 0L, var rxRate: Long = 0L,
var txRate: Long = 0L,
var rxRate: Long = 0L,
// Bytes for the current session
var txTotal: Long = 0L, var rxTotal: Long = 0L) : Parcelable {
operator fun plus(other: TrafficStats) = TrafficStats(txRate + other.txRate,
rxRate + other.rxRate,
txTotal + other.txTotal,
rxTotal + other.rxTotal)
constructor(parcel: Parcel) : this(parcel.readLong(),
parcel.readLong(),
parcel.readLong(),
parcel.readLong())
var txTotal: Long = 0L,
var rxTotal: Long = 0L
) : Parcelable {
operator fun plus(other: TrafficStats) = TrafficStats(
txRate + other.txRate, rxRate + other.rxRate,
txTotal + other.txTotal, rxTotal + other.rxTotal)
constructor(parcel: Parcel) : this(parcel.readLong(), parcel.readLong(), parcel.readLong(), parcel.readLong())
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeLong(txRate)
parcel.writeLong(rxRate)
parcel.writeLong(txTotal)
parcel.writeLong(rxTotal)
}
override fun describeContents() = 0
companion object CREATOR : Parcelable.Creator<TrafficStats> {

View file

@ -18,7 +18,7 @@
* *
*******************************************************************************/
package com.github.shadowsocks.bg
package org.amnezia.vpn.shadowsocks.core.bg
import android.app.Service
import android.content.Context
@ -26,16 +26,21 @@ import android.content.Intent
import android.content.IntentFilter
import android.os.*
import androidx.core.content.getSystemService
import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.net.HostsFile
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.*
import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.shadowsocks.core.Core.app
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.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.*
@ -50,26 +55,24 @@ object BaseService {
* Idle state is only used by UI and will never be returned by BaseService.
*/
Idle,
Connecting(true), Connected(true), Stopping, Stopped,
Connecting(true),
Connected(true),
Stopping,
Stopped,
}
const val CONFIG_FILE = "shadowsocks.conf"
const val CONFIG_FILE_UDP = "shadowsocks-udp.conf"
interface ExpectedException
class ExpectedExceptionWrapper(e: Exception) : Exception(e.localizedMessage, e),
ExpectedException
class Data internal constructor(private val service: Interface) {
var state = State.Stopped
var processes: GuardedProcessPool? = null
var proxy: ProxyInstance? = null
// no udpFallback. xinlake
var udpFallback: ProxyInstance? = null
var notification: ServiceNotification? = null
val closeReceiver = broadcastReceiver { _, intent ->
when (intent.action) {
Intent.ACTION_SHUTDOWN -> service.persistStats()
Action.RELOAD -> service.forceLoad()
else -> service.stopRunner()
}
@ -86,16 +89,15 @@ object BaseService {
}
}
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), CoroutineScope, AutoCloseable {
private val callbacks = object : RemoteCallbackList<IShadowsocksServiceCallback>() {
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), AutoCloseable {
val callbacks = object : RemoteCallbackList<IShadowsocksServiceCallback>() {
override fun onCallbackDied(callback: IShadowsocksServiceCallback?, cookie: Any?) {
super.onCallbackDied(callback, cookie)
stopListeningForBandwidth(callback ?: return)
}
}
private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier
override val coroutineContext = Dispatchers.Main.immediate + Job()
private var looper: Job? = null
private val handler = Handler()
override fun getState(): Int = (data?.state ?: State.Idle).ordinal
override fun getProfileName(): String = data?.proxy?.profile?.name ?: "Idle"
@ -105,27 +107,24 @@ object BaseService {
}
private fun broadcast(work: (IShadowsocksServiceCallback) -> Unit) {
val count = callbacks.beginBroadcast()
try {
repeat(count) {
repeat(callbacks.beginBroadcast()) {
try {
work(callbacks.getBroadcastItem(it))
} catch (_: RemoteException) {
} catch (_: DeadObjectException) {
} catch (e: Exception) {
printLog(e)
}
}
} finally {
callbacks.finishBroadcast()
}
}
private suspend fun loop() {
while (true) {
// delay(bandwidthListeners.values.min() ?: return)
delay(5000)
val proxies = listOfNotNull(data?.proxy)
val stats = proxies.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
private fun registerTimeout() {
handler.postDelayed(this::onTimeout, bandwidthListeners.values.min() ?: 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) }
if (stats.any { it.third } && data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {
@ -137,36 +136,38 @@ object BaseService {
}
}
}
}
registerTimeout()
}
override fun startListeningForBandwidth(cb: IShadowsocksServiceCallback, timeout: Long) {
launch {
if (bandwidthListeners.isEmpty() and (bandwidthListeners.put(cb.asBinder(), timeout) == null)) {
check(looper == null)
looper = launch { loop() }
}
if (data?.state != State.Connected) return@launch
val wasEmpty = bandwidthListeners.isEmpty()
if (bandwidthListeners.put(cb.asBinder(), timeout) == null) {
if (wasEmpty) registerTimeout()
if (data?.state != State.Connected) return
var sum = TrafficStats()
val data = data
val proxy = data?.proxy ?: return@launch
val proxy = data?.proxy ?: return
proxy.trafficMonitor?.out.also { 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(0, sum)
}
}
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
launch {
if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) {
looper!!.cancel()
looper = null
}
handler.removeCallbacksAndMessages(null)
}
}
@ -188,7 +189,7 @@ object BaseService {
override fun close() {
callbacks.kill()
cancel()
handler.removeCallbacksAndMessages(null)
data = null
}
}
@ -198,13 +199,13 @@ object BaseService {
val tag: String
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))
if (profile.host.isEmpty() || profile.password.isEmpty() || fallback != null && (fallback.host.isEmpty() || fallback.password.isEmpty())) {
if (profile.host.isEmpty() || profile.password.isEmpty() ||
fallback != null && (fallback.host.isEmpty() || fallback.password.isEmpty())) {
stopRunner(false, (this as Context).getString(R.string.proxy_empty))
return
}
@ -212,21 +213,25 @@ object BaseService {
when {
s == State.Stopped -> startRunner()
s.canStop -> stopRunner(true)
// else -> Crashlytics.log(Log.WARN, tag, "Illegal state when invoking use: $s")
else -> {}
}
}
fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
suspend fun startProcesses(hosts: HostsFile) {
val configRoot =
(if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
suspend fun startProcesses() {
val configRoot = (if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
?.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),
"-u")
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")
}
fun startRunner() {
@ -247,7 +252,6 @@ object BaseService {
// channge the state
data.changeState(State.Stopping)
GlobalScope.launch(Dispatchers.Main.immediate) {
// Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.connectingJob?.cancelAndJoin() // ensure stop connecting first
this@Interface as Service
// we use a coroutineScope here to allow clean-up in parallel
@ -263,11 +267,12 @@ object BaseService {
data.notification?.destroy()
data.notification = null
val ids = listOfNotNull(data.proxy).map {
val ids = listOfNotNull(data.proxy, data.udpFallback).map {
it.shutdown(this)
it.profile.id
}
data.proxy = null
data.udpFallback = null
data.binder.trafficPersisted(ids)
}
@ -275,17 +280,12 @@ 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 stopSelf()
}
}
fun persistStats() =
listOfNotNull(data.proxy).forEach { it.trafficMonitor?.persistStats(it.profile.id) }
suspend fun preInit() {}
suspend fun resolver(host: String) = DnsResolverCompat.resolveOnActiveNetwork(host)
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 {
@ -299,10 +299,11 @@ object BaseService {
stopRunner(false, getString(R.string.profile_empty))
return Service.START_NOT_STICKY
}
val (profile, _) = profilePair
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)
if (!data.closeReceiverRegistered) {
registerReceiver(data.closeReceiver, IntentFilter().apply {
@ -314,29 +315,35 @@ object BaseService {
}
data.notification = createNotification(profile.formattedName)
// Core.analytics.logEvent("start", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.changeState(State.Connecting)
data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
try {
Executable.killAll() // clean up old processes
preInit()
val hosts = HostsFile(DataStore.publicStore.getString(Key.hosts) ?: "")
proxy.init(this@Interface, hosts)
proxy.init(this@Interface)
data.udpFallback?.init(this@Interface)
data.processes = GuardedProcessPool {
printLog(it)
stopRunner(false, it.readableMessage)
}
startProcesses(hosts)
// proxy.scheduleUpdate() // XinLake. Bypass-LAN only
startProcesses()
proxy.scheduleUpdate()
data.udpFallback?.scheduleUpdate()
data.changeState(State.Connected)
} catch (_: CancellationException) {
// if the job was cancelled, it is canceller's responsibility to call stopRunner
} catch (_: UnknownHostException) {
stopRunner(false, getString(R.string.invalid_server))
} catch (exc: Throwable) {
if (exc is ExpectedException) exc.printStackTrace() else printLog(exc)
if (exc !is PluginManager.PluginNotFoundException &&
exc !is BindException &&
exc !is VpnService.NullConnectionException) {
printLog(exc)
}
stopRunner(false, "${getString(R.string.service_failed)}: ${exc.readableMessage}")
} finally {
data.connectingJob = null

View file

@ -18,25 +18,25 @@
* *
*******************************************************************************/
package com.github.shadowsocks.bg
package org.amnezia.vpn.shadowsocks.core.bg
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import android.text.TextUtils
import java.io.File
import java.io.IOException
object Executable {
// libredsocks.so is not required. xinlake
const val REDSOCKS = "libredsocks.so"
const val SS_LOCAL = "libss-local.so"
const val TUN2SOCKS = "libtun2socks.so"
private val EXECUTABLES = setOf(SS_LOCAL, TUN2SOCKS)
private val EXECUTABLES = setOf(SS_LOCAL, REDSOCKS, TUN2SOCKS)
fun killAll() {
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }
?: return) {
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
val exe = File(try {
File(process, "cmdline").inputStream().bufferedReader().readText()
} catch (_: IOException) {
@ -47,8 +47,6 @@ object Executable {
} catch (e: ErrnoException) {
if (e.errno != OsConstants.ESRCH) {
e.printStackTrace()
// Crashlytics.log(Log.WARN, "kill", "SIGKILL ${exe.absolutePath} (${process.name}) failed")
// Crashlytics.logException(e)
}
}
}

View file

@ -18,15 +18,17 @@
* *
*******************************************************************************/
package com.github.shadowsocks.bg
package org.amnezia.vpn.shadowsocks.core.bg
import android.os.Build
import android.os.SystemClock
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import android.util.Log
import androidx.annotation.MainThread
import com.github.shadowsocks.Core
import org.amnezia.vpn.shadowsocks.core.Core
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.io.File
@ -60,33 +62,22 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
val exitChannel = Channel<Int>()
try {
while (true) {
thread(name = "stderr-$cmdName") {
streamLogger(process.errorStream) {
// Crashlytics.log(Log.ERROR, cmdName, it)
}
}
thread(name = "stderr-$cmdName") { streamLogger(process.errorStream) { Log.e(cmdName, it) } }
thread(name = "stdout-$cmdName") {
streamLogger(process.inputStream) {
// Crashlytics.log(Log.VERBOSE, cmdName, it)
}
streamLogger(process.inputStream) { Log.i(cmdName, it) }
// this thread also acts as a daemon thread for waitFor
runBlocking { exitChannel.send(process.waitFor()) }
}
val startTime = SystemClock.elapsedRealtime()
val exitCode = exitChannel.receive()
running = false
when {
SystemClock.elapsedRealtime() - startTime < 1000 -> throw IOException("$cmdName exits too fast (exit code: $exitCode)")
// exitCode == 128 + OsConstants.SIGKILL -> Crashlytics.log(Log.WARN, TAG, "$cmdName was killed")
// else -> Crashlytics.logException(IOException("$cmdName unexpectedly exits with code $exitCode"))
if (SystemClock.elapsedRealtime() - startTime < 1000) {
throw IOException("$cmdName exits too fast (exit code: $exitCode)")
}
// Crashlytics.log(Log.DEBUG, TAG, "restart process: ${Commandline.toString(cmd)} (last exit code: $exitCode)")
start()
running = true
onRestartCallback?.invoke()
}
} catch (e: IOException) {
// Crashlytics.log(Log.WARN, TAG, "error occurred. stop guard: " + Commandline.toString(cmd))
GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
} finally {
if (running) withContext(NonCancellable) {
@ -114,7 +105,6 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
@MainThread
fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) {
// Crashlytics.log(Log.DEBUG, TAG, "start process: " + Commandline.toString(cmd))
Guard(cmd).apply {
start() // if start fails, IOException will be thrown directly
launch { looper(onRestartCallback) }

View file

@ -18,39 +18,47 @@
* *
*******************************************************************************/
package com.github.shadowsocks.bg
package org.amnezia.vpn.shadowsocks.core.bg
import com.github.shadowsocks.net.HostsFile
import com.github.shadowsocks.net.LocalDnsServer
import com.github.shadowsocks.net.Socks5Endpoint
import com.github.shadowsocks.preference.DataStore
import org.amnezia.vpn.shadowsocks.core.Core.app
import org.amnezia.vpn.shadowsocks.core.acl.Acl
import org.amnezia.vpn.shadowsocks.core.net.LocalDnsServer
import org.amnezia.vpn.shadowsocks.core.net.Socks5Endpoint
import org.amnezia.vpn.shadowsocks.core.net.Subnet
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
import kotlinx.coroutines.CoroutineScope
import java.net.InetSocketAddress
import java.net.URI
import java.net.URISyntaxException
import java.util.*
import org.amnezia.vpn.R
object LocalDnsService {
private val googleApisTester =
"(^|\\.)googleapis(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}\$".toRegex()
private val chinaIpList by lazy {
app.resources.openRawResource(R.raw.china_ip_list).bufferedReader()
.lineSequence().map(Subnet.Companion::fromString).filterNotNull().toList()
}
private val servers = WeakHashMap<Interface, LocalDnsServer>()
interface Interface : BaseService.Interface {
override suspend fun startProcesses(hosts: HostsFile) {
super.startProcesses(hosts)
override suspend fun startProcesses() {
super.startProcesses()
val profile = data.proxy!!.profile
val dns = try {
URI("dns://${profile.remoteDns}")
} catch (e: URISyntaxException) {
throw BaseService.ExpectedExceptionWrapper(e)
}
val dns = URI("dns://${profile.remoteDns}")
LocalDnsServer(this::resolver,
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
DataStore.proxyAddress,
hosts).apply {
DataStore.proxyAddress).apply {
tcp = !profile.udpdns
forwardOnly = true
when (profile.route) {
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> {
remoteDomainMatcher = googleApisTester
localIpMatcher = chinaIpList
}
Acl.CHINALIST -> { }
else -> forwardOnly = true
}
}.also { servers[this] = it }.start(InetSocketAddress(DataStore.listenAddress, DataStore.portLocalDns))
}

View file

@ -0,0 +1,129 @@
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 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.content.Context
import org.amnezia.vpn.shadowsocks.core.acl.Acl
import org.amnezia.vpn.shadowsocks.core.acl.AclSyncer
import org.amnezia.vpn.shadowsocks.core.database.Profile
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
import org.amnezia.vpn.shadowsocks.core.plugin.PluginConfiguration
import org.amnezia.vpn.shadowsocks.core.plugin.PluginManager
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
import org.amnezia.vpn.shadowsocks.core.utils.parseNumericAddress
import kotlinx.coroutines.*
import java.io.File
import java.io.IOException
import java.net.UnknownHostException
/**
* This class sets up environment for ss-local.
*/
class ProxyInstance(val profile: Profile, private val route: String = profile.route) {
private var configFile: File? = null
var trafficMonitor: TrafficMonitor? = null
private val plugin = PluginConfiguration(profile.plugin ?: "").selectedOptions
val pluginPath by lazy { PluginManager.init(plugin) }
suspend fun init(service: BaseService.Interface) {
if (route == Acl.CUSTOM_RULES) withContext(Dispatchers.IO) {
Acl.save(Acl.CUSTOM_RULES, Acl.customRules.flatten(10, service::openConnection))
}
// it's hard to resolve DNS on a specific interface so we'll do it here
if (profile.host.parseNumericAddress() == null) {
while (true) try {
val io = GlobalScope.async(Dispatchers.IO) { service.resolver(profile.host) }
profile.host = io.await().firstOrNull()?.hostAddress ?: throw UnknownHostException()
return
} catch (e: UnknownHostException) {
// retries are only needed on Chrome OS where arc0 is brought up/down during VPN changes
if (!DataStore.hasArc0) throw e
Thread.yield()
}
}
}
/**
* Sensitive shadowsocks configuration file requires extra protection. It may be stored in encrypted storage or
* device storage, depending on which is currently available.
*/
fun start(service: BaseService.Interface, stat: File, configFile: File, extraFlag: String? = null) {
trafficMonitor = TrafficMonitor(stat)
this.configFile = configFile
val config = profile.toJson()
if (pluginPath != null) config.put("plugin", pluginPath).put("plugin_opts", plugin.toString())
configFile.writeText(config.toString())
val cmd = service.buildAdditionalArguments(arrayListOf(
File((service as Context).applicationInfo.nativeLibraryDir, Executable.SS_LOCAL).absolutePath,
"-b", DataStore.listenAddress,
"-l", DataStore.portProxy.toString(),
"-t", "600",
"-S", stat.absolutePath,
"-c", configFile.absolutePath))
if (extraFlag != null) cmd.add(extraFlag)
if (route != Acl.ALL) {
cmd += "--acl"
cmd += Acl.getFile(route).absolutePath
}
// for UDP profile, it's only going to operate in UDP relay mode-only so this flag has no effect
if (profile.route == Acl.ALL || profile.route == Acl.BYPASS_LAN) cmd += "-D"
if (DataStore.tcpFastOpen) cmd += "--fast-open"
service.data.processes!!.start(cmd)
}
fun scheduleUpdate() {
if (route !in arrayOf(Acl.ALL, Acl.CUSTOM_RULES)) AclSyncer.schedule(route)
}
fun shutdown(scope: CoroutineScope) {
trafficMonitor?.apply {
thread.shutdown(scope)
// Make sure update total traffic when stopping the runner
try {
// profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
val profile = ProfileManager.getProfile(profile.id) ?: return
profile.tx += current.txTotal
profile.rx += current.rxTotal
ProfileManager.updateProfile(profile)
} catch (e: IOException) {
if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
val profile = DirectBoot.getDeviceProfile()!!.toList().filterNotNull().single { it.id == profile.id }
profile.tx += current.txTotal
profile.rx += current.rxTotal
profile.dirty = true
DirectBoot.update(profile)
DirectBoot.listenForUnlock()
}
}
trafficMonitor = null
configFile?.delete() // remove old config possibly in device storage
configFile = null
}
}

View file

@ -1,7 +1,7 @@
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* 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 *
@ -18,29 +18,25 @@
* *
*******************************************************************************/
package com.github.shadowsocks.preference
package org.amnezia.vpn.shadowsocks.core.bg
import android.graphics.Typeface
import android.text.InputFilter
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.preference.EditTextPreference
import android.app.Service
import android.content.Intent
object EditTextPreferenceModifiers {
object Monospace : EditTextPreference.OnBindEditTextListener {
override fun onBindEditText(editText: EditText) {
editText.typeface = Typeface.MONOSPACE
}
}
/**
* Shadowsocks service at its minimum.
*/
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)
object Port : EditTextPreference.OnBindEditTextListener {
private val portLengthFilter = arrayOf(InputFilter.LengthFilter(5))
override fun onBindEditText(editText: EditText) {
editText.inputType = EditorInfo.TYPE_CLASS_NUMBER
editText.filters = portLengthFilter
editText.setSingleLine()
editText.setSelection(editText.text.length)
}
override fun onBind(intent: Intent) = super.onBind(intent)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
super<BaseService.Interface>.onStartCommand(intent, flags, startId)
override fun onDestroy() {
super.onDestroy()
data.binder.close()
}
}

View file

@ -0,0 +1,135 @@
/*******************************************************************************
* *
* 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.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)
}
}

View file

@ -18,15 +18,12 @@
* *
*******************************************************************************/
package com.github.shadowsocks.bg
package org.amnezia.vpn.shadowsocks.core.bg
import android.net.LocalSocket
import android.os.SystemClock
import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.net.LocalSocketListener
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
import org.amnezia.vpn.shadowsocks.core.net.LocalSocketListener
import java.io.File
import java.io.IOException
import java.nio.ByteBuffer
@ -55,7 +52,6 @@ class TrafficMonitor(statFile: File) {
var out = TrafficStats()
private var timestampLast = 0L
private var dirty = false
private var persisted: TrafficStats? = null
fun requestUpdate(): Pair<TrafficStats, Boolean> {
val now = SystemClock.elapsedRealtime()
@ -83,26 +79,4 @@ class TrafficMonitor(statFile: File) {
}
return Pair(out, updated)
}
fun persistStats(id: Long) {
val current = current
check(persisted == null || persisted == current) { "Data loss occurred" }
persisted = current
try {
// profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
val profile = ProfileManager.getProfile(id) ?: return
profile.tx += current.txTotal
profile.rx += current.rxTotal
ProfileManager.updateProfile(profile)
} catch (e: IOException) {
if (!DataStore.directBootAware) throw e // we should only reach here because we're in direct boot
val profile = DirectBoot.getDeviceProfile()!!.toList().filterNotNull().single { it.id == id }
profile.tx += current.txTotal
profile.rx += current.rxTotal
profile.dirty = true
DirectBoot.update(profile)
DirectBoot.listenForUnlock()
}
}
}

View file

@ -0,0 +1,68 @@
/*******************************************************************************
* *
* 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.Service
import android.content.Intent
import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
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 onBind(intent: Intent) = super.onBind(intent)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
private fun startRedsocksDaemon() {
File(Core.deviceStorage.noBackupFilesDir, "redsocks.conf").writeText("""base {
log_debug = off;
log_info = off;
log = stderr;
daemon = off;
redirector = iptables;
}
redsocks {
local_ip = ${DataStore.listenAddress};
local_port = ${DataStore.portTransproxy};
ip = 127.0.0.1;
port = ${DataStore.portProxy};
type = socks5;
}
""")
data.processes!!.start(listOf(
File(applicationInfo.nativeLibraryDir, Executable.REDSOCKS).absolutePath, "-c", "redsocks.conf"))
}
override suspend fun startProcesses() {
startRedsocksDaemon()
super.startProcesses()
}
override fun onDestroy() {
super.onDestroy()
data.binder.close()
}
}

View file

@ -18,10 +18,11 @@
* *
*******************************************************************************/
package com.github.shadowsocks.bg
package org.amnezia.vpn.shadowsocks.core.bg
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import android.net.LocalSocket
import android.net.LocalSocketAddress
import android.net.Network
@ -29,14 +30,16 @@ import android.os.Build
import android.os.ParcelFileDescriptor
import android.system.ErrnoException
import android.system.Os
import com.github.shadowsocks.Core
import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.R
import com.github.shadowsocks.net.ConcurrentLocalSocketListener
import com.github.shadowsocks.net.DefaultNetworkListener
import com.github.shadowsocks.net.HostsFile
import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.printLog
import org.amnezia.vpn.shadowsocks.core.VpnRequestActivity
import org.amnezia.vpn.shadowsocks.core.acl.Acl
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
import org.amnezia.vpn.shadowsocks.core.utils.printLog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -45,7 +48,6 @@ import java.io.File
import java.io.FileDescriptor
import java.io.IOException
import java.net.URL
import java.util.*
import android.net.VpnService as BaseVpnService
@ -87,7 +89,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
}
}
inner class NullConnectionException : NullPointerException(), BaseService.ExpectedException {
inner class NullConnectionException : NullPointerException() {
override fun getLocalizedMessage() = getString(R.string.reboot_required)
}
@ -99,15 +101,16 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private var conn: ParcelFileDescriptor? = null
private var worker: ProtectWorker? = null
private var active = false
// metered = false. xinlake
private var metered = false
private var underlyingNetwork: Network? = null
set(value) {
field = value
if (active) setUnderlyingNetworks(underlyingNetworks)
if (active && Build.VERSION.SDK_INT >= 22) setUnderlyingNetworks(underlyingNetworks)
}
private val underlyingNetworks
get() = // clearing underlyingNetworks makes Android 9 consider the network to be metered
underlyingNetwork?.let { arrayOf(it) }
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) }
override fun onBind(intent: Intent) = when (intent.action) {
SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent)
@ -127,25 +130,22 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (DataStore.serviceMode == Key.modeVpn) {
if (prepare(this) != null) {
// startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
} else {
return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
} else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
}
stopRunner()
return Service.START_NOT_STICKY
}
override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
override suspend fun resolver(host: String) =
DnsResolverCompat.resolve(DefaultNetworkListener.get(), host)
override suspend fun resolver(host: String) = DefaultNetworkListener.get().getAllByName(host)
override suspend fun openConnection(url: URL) = DefaultNetworkListener.get().openConnection(url)
override suspend fun startProcesses(hosts: HostsFile) {
override suspend fun startProcesses() {
worker = ProtectWorker().apply { start() }
super.startProcesses(hosts)
super.startProcesses()
sendFd(startVpn())
}
@ -167,18 +167,39 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
builder.addRoute("::", 0)
}
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)
}
// XinLake. bypass lan
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)
active = true // possible race condition here?
if (Build.VERSION.SDK_INT >= 22) {
builder.setUnderlyingNetworks(underlyingNetworks)
}
}
metered = profile.metered
active = true // possible race condition here?
if (Build.VERSION.SDK_INT >= 22) builder.setUnderlyingNetworks(underlyingNetworks)
val conn = builder.establish() ?: throw NullConnectionException()
this.conn = conn
@ -199,6 +220,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
try {
sendFd(conn.fileDescriptor)
} catch (e: ErrnoException) {
e.printStackTrace()
stopRunner(false, e.message)
}
})

View file

@ -18,7 +18,7 @@
* *
*******************************************************************************/
package com.github.shadowsocks.database
package org.amnezia.vpn.shadowsocks.core.database
import androidx.room.*
import java.io.ByteArrayOutputStream
@ -62,8 +62,7 @@ class KeyValuePair() {
@Deprecated("Use long.", ReplaceWith("long"))
val int: Int?
get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null
val long: Long?
get() = when (valueType) {
val long: Long? get() = when (valueType) {
@Suppress("DEPRECATION")
TYPE_INT -> ByteBuffer.wrap(value).int.toLong()
TYPE_LONG -> ByteBuffer.wrap(value).long
@ -94,13 +93,11 @@ class KeyValuePair() {
this.value = ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte()).array()
return this
}
fun put(value: Float): KeyValuePair {
valueType = TYPE_FLOAT
this.value = ByteBuffer.allocate(4).putFloat(value).array()
return this
}
@Suppress("DEPRECATION")
@Deprecated("Use long.")
fun put(value: Int): KeyValuePair {
@ -108,26 +105,21 @@ class KeyValuePair() {
this.value = ByteBuffer.allocate(4).putInt(value).array()
return this
}
fun put(value: Long): KeyValuePair {
valueType = TYPE_LONG
this.value = ByteBuffer.allocate(8).putLong(value).array()
return this
}
fun put(value: String): KeyValuePair {
valueType = TYPE_STRING
this.value = value.toByteArray()
return this
}
fun put(value: Set<String>): KeyValuePair {
valueType = TYPE_STRING_SET
val stream = ByteArrayOutputStream()
val intBuffer = ByteBuffer.allocate(4)
for (v in value) {
intBuffer.rewind()
stream.write(intBuffer.putInt(v.length).array())
stream.write(ByteBuffer.allocate(4).putInt(v.length).array())
stream.write(v.toByteArray())
}
this.value = stream.toByteArray()

View file

@ -18,46 +18,52 @@
* *
*******************************************************************************/
package com.github.shadowsocks.database
package org.amnezia.vpn.shadowsocks.core.database
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.database.migration.RecreateSchemaMigration
import com.github.shadowsocks.utils.Key
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.amnezia.vpn.shadowsocks.core.Core.app
import org.amnezia.vpn.shadowsocks.core.database.migration.RecreateSchemaMigration
import org.amnezia.vpn.shadowsocks.core.utils.Key
@Database(entities = [Profile::class, KeyValuePair::class], version = 1000)
@Database(entities = [Profile::class, KeyValuePair::class], version = 29)
abstract class PrivateDatabase : RoomDatabase() {
companion object {
private val instance by lazy {
Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE).apply {
addMigrations(Migration1000)
allowMainThreadQueries()
enableMultiInstanceInvalidation()
fallbackToDestructiveMigration()
setQueryExecutor { GlobalScope.launch { it.run() } }
}.build()
Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE)
.addMigrations(
Migration26,
Migration27,
Migration28
)
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
}
val profileDao get() = instance.profileDao()
val kvPairDao get() = instance.keyValuePairDao()
}
abstract fun profileDao(): Profile.Dao
abstract fun keyValuePairDao(): KeyValuePair.Dao
object Migration1000 : RecreateSchemaMigration(999,
1000,
"Profile",
"(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL)",
"`id`, `name`, `host`, `remotePort`, `password`, `method`, `remoteDns`, `udpdns`, `ipv6`, `tx`, `rx`, `userOrder`") {
object Migration26 : RecreateSchemaMigration(25, 26, "Profile",
"(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `route` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `proxyApps` INTEGER NOT NULL, `bypass` INTEGER NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `individual` TEXT NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `plugin` TEXT)",
"`id`, `name`, `host`, `remotePort`, `password`, `method`, `route`, `remoteDns`, `proxyApps`, `bypass`, `udpdns`, `ipv6`, `individual`, `tx`, `rx`, `userOrder`, `plugin`") {
override fun migrate(database: SupportSQLiteDatabase) {
super.migrate(database)
PublicDatabase.Migration3.migrate(database)
}
}
object Migration27 : Migration(26, 27) {
override fun migrate(database: SupportSQLiteDatabase) =
database.execSQL("ALTER TABLE `Profile` ADD COLUMN `udpFallback` INTEGER")
}
object Migration28 : Migration(27, 28) {
override fun migrate(database: SupportSQLiteDatabase) =
database.execSQL("ALTER TABLE `Profile` ADD COLUMN `metered` INTEGER NOT NULL DEFAULT 0")
}
}

View file

@ -0,0 +1,330 @@
/*******************************************************************************
* *
* 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.database
import android.annotation.TargetApi
import android.net.Uri
import android.os.Parcelable
import android.util.Base64
import android.util.Log
import android.util.LongSparseArray
import androidx.core.net.toUri
import androidx.room.*
import org.amnezia.vpn.shadowsocks.core.plugin.PluginConfiguration
import org.amnezia.vpn.shadowsocks.plugin.PluginOptions
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
import org.amnezia.vpn.shadowsocks.core.utils.Key
import org.amnezia.vpn.shadowsocks.core.utils.asIterable
import org.amnezia.vpn.shadowsocks.core.utils.parsePort
import kotlinx.android.parcel.Parcelize
import org.json.JSONArray
import org.json.JSONObject
import org.json.JSONTokener
import java.io.Serializable
import java.net.URI
import java.net.URISyntaxException
import java.util.*
@Entity
@Parcelize
data class Profile(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
var name: String? = "",
var host: String = "155.94.174.51",
var remotePort: Int = 444,
var password: String = "789456123",
var method: String = "aes-256-cfb",
var route: String = "all",
var remoteDns: String = "dns.google",
var proxyApps: Boolean = false,
var bypass: Boolean = false,
var udpdns: Boolean = false,
var ipv6: Boolean = true,
@TargetApi(28)
var metered: Boolean = false,
var individual: String = "",
var tx: Long = 0,
var rx: Long = 0,
var userOrder: Long = 0,
var plugin: String? = null,
var udpFallback: Long? = null,
@Ignore // not persisted in db, only used by direct boot
var dirty: Boolean = false
) : Parcelable, Serializable {
companion object {
private const val TAG = "ShadowParser"
private const val serialVersionUID = 1L
private val pattern =
"""(?i)ss://[-a-zA-Z0-9+&@#/%?=.~*'()|!:,;\[\]]*[-a-zA-Z0-9+&@#/%=.~*'()|\[\]]""".toRegex()
private val userInfoPattern = "^(.+?):(.*)$".toRegex()
private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex()
fun findAllUrls(data: CharSequence?, feature: Profile? = null) = pattern.findAll(data ?: "").map {
val uri = it.value.toUri()
try {
if (uri.userInfo == null) {
val match = legacyPattern.matchEntire(String(Base64.decode(uri.host, Base64.NO_PADDING)))
if (match != null) {
val profile = Profile()
feature?.copyFeatureSettingsTo(profile)
profile.method = match.groupValues[1].toLowerCase()
profile.password = match.groupValues[2]
profile.host = match.groupValues[3]
profile.remotePort = match.groupValues[4].toInt()
profile.plugin = uri.getQueryParameter(Key.plugin)
profile.name = uri.fragment
profile
} else {
Log.e(TAG, "Unrecognized URI: ${it.value}")
null
}
} else {
val match = userInfoPattern.matchEntire(String(Base64.decode(uri.userInfo,
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)))
if (match != null) {
val profile = Profile()
feature?.copyFeatureSettingsTo(profile)
profile.method = match.groupValues[1]
profile.password = match.groupValues[2]
// bug in Android: https://code.google.com/p/android/issues/detail?id=192855
try {
val javaURI = URI(it.value)
profile.host = javaURI.host ?: ""
if (profile.host.firstOrNull() == '[' && profile.host.lastOrNull() == ']')
profile.host = profile.host.substring(1, profile.host.length - 1)
profile.remotePort = javaURI.port
profile.plugin = uri.getQueryParameter(Key.plugin)
profile.name = uri.fragment ?: ""
profile
} catch (e: URISyntaxException) {
Log.e(TAG, "Invalid URI: ${it.value}")
null
}
} else {
Log.e(TAG, "Unknown user info: ${it.value}")
null
}
}
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Invalid base64 detected: ${it.value}")
null
}
}.filterNotNull()
private class JsonParser(private val feature: Profile? = null) : ArrayList<Profile>() {
private val fallbackMap = mutableMapOf<Profile, Profile>()
private fun tryParse(json: JSONObject, fallback: Boolean = false): Profile? {
val host = json.optString("server")
if (host.isNullOrEmpty()) return null
val remotePort = json.optInt("server_port")
if (remotePort <= 0) return null
val password = json.optString("password")
if (password.isNullOrEmpty()) return null
val method = json.optString("method")
if (method.isNullOrEmpty()) return null
return Profile().also {
it.host = host
it.remotePort = remotePort
it.password = password
it.method = method
}.apply {
feature?.copyFeatureSettingsTo(this)
val id = json.optString("plugin")
if (!id.isNullOrEmpty()) {
plugin = PluginOptions(id, json.optString("plugin_opts")).toString(false)
}
name = json.optString("remarks")
route = json.optString("route", route)
if (fallback) return@apply
remoteDns = json.optString("remote_dns", remoteDns)
ipv6 = json.optBoolean("ipv6", ipv6)
metered = json.optBoolean("metered", metered)
json.optJSONObject("proxy_apps")?.also {
proxyApps = it.optBoolean("enabled", proxyApps)
bypass = it.optBoolean("bypass", bypass)
individual = it.optJSONArray("android_list")?.asIterable()?.joinToString("\n") ?: individual
}
udpdns = json.optBoolean("udpdns", udpdns)
json.optJSONObject("udp_fallback")?.let { tryParse(it, true) }?.also { fallbackMap[this] = it }
}
}
fun process(json: Any) {
when (json) {
is JSONObject -> {
val profile = tryParse(json)
if (profile != null) add(profile) else for (key in json.keys()) process(json.get(key))
}
is JSONArray -> json.asIterable().forEach(this::process)
// ignore other types
}
}
fun finalize(create: (Profile) -> Unit) {
val profiles = ProfileManager.getAllProfiles() ?: emptyList()
for ((profile, fallback) in fallbackMap) {
val match = profiles.firstOrNull {
fallback.host == it.host && fallback.remotePort == it.remotePort &&
fallback.password == it.password && fallback.method == it.method &&
it.plugin.isNullOrEmpty()
}
profile.udpFallback = if (match == null) {
create(fallback)
fallback.id
} else match.id
ProfileManager.updateProfile(profile)
}
}
}
fun parseJson(json: String, feature: Profile? = null, create: (Profile) -> Unit) = JsonParser(feature).run {
process(JSONTokener(json).nextValue())
for (profile in this) create(profile)
finalize(create)
}
}
@androidx.room.Dao
interface Dao {
@Query("SELECT * FROM `Profile` WHERE `id` = :id")
operator fun get(id: Long): Profile?
@Query("SELECT * FROM `Profile` ORDER BY `userOrder`")
fun list(): List<Profile>
@Query("SELECT MAX(`userOrder`) + 1 FROM `Profile`")
fun nextOrder(): Long?
@Query("SELECT 1 FROM `Profile` LIMIT 1")
fun isNotEmpty(): Boolean
@Insert
fun create(value: Profile): Long
@Update
fun update(value: Profile): Int
@Query("DELETE FROM `Profile` WHERE `id` = :id")
fun delete(id: Long): Int
@Query("DELETE FROM `Profile`")
fun deleteAll(): Int
}
val formattedAddress get() = (if (host.contains(":")) "[%s]:%d" else "%s:%d").format(host, remotePort)
val formattedName get() = if (name.isNullOrEmpty()) formattedAddress else name!!
fun copyFeatureSettingsTo(profile: Profile) {
profile.route = route
profile.ipv6 = ipv6
profile.metered = metered
profile.proxyApps = proxyApps
profile.bypass = bypass
profile.individual = individual
profile.udpdns = udpdns
}
fun toUri(): Uri {
val auth = Base64.encodeToString("$method:$password".toByteArray(),
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE)
val wrappedHost = if (host.contains(':')) "[$host]" else host
val builder = Uri.Builder()
.scheme("ss")
.encodedAuthority("$auth@$wrappedHost:$remotePort")
val configuration = PluginConfiguration(plugin ?: "")
if (configuration.selected.isNotEmpty())
builder.appendQueryParameter(Key.plugin, configuration.selectedOptions.toString(false))
if (!name.isNullOrEmpty()) builder.fragment(name)
return builder.build()
}
override fun toString() = toUri().toString()
fun toJson(profiles: LongSparseArray<Profile>? = null): JSONObject = JSONObject().apply {
put("server", host)
put("server_port", remotePort)
put("password", password)
put("method", method)
if (profiles == null) return@apply
PluginConfiguration(plugin ?: "").selectedOptions.also {
if (it.id.isNotEmpty()) {
put("plugin", it.id)
put("plugin_opts", it.toString())
}
}
put("remarks", name)
put("route", route)
put("remote_dns", remoteDns)
put("ipv6", ipv6)
put("metered", metered)
put("proxy_apps", JSONObject().apply {
put("enabled", proxyApps)
if (proxyApps) {
put("bypass", bypass)
// android_ prefix is used because package names are Android specific
put("android_list", JSONArray(individual.split("\n")))
}
})
put("udpdns", udpdns)
val fallback = profiles.get(udpFallback ?: return@apply)
if (fallback != null && fallback.plugin.isNullOrEmpty()) fallback.toJson().also { put("udp_fallback", it) }
}
fun serialize() {
DataStore.editingId = id
DataStore.privateStore.putString(Key.name, name)
DataStore.privateStore.putString(Key.host, host)
DataStore.privateStore.putString(Key.remotePort, remotePort.toString())
DataStore.privateStore.putString(Key.password, password)
DataStore.privateStore.putString(Key.route, route)
DataStore.privateStore.putString(Key.remoteDns, remoteDns)
DataStore.privateStore.putString(Key.method, method)
DataStore.proxyApps = proxyApps
DataStore.bypass = bypass
DataStore.privateStore.putBoolean(Key.udpdns, udpdns)
DataStore.privateStore.putBoolean(Key.ipv6, ipv6)
DataStore.privateStore.putBoolean(Key.metered, metered)
DataStore.individual = individual
DataStore.plugin = plugin ?: ""
DataStore.udpFallback = udpFallback
DataStore.privateStore.remove(Key.dirty)
}
fun deserialize() {
check(id == 0L || DataStore.editingId == id)
DataStore.editingId = null
// It's assumed that default values are never used, so 0/false/null is always used even if that isn't the case
name = DataStore.privateStore.getString(Key.name) ?: ""
host = DataStore.privateStore.getString(Key.host) ?: ""
remotePort = parsePort(DataStore.privateStore.getString(Key.remotePort), 8388, 1)
password = DataStore.privateStore.getString(Key.password) ?: ""
method = DataStore.privateStore.getString(Key.method) ?: ""
route = DataStore.privateStore.getString(Key.route) ?: ""
remoteDns = DataStore.privateStore.getString(Key.remoteDns) ?: ""
proxyApps = DataStore.proxyApps
bypass = DataStore.bypass
udpdns = DataStore.privateStore.getBoolean(Key.udpdns, false)
ipv6 = DataStore.privateStore.getBoolean(Key.ipv6, false)
metered = DataStore.privateStore.getBoolean(Key.metered, false)
individual = DataStore.individual
plugin = DataStore.plugin
udpFallback = DataStore.udpFallback
}
}

View file

@ -18,16 +18,14 @@
* *
*******************************************************************************/
package com.github.shadowsocks.database
package org.amnezia.vpn.shadowsocks.core.database
import android.database.sqlite.SQLiteCantOpenDatabaseException
import android.util.LongSparseArray
import com.github.shadowsocks.Core
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.forEachTry
import com.github.shadowsocks.utils.printLog
import com.google.gson.JsonStreamParser
import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
import org.amnezia.vpn.shadowsocks.core.utils.printLog
import org.json.JSONArray
import java.io.IOException
import java.io.InputStream
@ -43,7 +41,6 @@ object ProfileManager {
fun onRemove(profileId: Long)
fun onCleared()
}
var listener: Listener? = null
@Throws(SQLException::class)
@ -61,8 +58,9 @@ object ProfileManager {
profiles?.values?.singleOrNull { it.id == DataStore.profileId }
} else Core.currentProfile?.first
val lazyClear = lazy { clear() }
jsons.asIterable().forEachTry { json ->
Profile.parseJson(JsonStreamParser(json.bufferedReader()).asSequence().single(), feature) {
var result: Exception? = null
for (json in jsons) try {
Profile.parseJson(json.bufferedReader().readText(), feature) {
if (replace) {
lazyClear.value
// if two profiles has the same address, treat them as the same profile and copy stats over
@ -73,9 +71,11 @@ object ProfileManager {
}
createProfile(it)
}
} catch (e: Exception) {
if (result == null) result = e else result.addSuppressed(e)
}
if (result != null) throw result
}
fun serializeToJson(profiles: List<Profile>? = getAllProfiles()): JSONArray? {
if (profiles == null) return null
val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } }
@ -99,7 +99,7 @@ object ProfileManager {
}
@Throws(IOException::class)
fun expand(profile: Profile): Pair<Profile, Profile?> = Pair(profile, null)
fun expand(profile: Profile): Pair<Profile, Profile?> = Pair(profile, profile.udpFallback?.let { getProfile(it) })
@Throws(SQLException::class)
fun delProfile(id: Long) {

View file

@ -18,39 +18,33 @@
* *
*******************************************************************************/
package com.github.shadowsocks.database
package org.amnezia.vpn.shadowsocks.core.database
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.github.shadowsocks.Core
import com.github.shadowsocks.database.migration.RecreateSchemaMigration
import com.github.shadowsocks.utils.Key
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.shadowsocks.core.database.migration.RecreateSchemaMigration
import org.amnezia.vpn.shadowsocks.core.utils.Key
@Database(entities = [KeyValuePair::class], version = 3)
@Database(entities = [KeyValuePair::class], version = 4)
abstract class PublicDatabase : RoomDatabase() {
companion object {
private val instance by lazy {
Room.databaseBuilder(Core.deviceStorage, PublicDatabase::class.java, Key.DB_PUBLIC)
.apply {
addMigrations(Migration3)
allowMainThreadQueries()
enableMultiInstanceInvalidation()
fallbackToDestructiveMigration()
setQueryExecutor { GlobalScope.launch { it.run() } }
}.build()
.allowMainThreadQueries()
.addMigrations(
Migration3
)
.fallbackToDestructiveMigration()
.build()
}
val kvPairDao get() = instance.keyValuePairDao()
}
abstract fun keyValuePairDao(): KeyValuePair.Dao
internal object Migration3 : RecreateSchemaMigration(2,
3,
"KeyValuePair",
internal object Migration3 : RecreateSchemaMigration(2, 3, "KeyValuePair",
"(`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
"`key`, `valueType`, `value`")
}

View file

@ -18,14 +18,14 @@
* *
*******************************************************************************/
package com.github.shadowsocks.database.migration
package org.amnezia.vpn.shadowsocks.core.database.migration
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
open class RecreateSchemaMigration(oldVersion: Int, newVersion: Int, private val table: String,
private val schema: String, private val keys: String)
: Migration(oldVersion, newVersion) {
private val schema: String, private val keys: String) :
Migration(oldVersion, newVersion) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE `tmp` $schema")
database.execSQL("INSERT INTO `tmp` ($keys) SELECT $keys FROM `$table`")

View file

@ -18,10 +18,10 @@
* *
*******************************************************************************/
package com.github.shadowsocks.net
package org.amnezia.vpn.shadowsocks.core.net
import android.os.Build
import com.github.shadowsocks.utils.printLog
import org.amnezia.vpn.shadowsocks.core.utils.printLog
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.sendBlocking
@ -86,13 +86,11 @@ class ChannelMonitor : Thread("ChannelMonitor") {
return registration.result.await()
}
suspend fun wait(channel: SelectableChannel, ops: Int) =
CompletableDeferred<SelectionKey>().run {
suspend fun wait(channel: SelectableChannel, ops: Int) = CompletableDeferred<SelectionKey>().run {
register(channel, ops) {
if (it.isValid) try {
it.interestOps(0) // stop listening
} catch (_: CancelledKeyException) {
}
} catch (_: CancelledKeyException) { }
complete(it)
}
await()

View file

@ -18,17 +18,16 @@
* *
*******************************************************************************/
package com.github.shadowsocks.net
package org.amnezia.vpn.shadowsocks.core.net
import android.net.LocalSocket
import com.github.shadowsocks.utils.printLog
import org.amnezia.vpn.shadowsocks.core.utils.printLog
import kotlinx.coroutines.*
import java.io.File
abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) :
LocalSocketListener(name, socketFile), CoroutineScope {
override val coroutineContext =
Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : LocalSocketListener(name, socketFile),
CoroutineScope {
override val coroutineContext = Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
override fun accept(socket: LocalSocket) {
launch { super.accept(socket) }

View file

@ -18,7 +18,7 @@
* *
*******************************************************************************/
package com.github.shadowsocks.net
package org.amnezia.vpn.shadowsocks.core.net
import android.annotation.TargetApi
import android.net.ConnectivityManager
@ -26,12 +26,11 @@ import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import com.github.shadowsocks.Core
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import androidx.core.content.getSystemService
import org.amnezia.vpn.shadowsocks.core.Core.app
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.runBlocking
import java.net.UnknownHostException
object DefaultNetworkListener {
@ -40,14 +39,13 @@ object DefaultNetworkListener {
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()
}
@ObsoleteCoroutinesApi
private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) {
val listeners = mutableMapOf<Any, (Network?) -> Unit>()
var network: Network? = null
@ -74,9 +72,7 @@ object DefaultNetworkListener {
pendingRequests.clear()
listeners.values.forEach { it(network) }
}
is NetworkMessage.Update -> if (network == message.network) 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) }
@ -84,39 +80,55 @@ object DefaultNetworkListener {
}
}
suspend fun start(key: Any, listener: (Network?) -> Unit) =
networkActor.send(NetworkMessage.Start(key, listener))
suspend fun start(key: Any, listener: (Network?) -> Unit) = networkActor.send(DefaultNetworkListener.NetworkMessage.Start(key, listener))
suspend fun get() = if (fallback) @TargetApi(23) {
Core.connectivity.activeNetwork
?: throw UnknownHostException() // failed to listen, return current if available
} else NetworkMessage.Get().run {
connectivity.activeNetwork ?: throw UnknownHostException() // failed to listen, return current if available
} else DefaultNetworkListener.NetworkMessage.Get().run {
networkActor.send(this)
response.await()
}
suspend fun stop(key: Any) = networkActor.send(DefaultNetworkListener.NetworkMessage.Stop(key))
suspend fun stop(key: Any) = networkActor.send(NetworkMessage.Stop(key))
private object Callback: ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Put(network)) }
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
super.onCapabilitiesChanged(network, networkCapabilities)
// it's a good idea to refresh capabilities
runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Update(network)) }
}
override fun onLost(network: Network) {
super.onLost(network)
runBlocking {
networkActor.send(DefaultNetworkListener.NetworkMessage.Lost(network))
}
}
}
// 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 onAvailable(network: Network) = runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Put(network)) }
// override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
// // it's a good idea to refresh capabilities
// runBlocking { networkActor.send(NetworkMessage.Update(network)) }
// runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Update(network)) }
// }
//
// override fun onLost(network: Network) =
// runBlocking { networkActor.send(NetworkMessage.Lost(network)) }
// override fun onLost(network: Network) = runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Lost(network)) }
// }
private var fallback = false
private val connectivity = app.getSystemService<ConnectivityManager>()!!
private val request = NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
}.build()
/**
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
@ -128,18 +140,15 @@ object DefaultNetworkListener {
* Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
*/
private fun register() {
// if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
// Core.connectivity.registerDefaultNetworkCallback(Callback)
// } else try {
// fallback = false
// // we want REQUEST here instead of LISTEN
// Core.connectivity.requestNetwork(request, Callback)
// } catch (e: SecurityException) {
// // known bug: https://stackoverflow.com/a/33509180/2245107
// // if (Build.VERSION.SDK_INT != 23) Crashlytics.logException(e)
// fallback = true
// }
if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
connectivity.registerDefaultNetworkCallback(Callback)
} else try {
fallback = false
// we want REQUEST here instead of LISTEN
connectivity.requestNetwork(request, Callback)
} catch (e: SecurityException) {
fallback = true
}
private fun unregister() {}//= Core.connectivity.unregisterNetworkCallback(Callback)
}
private fun unregister() = connectivity.unregisterNetworkCallback(Callback)
}

View file

@ -18,21 +18,23 @@
* *
*******************************************************************************/
package com.github.shadowsocks.net
package org.amnezia.vpn.shadowsocks.core.net
import android.os.Build
import android.os.SystemClock
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.github.shadowsocks.Core.app
import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.shadowsocks.core.Core.app
import org.amnezia.vpn.shadowsocks.core.acl.Acl
import org.amnezia.vpn.R
import com.github.shadowsocks.utils.useCancellable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
import org.amnezia.vpn.shadowsocks.core.utils.Key
import org.amnezia.vpn.shadowsocks.core.utils.disconnectFromMain
import kotlinx.coroutines.*
import java.io.IOException
import java.net.HttpURLConnection
import java.net.Proxy
import java.net.URL
import java.net.URLConnection
@ -42,21 +44,17 @@ import java.net.URLConnection
class HttpsTest : ViewModel() {
sealed class Status {
protected abstract val status: CharSequence
open fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) =
setStatus(status)
open fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) = setStatus(status)
object Idle : Status() {
override val status get() = app.getText(R.string.vpn_connected)
}
object Testing : Status() {
override val status get() = app.getText(R.string.connection_test_testing)
}
class Success(private val elapsed: Long) : Status() {
override val status get() = app.getString(R.string.connection_test_available, elapsed)
}
sealed class Error : Status() {
override val status get() = app.getText(R.string.connection_test_fail)
protected abstract val error: String
@ -71,43 +69,48 @@ class HttpsTest : ViewModel() {
class UnexpectedResponseCode(private val code: Int) : Error() {
override val error get() = app.getString(R.string.connection_test_error_status_code, code)
}
class IOFailure(private val e: IOException) : Error() {
override val error get() = app.getString(R.string.connection_test_error, e.message)
}
}
}
private var running: Job? = null
private var running: Pair<HttpURLConnection, Job>? = null
val status = MutableLiveData<Status>().apply { value = Status.Idle }
fun testConnection() {
cancelTest()
status.value = Status.Testing
val url = URL("https", "www.google.com", "/generate_204")
val conn = (url.openConnection()) as HttpURLConnection
val url = URL("https", when ((Core.currentProfile ?: return).first.route) {
Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com"
}, "/generate_204")
val conn = (if (DataStore.serviceMode != Key.modeVpn) {
url.openConnection(Proxy(Proxy.Type.SOCKS, DataStore.proxyAddress))
} else url.openConnection()) as HttpURLConnection
conn.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false
conn.useCaches = false
running = GlobalScope.launch(Dispatchers.Main.immediate) {
status.value = conn.useCancellable {
running = conn to GlobalScope.launch(Dispatchers.Main.immediate) {
status.value = withContext(Dispatchers.IO) {
try {
val start = SystemClock.elapsedRealtime()
val code = responseCode
val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && responseLength == 0L) Status.Success(elapsed)
if (code == 204 || code == 200 && conn.responseLength == 0L) Status.Success(elapsed)
else Status.Error.UnexpectedResponseCode(code)
} catch (e: IOException) {
Status.Error.IOFailure(e)
} finally {
disconnect()
conn.disconnect()
}
}
}
}
private fun cancelTest() {
running?.cancel()
private fun cancelTest() = running?.let { (conn, job) ->
job.cancel() // ensure job is cancelled before interrupting
conn.disconnectFromMain()
running = null
}

View file

@ -18,10 +18,9 @@
* *
*******************************************************************************/
package com.github.shadowsocks.net
package org.amnezia.vpn.shadowsocks.core.net
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.utils.printLog
import org.amnezia.vpn.shadowsocks.core.utils.printLog
import kotlinx.coroutines.*
import org.xbill.DNS.*
import java.io.IOException
@ -30,6 +29,7 @@ import java.nio.ByteBuffer
import java.nio.channels.DatagramChannel
import java.nio.channels.SelectionKey
import java.nio.channels.SocketChannel
import org.amnezia.vpn.R
/**
* A simple DNS conditional forwarder.
@ -41,9 +41,7 @@ import java.nio.channels.SocketChannel
* https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04
*/
class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>,
private val remoteDns: Socks5Endpoint,
private val proxy: SocketAddress,
private val hosts: HostsFile) : CoroutineScope {
private val remoteDns: Socks5Endpoint, private val proxy: SocketAddress) : CoroutineScope {
/**
* Forward all requests to remote and ignore localResolver.
*/
@ -70,30 +68,14 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
if (request.header.getFlag(Flags.RD.toInt())) header.setFlag(Flags.RD.toInt())
request.question?.also { addRecord(it, Section.QUESTION) }
}
private fun cookDnsResponse(request: Message, results: Iterable<InetAddress>) =
ByteBuffer.wrap(prepareDnsResponse(request).apply {
header.setFlag(Flags.RA.toInt()) // recursion available
for (address in results) addRecord(when (address) {
is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address)
is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address)
else -> error("Unsupported address $address")
}, Section.ANSWER)
}.toWire())
}
private val monitor = ChannelMonitor()
override val coroutineContext =
SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
suspend fun start(listen: SocketAddress) = DatagramChannel.open().run {
configureBlocking(false)
try {
socket().bind(listen)
} catch (e: BindException) {
throw BaseService.ExpectedExceptionWrapper(e)
}
monitor.register(this, SelectionKey.OP_READ) { handlePacket(this) }
}
@ -111,27 +93,20 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
val request = try {
Message(packet)
} catch (e: IOException) { // we cannot parse the message, do not attempt to handle it at all
// Crashlytics.log(Log.WARN, TAG, e.message)
printLog(e)
return forward(packet)
}
return supervisorScope {
val remote = async { withTimeout(TIMEOUT) { forward(packet) } }
try {
if (request.header.opcode != Opcode.QUERY) return@supervisorScope remote.await()
if (forwardOnly || request.header.opcode != Opcode.QUERY) return@supervisorScope remote.await()
val question = request.question
if (question?.type != Type.A) return@supervisorScope remote.await()
val host = question.name.toString(true)
val hostsResults = hosts.resolve(host)
if (hostsResults.isNotEmpty()) {
remote.cancel()
return@supervisorScope cookDnsResponse(request, hostsResults)
}
if (forwardOnly) return@supervisorScope remote.await()
if (remoteDomainMatcher?.containsMatchIn(host) == true) return@supervisorScope remote.await()
val localResults = try {
withTimeout(TIMEOUT) { localResolver(host) }
withTimeout(TIMEOUT) { GlobalScope.async(Dispatchers.IO) { localResolver(host) }.await() }
} catch (_: TimeoutCancellationException) {
// Crashlytics.log(Log.WARN, TAG, "Local resolving timed out, falling back to remote resolving")
return@supervisorScope remote.await()
} catch (_: UnknownHostException) {
return@supervisorScope remote.await()
@ -139,15 +114,19 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
if (localResults.isEmpty()) return@supervisorScope remote.await()
if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) {
remote.cancel()
cookDnsResponse(request, localResults.asIterable())
ByteBuffer.wrap(prepareDnsResponse(request).apply {
header.setFlag(Flags.RA.toInt()) // recursion available
for (address in localResults) addRecord(when (address) {
is Inet4Address -> ARecord(question.name, DClass.IN, TTL, address)
is Inet6Address -> AAAARecord(question.name, DClass.IN, TTL, address)
else -> throw IllegalStateException("Unsupported address $address")
}, Section.ANSWER)
}.toWire())
} else remote.await()
} catch (e: Exception) {
remote.cancel()
when (e) {
// is TimeoutCancellationException -> Crashlytics.log(Log.WARN, TAG, "Remote resolving timed out")
is CancellationException -> {
} // ignore
// is IOException -> Crashlytics.log(Log.WARN, TAG, e.message)
is CancellationException -> { } // ignore
else -> printLog(e)
}
ByteBuffer.wrap(prepareDnsResponse(request).apply {
@ -157,6 +136,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
}
}
@ExperimentalUnsignedTypes
private suspend fun forward(packet: ByteBuffer): ByteBuffer {
packet.position(0) // the packet might have been parsed, reset to beginning
return if (tcp) SocketChannel.open().use { channel ->
@ -166,9 +146,7 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
while (!channel.finishConnect()) monitor.wait(channel, SelectionKey.OP_CONNECT)
while (channel.write(wrapped) >= 0 && wrapped.hasRemaining()) monitor.wait(channel, SelectionKey.OP_WRITE)
val result = remoteDns.tcpReceiveBuffer(UDP_PACKET_SIZE)
remoteDns.tcpUnwrap(result, channel::read) {
monitor.wait(channel, SelectionKey.OP_READ)
}
remoteDns.tcpUnwrap(result, channel::read) { monitor.wait(channel, SelectionKey.OP_READ) }
result
} else DatagramChannel.open().use { channel ->
channel.configureBlocking(false)

View file

@ -18,7 +18,7 @@
* *
*******************************************************************************/
package com.github.shadowsocks.net
package org.amnezia.vpn.shadowsocks.core.net
import android.net.LocalServerSocket
import android.net.LocalSocket
@ -26,7 +26,7 @@ import android.net.LocalSocketAddress
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import com.github.shadowsocks.utils.printLog
import org.amnezia.vpn.shadowsocks.core.utils.printLog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.sendBlocking
@ -48,7 +48,6 @@ abstract class LocalSocketListener(name: String, socketFile: File) : Thread(name
* Inherited class do not need to close input/output streams as they will be closed automatically.
*/
protected open fun accept(socket: LocalSocket) = socket.use { acceptInternal(socket) }
protected abstract fun acceptInternal(socket: LocalSocket)
final override fun run() {
localSocket.use {

View file

@ -18,9 +18,9 @@
* *
*******************************************************************************/
package com.github.shadowsocks.net
package org.amnezia.vpn.shadowsocks.core.net
import com.github.shadowsocks.utils.parseNumericAddress
import org.amnezia.vpn.shadowsocks.core.utils.parseNumericAddress
import net.sourceforge.jsocks.Socks4Message
import net.sourceforge.jsocks.Socks5Message
import java.io.EOFException
@ -32,13 +32,12 @@ import kotlin.math.max
class Socks5Endpoint(host: String, port: Int) {
private val dest = host.parseNumericAddress().let { numeric ->
val bytes = numeric?.address
?: host.toByteArray().apply { check(size < 256) { "Hostname too long" } }
val bytes = numeric?.address ?: host.toByteArray().apply { check(size < 256) { "Hostname too long" } }
val type = when (numeric) {
null -> Socks5Message.SOCKS_ATYP_DOMAINNAME
is Inet4Address -> Socks5Message.SOCKS_ATYP_IPV4
is Inet6Address -> Socks5Message.SOCKS_ATYP_IPV6
else -> error("Unsupported address type $numeric")
else -> throw IllegalStateException("Unsupported address type")
}
ByteBuffer.allocate(bytes.size + (if (numeric == null) 1 else 0) + 3).apply {
put(type.toByte())
@ -66,35 +65,34 @@ class Socks5Endpoint(host: String, port: Int) {
flip()
}
}
fun tcpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + 4 + size)
@ExperimentalUnsignedTypes
suspend fun tcpUnwrap(buffer: ByteBuffer, reader: (ByteBuffer) -> Int, wait: suspend () -> Unit) {
suspend fun readBytes(till: Int) {
if (buffer.position() >= till) return
while (reader(buffer) >= 0 && buffer.position() < till) wait()
if (buffer.position() < till) throw EOFException("${buffer.position()} < $till")
}
suspend fun read(index: Int): Byte {
readBytes(index + 1)
return buffer[index]
}
if (read(0) != Socks5Message.SOCKS_VERSION.toByte()) throw IOException("Unsupported SOCKS version ${buffer[0]}")
check(read(0) == Socks5Message.SOCKS_VERSION.toByte()) { "Unsupported SOCKS version" }
if (read(1) != 0.toByte()) throw IOException("Unsupported authentication ${buffer[1]}")
if (read(2) != Socks5Message.SOCKS_VERSION.toByte()) throw IOException("Unsupported SOCKS version ${buffer[2]}")
check(read(2) == Socks5Message.SOCKS_VERSION.toByte()) { "Unsupported SOCKS version" }
if (read(3) != 0.toByte()) throw IOException("SOCKS5 server returned error ${buffer[3]}")
val dataOffset = when (val type = read(5)) {
val dataOffset = when (read(5)) {
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + read(6)
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
else -> throw IOException("Unsupported address type $type")
else -> throw IllegalStateException("Unsupported address type ${buffer[5]}")
} + 8
readBytes(dataOffset + 2)
buffer.limit(buffer.position()) // store old position to update mark
buffer.position(dataOffset)
val dataLength = buffer.short.toUShort().toInt()
val end = buffer.position() + dataLength
if (end > buffer.capacity()) throw IOException("Buffer too small to contain the message: $dataLength > ${buffer.capacity() - buffer.position()}")
check(end <= buffer.capacity()) { "Buffer too small to contain the message" }
buffer.mark()
buffer.position(buffer.limit()) // restore old position
buffer.limit(end)
@ -102,13 +100,7 @@ class Socks5Endpoint(host: String, port: Int) {
buffer.reset()
}
private fun ByteBuffer.tryPosition(newPosition: Int) {
if (limit() < newPosition) throw EOFException("${limit()} < $newPosition")
position(newPosition)
}
fun udpWrap(packet: ByteBuffer) =
ByteBuffer.allocateDirect(3 + dest.size + packet.remaining()).apply {
fun udpWrap(packet: ByteBuffer) = ByteBuffer.allocateDirect(3 + dest.size + packet.remaining()).apply {
// header
putShort(0) // reserved
put(0) // fragment number
@ -117,15 +109,14 @@ class Socks5Endpoint(host: String, port: Int) {
put(packet)
flip()
}
fun udpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + size)
fun udpUnwrap(packet: ByteBuffer) {
packet.tryPosition(3)
packet.tryPosition(6 + when (val type = packet.get()) {
packet.position(3)
packet.position(6 + when (packet.get()) {
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + packet.get()
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
else -> throw IOException("Unsupported address type $type")
else -> throw IllegalStateException("Unsupported address type")
})
packet.mark()
}

View file

@ -18,9 +18,9 @@
* *
*******************************************************************************/
package com.github.shadowsocks.net
package org.amnezia.vpn.shadowsocks.core.net
import com.github.shadowsocks.utils.parseNumericAddress
import org.amnezia.vpn.shadowsocks.core.utils.parseNumericAddress
import java.net.InetAddress
import java.util.*
@ -42,7 +42,7 @@ class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet>
private val addressLength get() = address.address.size shl 3
init {
require(prefixSize in 0..addressLength) { "prefixSize $prefixSize not in 0..$addressLength" }
if (prefixSize < 0 || prefixSize > addressLength) throw IllegalArgumentException("prefixSize: $prefixSize")
}
fun matches(other: InetAddress): Boolean {
@ -80,6 +80,5 @@ class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet>
val that = other as? Subnet
return address == that?.address && prefixSize == that.prefixSize
}
override fun hashCode(): Int = Objects.hash(address, prefixSize)
}

View file

@ -18,9 +18,9 @@
* *
*******************************************************************************/
package com.github.shadowsocks.net
package org.amnezia.vpn.shadowsocks.core.net
import com.github.shadowsocks.utils.readableMessage
import org.amnezia.vpn.shadowsocks.core.utils.readableMessage
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
@ -34,8 +34,7 @@ object TcpFastOpen {
*/
val supported by lazy {
if (File(PATH).canRead()) return@lazy true
val match =
"""^(\d+)\.(\d+)\.(\d+)""".toRegex().find(System.getProperty("os.version") ?: "")
val match = """^(\d+)\.(\d+)\.(\d+)""".toRegex().find(System.getProperty("os.version") ?: "")
if (match == null) false else when (match.groupValues[1].toInt()) {
in Int.MIN_VALUE..2 -> false
3 -> when (match.groupValues[2].toInt()) {
@ -47,8 +46,7 @@ object TcpFastOpen {
}
}
val sendEnabled: Boolean
get() {
val sendEnabled: Boolean get() {
val file = File(PATH)
// File.readText doesn't work since this special file will return length 0
// on Android containers like Chrome OS, this file does not exist so we simply judge by the kernel version
@ -63,6 +61,5 @@ object TcpFastOpen {
e.readableMessage
}
}
fun enableTimeout() = runBlocking { withTimeoutOrNull(1000) { enable() } }
}

View file

@ -1,7 +1,7 @@
/*******************************************************************************
* *
* Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* 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 *
@ -18,23 +18,16 @@
* *
*******************************************************************************/
package com.github.shadowsocks.net
package org.amnezia.vpn.shadowsocks.core.plugin
import com.github.shadowsocks.utils.computeIfAbsentCompat
import com.github.shadowsocks.utils.parseNumericAddress
import java.net.InetAddress
class HostsFile(input: String = "") {
private val map = mutableMapOf<String, MutableSet<InetAddress>>()
import android.content.pm.ResolveInfo
import android.os.Bundle
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
init {
for (line in input.lineSequence()) {
val entries = line.substringBefore('#').splitToSequence(' ', '\t').filter { it.isNotEmpty() }
val address = entries.firstOrNull()?.parseNumericAddress() ?: continue
for (hostname in entries.drop(1)) map.computeIfAbsentCompat(hostname) { LinkedHashSet(1) }.add(address)
}
check(resolveInfo.providerInfo != null)
}
val configuredHostnames get() = map.size
fun resolve(hostname: String) = map[hostname]?.shuffled() ?: emptyList()
override val metaData: Bundle get() = resolveInfo.providerInfo.metaData
override val packageName: String get() = resolveInfo.providerInfo.packageName
}

View file

@ -0,0 +1,9 @@
package org.amnezia.vpn.shadowsocks.core.plugin
import org.amnezia.vpn.shadowsocks.core.Core.app
import org.amnezia.vpn.R
object NoPlugin : Plugin() {
override val id: String get() = ""
override val label: CharSequence get() = app.getText(R.string.plugin_disabled)
}

View file

@ -0,0 +1,32 @@
/*******************************************************************************
* *
* 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.plugin
import android.graphics.drawable.Drawable
abstract class Plugin {
abstract val id: String
abstract val label: CharSequence
open val icon: Drawable? get() = null
open val defaultConfig: String? get() = null
open val packageName: String get() = ""
open val trusted: Boolean get() = true
}

View file

@ -0,0 +1,61 @@
/*******************************************************************************
* *
* 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.plugin
import org.amnezia.vpn.shadowsocks.core.utils.Commandline
import org.amnezia.vpn.shadowsocks.plugin.PluginOptions
import java.util.*
class PluginConfiguration(val pluginsOptions: Map<String, PluginOptions>, val selected: String) {
private constructor(plugins: List<PluginOptions>) : this(
plugins.filter { it.id.isNotEmpty() }.associate { it.id to it },
if (plugins.isEmpty()) "" else plugins[0].id)
constructor(plugin: String) : this(plugin.split('\n').map { line ->
if (line.startsWith("kcptun ")) {
val opt = PluginOptions()
opt.id = "kcptun"
try {
val iterator = Commandline.translateCommandline(line).drop(1).iterator()
while (iterator.hasNext()) {
val option = iterator.next()
when {
option == "--nocomp" -> opt["nocomp"] = null
option.startsWith("--") -> opt[option.substring(2)] = iterator.next()
else -> throw IllegalArgumentException("Unknown kcptun parameter: $option")
}
}
} catch (exc: Exception) {
}
opt
} else PluginOptions(line)
})
fun getOptions(id: String): PluginOptions = if (id.isEmpty()) PluginOptions() else
pluginsOptions[id] ?: PluginOptions(id, PluginManager.fetchPlugins()[id]?.defaultConfig)
val selectedOptions: PluginOptions get() = getOptions(selected)
override fun toString(): String {
val result = LinkedList<PluginOptions>()
for ((id, opt) in pluginsOptions) if (id == this.selected) result.addFirst(opt) else result.addLast(opt)
if (!pluginsOptions.contains(selected)) result.addFirst(selectedOptions)
return result.joinToString("\n") { it.toString(false) }
}
}

View file

@ -0,0 +1,193 @@
/*******************************************************************************
* *
* 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.plugin
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ContentResolver
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.database.Cursor
import android.net.Uri
import android.system.Os
import android.util.Base64
import androidx.core.os.bundleOf
import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.shadowsocks.core.Core.app
import org.amnezia.vpn.R
import org.amnezia.vpn.shadowsocks.core.utils.printLog
import org.amnezia.vpn.shadowsocks.core.utils.signaturesCompat
import org.amnezia.vpn.shadowsocks.plugin.PluginContract
import org.amnezia.vpn.shadowsocks.plugin.PluginOptions
import java.io.File
import java.io.FileNotFoundException
object PluginManager {
class PluginNotFoundException(private val plugin: String) : FileNotFoundException(plugin) {
override fun getLocalizedMessage() = app.getString(R.string.plugin_unknown, plugin)
}
/**
* Trusted signatures by the app. Third-party fork should add their public key to their fork if the developer wishes
* to publish or has published plugins for this app. You can obtain your public key by executing:
*
* $ keytool -export -alias key-alias -keystore /path/to/keystore.jks -rfc
*
* If you don't plan to publish any plugin but is developing/has developed some, it's not necessary to add your
* public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
*/
val trustedSignatures by lazy {
Core.packageInfo.signaturesCompat.toSet() +
Signature(Base64.decode( // @Mygod
"""
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
|TjEOMAwGA1UECBMFTXlnb2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdv
|ZDEOMAwGA1UECxMFTXlnb2QxDjAMBgNVBAMTBU15Z29kMCAXDTE0MDUwMjA5MjQx
|OVoYDzMwMTMwOTAyMDkyNDE5WjBdMQswCQYDVQQGEwJDTjEOMAwGA1UECBMFTXln
|b2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdvZDEOMAwGA1UECxMFTXln
|b2QxDjAMBgNVBAMTBU15Z29kMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|AQEAjm5ikHoP3w6zavvZU5bRo6Birz41JL/nZidpdww21q/G9APA+IiJMUeeocy0
|L7/QY8MQZABVwNq79LXYWJBcmmFXM9xBPgDqQP4uh9JsvazCI9bvDiMn92mz9HiS
|Sg9V4KGg0AcY0r230KIFo7hz+2QBp1gwAAE97myBfA3pi3IzJM2kWsh4LWkKQMfL
|M6KDhpb4mdDQnHlgi4JWe3SYbLtpB6whnTqjHaOzvyiLspx1tmrb0KVxssry9KoX
|YQzl56scfE/QJX0jJ5qYmNAYRCb4PibMuNSGB2NObDabSOMAdT4JLueOcHZ/x9tw
|agGQ9UdymVZYzf8uqc+29ppKdQIDAQABoyEwHzAdBgNVHQ4EFgQUBK4uJ0cqmnho
|6I72VmOVQMvVCXowDQYJKoZIhvcNAQELBQADggEBABZQ3yNESQdgNJg+NRIcpF9l
|YSKZvrBZ51gyrC7/2ZKMpRIyXruUOIrjuTR5eaONs1E4HI/uA3xG1eeW2pjPxDnO
|zgM4t7EPH6QbzibihoHw1MAB/mzECzY8r11PBhDQlst0a2hp+zUNR8CLbpmPPqTY
|RSo6EooQ7+NBejOXysqIF1q0BJs8Y5s/CaTOmgbL7uPCkzArB6SS/hzXgDk5gw6v
|wkGeOtzcj1DlbUTvt1s5GlnwBTGUmkbLx+YUje+n+IBgMbohLUDYBtUHylRVgMsc
|1WS67kDqeJiiQZvrxvyW6CZZ/MIGI+uAkkj3DqJpaZirkwPgvpcOIrjZy0uFvQM=
""", Base64.DEFAULT)) +
Signature(Base64.decode( // @madeye
"""
|MIICQzCCAaygAwIBAgIETV9OhjANBgkqhkiG9w0BAQUFADBmMQswCQYDVQQGEwJjbjERMA8GA1UE
|CBMIU2hhbmdoYWkxDzANBgNVBAcTBlB1ZG9uZzEUMBIGA1UEChMLRnVkYW4gVW5pdi4xDDAKBgNV
|BAsTA1BQSTEPMA0GA1UEAxMGTWF4IEx2MB4XDTExMDIxOTA1MDA1NFoXDTM2MDIxMzA1MDA1NFow
|ZjELMAkGA1UEBhMCY24xETAPBgNVBAgTCFNoYW5naGFpMQ8wDQYDVQQHEwZQdWRvbmcxFDASBgNV
|BAoTC0Z1ZGFuIFVuaXYuMQwwCgYDVQQLEwNQUEkxDzANBgNVBAMTBk1heCBMdjCBnzANBgkqhkiG
|9w0BAQEFAAOBjQAwgYkCgYEAq6lA8LqdeEI+es9SDX85aIcx8LoL3cc//iRRi+2mFIWvzvZ+bLKr
|4Wd0rhu/iU7OeMm2GvySFyw/GdMh1bqh5nNPLiRxAlZxpaZxLOdRcxuvh5Nc5yzjM+QBv8ECmuvu
|AOvvT3UDmA0AMQjZqSCmxWIxc/cClZ/0DubreBo2st0CAwEAATANBgkqhkiG9w0BAQUFAAOBgQAQ
|Iqonxpwk2ay+Dm5RhFfZyG9SatM/JNFx2OdErU16WzuK1ItotXGVJaxCZv3u/tTwM5aaMACGED5n
|AvHaDGCWynY74oDAopM4liF/yLe1wmZDu6Zo/7fXrH+T03LBgj2fcIkUfN1AA4dvnBo8XWAm9VrI
|1iNuLIssdhDz3IL9Yg==
""", Base64.DEFAULT))
}
private var receiver: BroadcastReceiver? = null
private var cachedPlugins: Map<String, Plugin>? = null
fun fetchPlugins(): Map<String, Plugin> = synchronized(this) {
if (receiver == null) receiver = Core.listenForPackageChanges {
synchronized(this) {
receiver = null
cachedPlugins = null
}
}
if (cachedPlugins == null) {
val pm = app.packageManager
cachedPlugins = (pm.queryIntentContentProviders(Intent(PluginContract.ACTION_NATIVE_PLUGIN),
PackageManager.GET_META_DATA).map { NativePlugin(it) } + NoPlugin).associate { it.id to it }
}
cachedPlugins!!
}
private fun buildUri(id: String) = Uri.Builder()
.scheme(PluginContract.SCHEME)
.authority(PluginContract.AUTHORITY)
.path("/$id")
.build()
fun buildIntent(id: String, action: String): Intent = Intent(action, buildUri(id))
// the following parts are meant to be used by :bg
@Throws(Throwable::class)
fun init(options: PluginOptions): String? {
if (options.id.isEmpty()) return null
var throwable: Throwable? = null
try {
val path = initNative(options)
if (path != null) return path
} catch (t: Throwable) {
if (throwable == null) throwable = t else printLog(t)
}
// add other plugin types here
throw throwable ?: PluginNotFoundException(options.id)
}
private fun initNative(options: PluginOptions): String? {
val providers = app.packageManager.queryIntentContentProviders(
Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
if (providers.isEmpty()) return null
val uri = Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(providers.single().providerInfo.authority)
.build()
val cr = app.contentResolver
return try {
initNativeFast(cr, options, uri)
} catch (t: Throwable) {
printLog(t)
initNativeSlow(cr, options, uri)
}
}
private fun initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String {
val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null,
bundleOf(Pair(PluginContract.EXTRA_OPTIONS, options.id)))!!.getString(PluginContract.EXTRA_ENTRY)!!
check(File(result).canExecute())
return result
}
@SuppressLint("Recycle")
private fun initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String? {
var initialized = false
fun entryNotFound(): Nothing = throw IndexOutOfBoundsException("Plugin entry binary not found")
val pluginDir = File(Core.deviceStorage.noBackupFilesDir, "plugin")
(cr.query(uri, arrayOf(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null)
?: return null).use { cursor ->
if (!cursor.moveToFirst()) entryNotFound()
pluginDir.deleteRecursively()
if (!pluginDir.mkdirs()) throw FileNotFoundException("Unable to create plugin directory")
val pluginDirPath = pluginDir.absolutePath + '/'
do {
val path = cursor.getString(0)
val file = File(pluginDir, path)
check(file.absolutePath.startsWith(pluginDirPath))
cr.openInputStream(uri.buildUpon().path(path).build())!!.use { inStream ->
file.outputStream().use { outStream -> inStream.copyTo(outStream) }
}
Os.chmod(file.absolutePath, when (cursor.getType(1)) {
Cursor.FIELD_TYPE_INTEGER -> cursor.getInt(1)
Cursor.FIELD_TYPE_STRING -> cursor.getString(1).toInt(8)
else -> throw IllegalArgumentException("File mode should be of type int")
})
if (path == options.id) initialized = true
} while (cursor.moveToNext())
}
if (!initialized) entryNotFound()
return File(pluginDir, options.id).absolutePath
}
}

View file

@ -0,0 +1,42 @@
/*******************************************************************************
* *
* 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.plugin
import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
import android.os.Bundle
import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.shadowsocks.core.Core.app
import org.amnezia.vpn.shadowsocks.core.utils.signaturesCompat
import org.amnezia.vpn.shadowsocks.plugin.PluginContract
abstract class ResolvedPlugin(protected val resolveInfo: ResolveInfo) : Plugin() {
protected abstract val metaData: Bundle
override val id: String by lazy { metaData.getString(PluginContract.METADATA_KEY_ID)!! }
override val label: CharSequence by lazy { resolveInfo.loadLabel(app.packageManager) }
override val icon: Drawable by lazy { resolveInfo.loadIcon(app.packageManager) }
override val defaultConfig: String by lazy { metaData.getString(PluginContract.METADATA_KEY_DEFAULT_CONFIG)!! }
override val packageName: String get() = resolveInfo.resolvePackageName
override val trusted by lazy {
Core.getPackageInfo(packageName).signaturesCompat.any(PluginManager.trustedSignatures::contains)
}
}

View file

@ -18,18 +18,20 @@
* *
*******************************************************************************/
package com.github.shadowsocks.preference
package org.amnezia.vpn.shadowsocks.core.preference
import android.os.Binder
import androidx.preference.PreferenceDataStore
import com.github.shadowsocks.Core
import com.github.shadowsocks.database.PrivateDatabase
import com.github.shadowsocks.database.PublicDatabase
import com.github.shadowsocks.net.TcpFastOpen
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.parsePort
import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.shadowsocks.core.database.PrivateDatabase
import org.amnezia.vpn.shadowsocks.core.database.PublicDatabase
import org.amnezia.vpn.shadowsocks.core.net.TcpFastOpen
import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
import org.amnezia.vpn.shadowsocks.core.utils.Key
import org.amnezia.vpn.shadowsocks.core.utils.parsePort
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.net.SocketException
object DataStore : OnPreferenceDataStoreChangeListener {
val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
@ -40,7 +42,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
publicStore.registerChangeListener(this)
}
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
when (key) {
Key.id -> if (directBootAware) DirectBoot.update()
}
@ -48,7 +50,6 @@ object DataStore : OnPreferenceDataStoreChangeListener {
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
private fun getLocalPort(key: String, default: Int): Int {
val value = publicStore.getInt(key)
return if (value != null) {
@ -62,8 +63,29 @@ object DataStore : OnPreferenceDataStoreChangeListener {
set(value) = publicStore.putLong(Key.id, value)
val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true
val directBootAware: Boolean get() = Core.directBootSupported && canToggleLocked
val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && publicStore.getBoolean(Key.tfo, false)
val listenAddress get() = "127.0.0.1"
val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && publicStore.getBoolean(Key.tfo, true)
val serviceMode get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn
/**
* An alternative way to detect this interface could be checking MAC address = 00:ff:aa:00:00:55, but there is no
* reliable way of getting MAC address for now.
*/
val hasArc0 by lazy {
var retry = 0
while (retry < 5) {
try {
return@lazy NetworkInterface.getByName("arc0") != null
} catch (_: SocketException) { }
retry++
Thread.sleep(100L shl retry)
}
false
}
/**
* Binding bogus IP address 100.115.92.2 in Chrome OS directly does not seem to work reliably. It might be due to
* the IP may not be available when the device is not connected to any network.
*/
val listenAddress get() = if (publicStore.getBoolean(Key.shareOverLan, hasArc0)) "0.0.0.0" else "127.0.0.1"
var portProxy: Int
get() = getLocalPort(Key.portProxy, 1080)
set(value) = publicStore.putString(Key.portProxy, value.toString())
@ -71,6 +93,9 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var portLocalDns: Int
get() = getLocalPort(Key.portLocalDns, 5450)
set(value) = publicStore.putString(Key.portLocalDns, value.toString())
var portTransproxy: Int
get() = getLocalPort(Key.portTransproxy, 8200)
set(value) = publicStore.putString(Key.portTransproxy, value.toString())
/**
* Initialize settings that have complicated default values.
@ -79,11 +104,27 @@ object DataStore : OnPreferenceDataStoreChangeListener {
if (publicStore.getBoolean(Key.tfo) == null) publicStore.putBoolean(Key.tfo, tcpFastOpen)
if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy
if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
if (publicStore.getString(Key.portTransproxy) == null) portTransproxy = portTransproxy
}
var editingId: Long?
get() = privateStore.getLong(Key.id)
set(value) = privateStore.putLong(Key.id, value)
var proxyApps: Boolean
get() = privateStore.getBoolean(Key.proxyApps) ?: false
set(value) = privateStore.putBoolean(Key.proxyApps, value)
var bypass: Boolean
get() = privateStore.getBoolean(Key.bypass) ?: false
set(value) = privateStore.putBoolean(Key.bypass, value)
var individual: String
get() = privateStore.getString(Key.individual) ?: ""
set(value) = privateStore.putString(Key.individual, value)
var plugin: String
get() = privateStore.getString(Key.plugin) ?: ""
set(value) = privateStore.putString(Key.plugin, value)
var udpFallback: Long?
get() = privateStore.getLong(Key.udpFallback)
set(value) = privateStore.putLong(Key.udpFallback, value)
var dirty: Boolean
get() = privateStore.getBoolean(Key.dirty) ?: false
set(value) = privateStore.putBoolean(Key.dirty, value)

View file

@ -18,10 +18,10 @@
* *
*******************************************************************************/
package com.github.shadowsocks.preference
package org.amnezia.vpn.shadowsocks.core.preference
import androidx.preference.PreferenceDataStore
interface OnPreferenceDataStoreChangeListener {
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?)
}

View file

@ -18,15 +18,14 @@
* *
*******************************************************************************/
package com.github.shadowsocks.preference
package org.amnezia.vpn.shadowsocks.core.preference
import androidx.preference.PreferenceDataStore
import com.github.shadowsocks.database.KeyValuePair
import java.util.*
import org.amnezia.vpn.shadowsocks.core.database.KeyValuePair
import java.util.HashSet
@Suppress("MemberVisibilityCanBePrivate", "unused")
open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) :
PreferenceDataStore() {
open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) : PreferenceDataStore() {
fun getBoolean(key: String) = kvPairDao[key]?.boolean
fun getFloat(key: String) = kvPairDao[key]?.float
fun getInt(key: String) = kvPairDao[key]?.long?.toInt()
@ -39,46 +38,33 @@ open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) :
override fun getInt(key: String, defValue: Int) = getInt(key) ?: defValue
override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue
override fun getString(key: String, defValue: String?) = getString(key) ?: defValue
override fun getStringSet(key: String, defValue: MutableSet<String>?) =
getStringSet(key) ?: defValue
fun putBoolean(key: String, value: Boolean?) =
if (value == null) remove(key) else putBoolean(key, value)
fun putFloat(key: String, value: Float?) =
if (value == null) remove(key) else putFloat(key, value)
fun putInt(key: String, value: Int?) =
if (value == null) remove(key) else putLong(key, value.toLong())
override fun getStringSet(key: String, defValue: MutableSet<String>?) = getStringSet(key) ?: defValue
fun putBoolean(key: String, value: Boolean?) = if (value == null) remove(key) else putBoolean(key, value)
fun putFloat(key: String, value: Float?) = if (value == null) remove(key) else putFloat(key, value)
fun putInt(key: String, value: Int?) = if (value == null) remove(key) else putLong(key, value.toLong())
fun putLong(key: String, value: Long?) = if (value == null) remove(key) else putLong(key, value)
override fun putBoolean(key: String, value: Boolean) {
kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putFloat(key: String, value: Float) {
kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putInt(key: String, value: Int) {
kvPairDao.put(KeyValuePair(key).put(value.toLong()))
fireChangeListener(key)
}
override fun putLong(key: String, value: Long) {
kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putString(key: String, value: String?) = if (value == null) remove(key) else {
kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key)
}
override fun putStringSet(key: String, values: MutableSet<String>?) =
if (values == null) remove(key) else {
override fun putStringSet(key: String, values: MutableSet<String>?) = if (values == null) remove(key) else {
kvPairDao.put(KeyValuePair(key).put(values))
fireChangeListener(key)
}
@ -89,12 +75,7 @@ open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) :
}
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
private fun fireChangeListener(key: String) =
listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) =
listeners.add(listener)
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) =
listeners.remove(listener)
private fun fireChangeListener(key: String) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener)
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener)
}

View file

@ -18,10 +18,11 @@
* *
*******************************************************************************/
package com.github.shadowsocks.utils
package org.amnezia.vpn.shadowsocks.core.utils
import android.content.ClipData
import androidx.recyclerview.widget.SortedList
import org.json.JSONArray
private sealed class ArrayIterator<out T> : Iterator<T> {
abstract val size: Int
@ -35,12 +36,16 @@ private class ClipDataIterator(private val data: ClipData) : ArrayIterator<ClipD
override val size get() = data.itemCount
override fun get(index: Int) = data.getItemAt(index)
}
fun ClipData.asIterable() = Iterable { ClipDataIterator(this) }
private class JSONArrayIterator(private val arr: JSONArray) : ArrayIterator<Any>() {
override val size get() = arr.length()
override fun get(index: Int) = arr.get(index)
}
fun JSONArray.asIterable() = Iterable { JSONArrayIterator(this) }
private class SortedListIterator<out T>(private val list: SortedList<T>) : ArrayIterator<T>() {
override val size get() = list.size()
override fun get(index: Int) = list[index]
}
fun <T> SortedList<T>.asIterable() = Iterable { SortedListIterator(this) }

View file

@ -0,0 +1,173 @@
/*******************************************************************************
* *
* 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.utils
import java.util.*
/**
* Commandline objects help handling command lines specifying processes to
* execute.
*
* The class can be used to define a command line as nested elements or as a
* helper to define a command line by an application.
*
*
* `
* <someelement><br></br>
* &nbsp;&nbsp;<acommandline executable="/executable/to/run"><br></br>
* &nbsp;&nbsp;&nbsp;&nbsp;<argument value="argument 1" /><br></br>
* &nbsp;&nbsp;&nbsp;&nbsp;<argument line="argument_1 argument_2 argument_3" /><br></br>
* &nbsp;&nbsp;&nbsp;&nbsp;<argument value="argument 4" /><br></br>
* &nbsp;&nbsp;</acommandline><br></br>
* </someelement><br></br>
` *
*
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
*
* Adds support for escape character '\'.
*/
object Commandline {
/**
* Quote the parts of the given array in way that makes them
* usable as command line arguments.
* @param args the list of arguments to quote.
* @return empty string for null or no command, else every argument split
* by spaces and quoted by quoting rules.
*/
fun toString(args: Iterable<String>?): String {
// empty path return empty string
if (args == null) {
return ""
}
// path containing one or more elements
val result = StringBuilder()
for (arg in args) {
if (result.isNotEmpty()) result.append(' ')
(0 until arg.length)
.map { arg[it] }
.forEach {
when (it) {
' ', '\\', '"', '\'' -> {
result.append('\\') // intentionally no break
result.append(it)
}
else -> result.append(it)
}
}
}
return result.toString()
}
/**
* Quote the parts of the given array in way that makes them
* usable as command line arguments.
* @param args the list of arguments to quote.
* @return empty string for null or no command, else every argument split
* by spaces and quoted by quoting rules.
*/
fun toString(args: Array<String>) = toString(args.asIterable()) // thanks to Java, arrays aren't iterable
/**
* Crack a command line.
* @param toProcess the command line to process.
* @return the command line broken into strings.
* An empty or null toProcess parameter results in a zero sized array.
*/
fun translateCommandline(toProcess: String?): Array<String> {
if (toProcess == null || toProcess.isEmpty()) {
//no command? no string
return arrayOf()
}
// parse with a simple finite state machine
val normal = 0
val inQuote = 1
val inDoubleQuote = 2
var state = normal
val tok = StringTokenizer(toProcess, "\\\"\' ", true)
val result = ArrayList<String>()
val current = StringBuilder()
var lastTokenHasBeenQuoted = false
var lastTokenIsSlash = false
while (tok.hasMoreTokens()) {
val nextTok = tok.nextToken()
when (state) {
inQuote -> if ("\'" == nextTok) {
lastTokenHasBeenQuoted = true
state = normal
} else {
current.append(nextTok)
}
inDoubleQuote -> if ("\"" == nextTok) {
if (lastTokenIsSlash) {
current.append(nextTok)
lastTokenIsSlash = false
} else {
lastTokenHasBeenQuoted = true
state = normal
}
} else if ("\\" == nextTok) {
lastTokenIsSlash = if (lastTokenIsSlash) {
current.append(nextTok)
false
} else
true
} else {
if (lastTokenIsSlash) {
current.append("\\") // unescaped
lastTokenIsSlash = false
}
current.append(nextTok)
}
else -> {
if (lastTokenIsSlash) {
current.append(nextTok)
lastTokenIsSlash = false
} else if ("\\" == nextTok)
lastTokenIsSlash = true
else if ("\'" == nextTok) {
state = inQuote
} else if ("\"" == nextTok) {
state = inDoubleQuote
} else if (" " == nextTok) {
if (lastTokenHasBeenQuoted || current.isNotEmpty()) {
result.add(current.toString())
current.setLength(0)
}
} else {
current.append(nextTok)
}
lastTokenHasBeenQuoted = false
}
}
}
if (lastTokenHasBeenQuoted || current.isNotEmpty()) {
result.add(current.toString())
}
if (state == inQuote || state == inDoubleQuote) {
throw IllegalArgumentException("unbalanced quotes in $toProcess")
}
if (lastTokenIsSlash) throw IllegalArgumentException("escape character following nothing in $toProcess")
return result.toTypedArray()
}
}

View file

@ -18,7 +18,7 @@
* *
*******************************************************************************/
package com.github.shadowsocks.utils
package org.amnezia.vpn.shadowsocks.core.utils
object Key {
/**
@ -30,13 +30,27 @@ object Key {
const val id = "profileId"
const val name = "profileName"
const val individual = "Proxyed"
const val serviceMode = "serviceMode"
const val modeProxy = "proxy"
const val modeVpn = "vpn"
const val modeTransproxy = "transproxy"
const val shareOverLan = "shareOverLan"
const val portProxy = "portProxy"
const val portLocalDns = "portLocalDns"
const val portTransproxy = "portTransproxy"
const val route = "route"
const val isAutoConnect = "isAutoConnect"
const val directBootAware = "directBootAware"
const val proxyApps = "isProxyApps"
const val bypass = "isBypassApps"
const val udpdns = "isUdpDns"
const val ipv6 = "isIpv6"
const val metered = "metered"
const val host = "proxy"
const val password = "sitekey"
@ -44,17 +58,26 @@ object Key {
const val remotePort = "remotePortNum"
const val remoteDns = "remoteDns"
const val plugin = "plugin"
const val pluginConfigure = "plugin.configure"
const val udpFallback = "udpFallback"
const val dirty = "profileDirty"
const val tfo = "tcp_fastopen"
const val hosts = "hosts"
const val assetUpdateTime = "assetUpdateTime"
// TV specific values
const val controlStats = "control.stats"
const val controlImport = "control.import"
const val controlExport = "control.export"
const val about = "about"
}
object Action {
const val SERVICE = "com.github.shadowsocks.SERVICE"
const val CLOSE = "com.github.shadowsocks.CLOSE"
const val RELOAD = "com.github.shadowsocks.RELOAD"
const val SERVICE = "com.kyle.shadowsocks.SERVICE"
const val CLOSE = "com.kyle.shadowsocks.CLOSE"
const val RELOAD = "com.kyle.shadowsocks.RELOAD"
const val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
const val EXTRA_PROFILE_ID = "com.kyle.shadowsocks.EXTRA_PROFILE_ID"
}

View file

@ -18,7 +18,7 @@
* *
*******************************************************************************/
package com.github.shadowsocks.utils
package org.amnezia.vpn.shadowsocks.core.utils
import android.annotation.SuppressLint
import android.annotation.TargetApi

View file

@ -1,16 +1,16 @@
package com.github.shadowsocks.utils
package org.amnezia.vpn.shadowsocks.core.utils
import android.annotation.TargetApi
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.database.ProfileManager
import com.github.shadowsocks.preference.DataStore
import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.shadowsocks.core.Core.app
import org.amnezia.vpn.shadowsocks.core.bg.BaseService
import org.amnezia.vpn.shadowsocks.core.database.Profile
import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
import org.amnezia.vpn.shadowsocks.core.preference.DataStore
import java.io.File
import java.io.IOException
import java.io.ObjectInputStream
@ -23,9 +23,7 @@ object DirectBoot : BroadcastReceiver() {
fun getDeviceProfile(): Pair<Profile, Profile?>? = try {
ObjectInputStream(file.inputStream()).use { it.readObject() as? Pair<Profile, Profile?> }
} catch (_: IOException) {
null
}
} catch (_: IOException) { null }
fun clean() {
file.delete()
@ -38,9 +36,7 @@ object DirectBoot : BroadcastReceiver() {
*/
fun update(profile: Profile? = ProfileManager.getProfile(DataStore.profileId)) =
if (profile == null) clean()
else ObjectOutputStream(file.outputStream()).use {
it.writeObject(ProfileManager.expand(profile))
}
else ObjectOutputStream(file.outputStream()).use { it.writeObject(ProfileManager.expand(profile)) }
fun flushTrafficStats() {
getDeviceProfile()?.also { (profile, fallback) ->
@ -55,7 +51,6 @@ object DirectBoot : BroadcastReceiver() {
app.registerReceiver(this, IntentFilter(Intent.ACTION_BOOT_COMPLETED))
registered = true
}
override fun onReceive(context: Context, intent: Intent) {
flushTrafficStats()
app.unregisterReceiver(this)

View file

@ -18,9 +18,8 @@
* *
*******************************************************************************/
package com.github.shadowsocks.utils
package org.amnezia.vpn.shadowsocks.core.utils
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ContentResolver
import android.content.Context
@ -36,64 +35,30 @@ import android.system.OsConstants
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.preference.Preference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import java.net.HttpURLConnection
import java.net.InetAddress
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
fun <T> Iterable<T>.forEachTry(action: (T) -> Unit) {
var result: Exception? = null
for (element in this) try {
action(element)
} catch (e: Exception) {
if (result == null) result = e else result.addSuppressed(e)
}
if (result != null) {
result.printStackTrace()
throw result
}
}
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name
private val parseNumericAddress by lazy @SuppressLint("DiscouragedPrivateApi") {
private val parseNumericAddress by lazy {
InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
isAccessible = true
}
}
/**
* A slightly more performant variant of parseNumericAddress.
* A slightly more performant variant of InetAddress.parseNumericAddress.
*
* Bug in Android 9.0 and lower: https://issuetracker.google.com/issues/123456213
* Bug: https://issuetracker.google.com/issues/123456213
*/
fun String?.parseNumericAddress(): InetAddress? =
Os.inet_pton(OsConstants.AF_INET, this) ?: Os.inet_pton(OsConstants.AF_INET6, this)?.let {
if (Build.VERSION.SDK_INT >= 29) it else parseNumericAddress.invoke(null,
this) as InetAddress
}
fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this)
?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { parseNumericAddress.invoke(null, this) as InetAddress }
fun <K, V> MutableMap<K, V>.computeIfAbsentCompat(key: K, value: () -> V) =
if (Build.VERSION.SDK_INT >= 24) computeIfAbsent(key) { value() } else this[key]
?: value().also { put(key, it) }
suspend fun <T> HttpURLConnection.useCancellable(block: suspend HttpURLConnection.() -> T): T {
return suspendCancellableCoroutine { cont ->
cont.invokeOnCancellation {
fun HttpURLConnection.disconnectFromMain() {
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() }
}
GlobalScope.launch(Dispatchers.IO) {
try {
cont.resume(block())
} catch (e: Throwable) {
cont.resumeWithException(e)
}
}
}
}
fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
@ -101,17 +66,16 @@ fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
return if (value < min || value > 65535) default else value
}
fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver =
object : BroadcastReceiver() {
fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = callback(context, intent)
}
}
fun ContentResolver.openBitmap(uri: Uri) =
if (Build.VERSION.SDK_INT >= 28) ImageDecoder.decodeBitmap(ImageDecoder.createSource(this, uri))
else BitmapFactory.decodeStream(openInputStream(uri))
val PackageInfo.signaturesCompat
get() = if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures
val PackageInfo.signaturesCompat get() =
if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures
/**
* Based on: https://stackoverflow.com/a/26348729/2245107
@ -122,11 +86,9 @@ fun Resources.Theme.resolveResourceId(@AttrRes resId: Int): Int {
return typedValue.resourceId
}
val Intent.datas
get() = listOfNotNull(data) + (clipData?.asIterable()?.mapNotNull { it.uri } ?: emptyList())
val Intent.datas get() = listOfNotNull(data) + (clipData?.asIterable()?.mapNotNull { it.uri } ?: emptyList())
fun printLog(t: Throwable) {
// Crashlytics.logException(t)
t.printStackTrace()
}

View file

@ -0,0 +1,97 @@
/*******************************************************************************
* *
* 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.plugin
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.os.Bundle
import android.os.ParcelFileDescriptor
import androidx.core.os.bundleOf
/**
* Base class for a native plugin provider. A native plugin provider offers read-only access to files that are required
* to run a plugin, such as binary files and other configuration files. To create a native plugin provider, extend this
* class, implement the abstract methods, and add it to your manifest like this:
*
* <pre class="prettyprint">&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;provider android:name="com.kyle.shadowsocks.$PLUGIN_ID.BinaryProvider"
* android:authorities="com.kyle.shadowsocks.plugin.$PLUGIN_ID.BinaryProvider"&gt;
* &lt;intent-filter&gt;
* &lt;category android:name="com.kyle.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" /&gt;
* &lt;/intent-filter&gt;
* &lt;/provider&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
*/
abstract class NativePluginProvider : ContentProvider() {
override fun getType(p0: Uri): String = "application/x-elf"
override fun onCreate(): Boolean = true
/**
* Provide all files needed for native plugin.
*
* @param provider A helper object to use to add files.
*/
protected abstract fun populateFiles(provider: PathProvider)
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?,
sortOrder: String?): Cursor {
check(selection == null && selectionArgs == null && sortOrder == null)
val result = MatrixCursor(projection)
populateFiles(PathProvider(uri, result))
return result
}
/**
* Returns executable entry absolute path. This is used if plugin is sharing UID with the host.
*
* Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the
* default behavior.
*
* @return Absolute path for executable entry.
*/
open fun getExecutable(): String = throw UnsupportedOperationException()
abstract fun openFile(uri: Uri?): ParcelFileDescriptor
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
check(mode == "r")
return openFile(uri)
}
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? = when (method) {
PluginContract.METHOD_GET_EXECUTABLE -> bundleOf(Pair(PluginContract.EXTRA_ENTRY, getExecutable()))
else -> super.call(method, arg, extras)
}
// Methods that should not be used
override fun insert(p0: Uri, p1: ContentValues?): Uri = throw UnsupportedOperationException()
override fun update(p0: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int =
throw UnsupportedOperationException()
override fun delete(uri: Uri, p1: String?, p2: Array<out String>?): Int = throw UnsupportedOperationException()
}

View file

@ -0,0 +1,53 @@
/*******************************************************************************
* *
* 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.plugin
import android.database.MatrixCursor
import android.net.Uri
import java.io.File
/**
* Helper class to provide relative paths of files to copy.
*/
class PathProvider internal constructor(baseUri: Uri, private val cursor: MatrixCursor) {
private val basePath = baseUri.path?.trim('/') ?: ""
fun addPath(path: String, mode: Int = 0b110100100): PathProvider {
val trimmed = path.trim('/')
if (trimmed.startsWith(basePath)) cursor.newRow()
.add(PluginContract.COLUMN_PATH, trimmed)
.add(PluginContract.COLUMN_MODE, mode)
return this
}
fun addTo(file: File, to: String = "", mode: Int = 0b110100100): PathProvider {
var sub = to + file.name
if (basePath.startsWith(sub)) if (file.isDirectory) {
sub += '/'
file.listFiles().forEach { addTo(it, sub, mode) }
} else addPath(sub, mode)
return this
}
fun addAt(file: File, at: String = "", mode: Int = 0b110100100): PathProvider {
if (basePath.startsWith(at))
if (file.isDirectory) file.listFiles().forEach { addTo(it, at, mode) } else addPath(at, mode)
return this
}
}

View file

@ -0,0 +1,118 @@
/*******************************************************************************
* *
* 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.plugin
/**
* The contract between the plugin provider and host. Contains definitions for the supported actions, extras, etc.
*
* This class is written in Java to keep Java interoperability.
*/
object PluginContract {
/**
* ContentProvider Action: Used for NativePluginProvider.
*
* Constant Value: "com.kyle.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
*/
const val ACTION_NATIVE_PLUGIN = "com.kyle.shadowsocks.plugin.ACTION_NATIVE_PLUGIN"
/**
* Activity Action: Used for ConfigurationActivity.
*
* Constant Value: "com.kyle.shadowsocks.plugin.ACTION_CONFIGURE"
*/
const val ACTION_CONFIGURE = "com.kyle.shadowsocks.plugin.ACTION_CONFIGURE"
/**
* Activity Action: Used for HelpActivity or HelpCallback.
*
* Constant Value: "com.kyle.shadowsocks.plugin.ACTION_HELP"
*/
const val ACTION_HELP = "com.kyle.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"
*
* Constant Value: "com.kyle.shadowsocks.plugin.EXTRA_ENTRY"
*/
const val EXTRA_ENTRY = "com.kyle.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"
*/
const val EXTRA_OPTIONS = "com.kyle.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"
</host_name> */
const val EXTRA_HELP_MESSAGE = "com.kyle.shadowsocks.plugin.EXTRA_HELP_MESSAGE"
/**
* The metadata key to retrieve plugin id. Required for plugins.
*
* Constant Value: "com.kyle.shadowsocks.plugin.id"
*/
const val METADATA_KEY_ID = "com.kyle.shadowsocks.plugin.id"
/**
* The metadata key to retrieve default configuration. Default value is empty.
*
* Constant Value: "com.kyle.shadowsocks.plugin.default_config"
*/
const val METADATA_KEY_DEFAULT_CONFIG = "com.kyle.shadowsocks.plugin.default_config"
const val METHOD_GET_EXECUTABLE = "shadowsocks:getExecutable"
/** ConfigurationActivity result: fallback to manual edit mode. */
const val RESULT_FALLBACK = 1
/**
* Relative to the file to be copied. This column is required.
*
* Example: "kcptun", "doc/help.txt"
*
* Type: String
*/
const val COLUMN_PATH = "path"
/**
* File mode bits. Default value is "644".
*
* Example: "755"
*
* Type: String
*/
const val COLUMN_MODE = "mode"
/**
* The scheme for general plugin actions.
*/
const val SCHEME = "plugin"
/**
* The authority for general plugin actions.
*/
const val AUTHORITY = "com.kyle.shadowsocks"
}

View file

@ -0,0 +1,110 @@
/*******************************************************************************
* *
* 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.plugin
import java.util.*
/**
* Helper class for processing plugin options.
*
* Based on: https://github.com/apache/ant/blob/588ce1f/src/main/org/apache/tools/ant/types/Commandline.java
*/
class PluginOptions : HashMap<String, String?> {
var id = ""
constructor() : super()
constructor(initialCapacity: Int) : super(initialCapacity)
constructor(initialCapacity: Int, loadFactor: Float) : super(initialCapacity, loadFactor)
private constructor(options: String?, parseId: Boolean) : this() {
@Suppress("NAME_SHADOWING")
var parseId = parseId
if (options.isNullOrEmpty()) return
check(options.all { !it.isISOControl() }) { "No control characters allowed." }
val tokenizer = StringTokenizer("$options;", "\\=;", true)
val current = StringBuilder()
var key: String? = null
while (tokenizer.hasMoreTokens()) when (val nextToken = tokenizer.nextToken()) {
"\\" -> current.append(tokenizer.nextToken())
"=" -> if (key == null) {
key = current.toString()
current.setLength(0)
} else current.append(nextToken)
";" -> {
if (key != null) {
put(key, current.toString())
key = null
} else if (current.isNotEmpty())
if (parseId) id = current.toString() else put(current.toString(), null)
current.setLength(0)
parseId = false
}
else -> current.append(nextToken)
}
}
constructor(options: String?) : this(options, true)
constructor(id: String, options: String?) : this(options, false) {
this.id = id
}
/**
* Put but if value is null or default, the entry is deleted.
*
* @return Old value before put.
*/
fun putWithDefault(key: String, value: String?, default: String? = null) =
if (value == null || value == default) remove(key) else put(key, value)
private fun append(result: StringBuilder, str: String) = (0 until str.length)
.map { str[it] }
.forEach {
when (it) {
'\\', '=', ';' -> {
result.append('\\') // intentionally no break
result.append(it)
}
else -> result.append(it)
}
}
fun toString(trimId: Boolean): String {
val result = StringBuilder()
if (!trimId) if (id.isEmpty()) return "" else append(result, id)
for ((key, value) in entries) {
if (result.isNotEmpty()) result.append(';')
append(result, key)
if (value != null) {
result.append('=')
append(result, value)
}
}
return result.toString()
}
override fun toString(): String = toString(true)
override fun equals(other: Any?): Boolean {
if (this === other) return true
return javaClass == other?.javaClass && super.equals(other) && id == (other as PluginOptions).id
}
override fun hashCode(): Int = Objects.hash(super.hashCode(), id)
}

View file

@ -273,6 +273,7 @@ android {
ANDROID_EXTRA_LIBS += $$PWD/android/lib/shadowsocks/$${abi}/libss-local.so
ANDROID_EXTRA_LIBS += $$PWD/android/lib/shadowsocks/$${abi}/libtun2socks.so
ANDROID_EXTRA_LIBS += $$PWD/android/lib/shadowsocks/$${abi}/libredsocks.so
}
}
@ -370,5 +371,7 @@ ios {
# %{sourceDir}/client/ios/xcode_patcher.rb %{buildDir}/client/AmneziaVPN.xcodeproj 2.0 2.0.0 ios 1
}
DISTFILES +=

View file

@ -158,6 +158,7 @@ bool ContainerProps::isSupportedByCurrentPlatform(DockerContainer c)
switch (c) {
case DockerContainer::WireGuard: return true;
case DockerContainer::OpenVpn: return true;
case DockerContainer::ShadowSocks: return true;
default: return false;
}