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 --> <!-- %%INSERT_FEATURES -->
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/> <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"> <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> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
@ -76,17 +77,21 @@
<!-- extract android style --> <!-- extract android style -->
<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splashscreen"/> <meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/splashscreen"/>
</activity> </activity>
<service android:name=".VPNService" android:process=":QtOnlyProcess">
<service android:name=".VPNService" <meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
android:process=":QtOnlyProcess" <meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
android:permission="android.permission.BIND_VPN_SERVICE"> <meta-data android:name="android.app.repository" android:value="default"/>
<intent-filter> <meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<action android:name="android.net.VpnService"/> <meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
</intent-filter> <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>
<service android:name="org.amnezia.vpn.qt.VPNPermissionHelper">
<service android:name="org.amnezia.vpn.qt.VPNPermissionHelper" <meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
android:permission="android.permission.BIND_VPN_SERVICE">
</service> </service>
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices --> <!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
</application> </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 { interface IShadowsocksService {
int getState(); 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 127.0.0.0/8
169.254.0.0/16 169.254.0.0/16
172.16.0.0/12 172.16.0.0/12
192.0.0.0/24 192.0.0.0/29
192.0.2.0/24 192.0.2.0/24
192.31.196.0/24
192.52.193.0/24
192.88.99.0/24 192.88.99.0/24
192.168.0.0/16 192.168.0.0/16
192.175.48.0/24
198.18.0.0/15 198.18.0.0/15
198.51.100.0/24 198.51.100.0/24
203.0.113.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 { buildscript {
ext{ ext{
kotlin_version = "1.5.0" kotlin_version = "1.4.30-M1"
// for libwg // for libwg
appcompatVersion = '1.1.0' appcompatVersion = '1.1.0'
annotationsVersion = '1.0.1' annotationsVersion = '1.0.1'
@ -19,6 +21,8 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.0.0' 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-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$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'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
apply plugin: 'kotlin-kapt'
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'androidx.core:core-ktx:1.1.0' 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-crypto:1.1.0-alpha03"
implementation "androidx.security:security-identity-credential:1.0.0-alpha02" implementation "androidx.security:security-identity-credential:1.0.0-alpha02"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10" coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"
//ss //ss
implementation "androidx.preference:preference:1.1.0" implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0'
implementation "androidx.work:work-runtime-ktx:2.3.4" 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.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.room:room-runtime:2.2.5" // runtime 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 "com.google.code.gson:gson:2.8.5"
implementation "dnsjava:dnsjava:2.1.9"
implementation "org.connectbot.jsocks:jsocks:1.0.0" 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 { android {
@ -86,6 +110,7 @@ android {
renderscript.srcDirs = ['src'] renderscript.srcDirs = ['src']
assets.srcDirs = ['assets'] assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs'] jniLibs.srcDirs = ['libs']
androidTest.assets.srcDirs += files("${qt5AndroidDir}/schemas".toString())
} }
} }
@ -114,6 +139,10 @@ android {
targetSdkVersion = 30 targetSdkVersion = 30
versionCode 8 // Change to a higher number versionCode 8 // Change to a higher number
versionName "2.0.8" // Change to a higher number versionName "2.0.8" // Change to a higher number
javaCompileOptions.annotationProcessorOptions.arguments = [
"room.schemaLocation": "${qt5AndroidDir}/schemas".toString()
]
} }
buildTypes { 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 # http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # 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 # Gradle caching allows reusing the build artifacts from a previous
# build with the same inputs. However, over time, the cache size will # build with the same inputs. However, over time, the cache size will
@ -21,3 +22,7 @@ androidBuildToolsVersion=30.0.2
androidCompileSdkVersion=30 androidCompileSdkVersion=30
org.gradle.caching=true org.gradle.caching=true
org.gradle.parallel=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"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Shadowsocks</string> <string name="app_name">shadowsocks</string>
<string name="send_email">Send email</string>
<!-- ssplugin --> <string name="service_mode_vpn">VPN</string>
<string name="proxy_cat">Server Settings</string> <string name="speed">%s/s</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="quick_toggle">"Switch"</string>
<string name="no">No</string> <string name="remote_dns">"Remote DNS"</string>
<string name="apply">Apply</string> <string name="stat_summary">"Upload: \t%3$s\t↑\t%1$s
<string name="file_manager_missing">File Explorer Missing</string> Download: \t%4$s\t↓\t%2$s"</string>
<string name="browse">Browse…</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 --> <!-- proxy category -->
<string name="profile_name">Profile Name</string> <string name="profile_name">"Profile name"</string>
<string name="proxy">Server</string> <string name="proxy">"Server"</string>
<string name="remote_port">Remote Port</string> <string name="remote_port">"Remote Port"</string>
<string name="sitekey">Password</string> <string name="sitekey">"Password"</string>
<string name="enc_method">Encrypt Method</string> <string name="enc_method">"Encryption"</string>
<!-- feature category --> <!-- feature category -->
<string name="ipv6">IPv6 Route</string> <string name="ipv6">"IPv6 routing"</string>
<string name="ipv6_summary">Redirect IPv6 traffic to remote</string> <string name="ipv6_summary">"Forward IPv6 traffic to remote server"</string>
<string name="on">On</string> <string name="route_list">"Routing"</string>
<string name="off">Off</string> <string name="route_entry_gfwlist">"GFW List"</string>
<string name="tcp_fastopen_summary">Toggling might require ROOT permission</string> <string name="proxied_apps">"Proxied VPN"</string>
<string name="tcp_fastopen_summary_unsupported">Unsupported kernel version: %s &lt; 3.7.1</string> <string name="proxied_apps_summary">"Allow some apps to bypass VPN"</string>
<string name="tcp_fastopen_failure">Toggle failed</string> <string name="on">"On"</string>
<string name="udp_dns">Send DNS over UDP</string> <string name="bypass_apps">"Bypass"</string>
<string name="udp_dns_summary">Requires UDP forwarding on server side</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 --> <!-- notification category -->
<string name="service_vpn">VPN Service</string> <string name="forward_success">"Background service has started running. "</string>
<string name="forward_success">Shadowsocks started.</string> <string name="invalid_server">"Invalid server name"</string>
<string name="invalid_server">Invalid server name</string> <string name="service_failed">"Unable to connect to remote server"</string>
<string name="service_failed">Failed to connect the remote server</string> <string name="stop">"Stop"</string>
<string name="stop">Stop</string> <string name="stopping">"stopping…"</string>
<string name="stopping">Shutting down…</string> <string name="vpn_error">"Background service failed to start: %s"</string>
<string name="vpn_error">%s</string> <string name="reboot_required">"VPN service failed to start. You may need to restart your device."</string>
<string name="vpn_permission_denied">Permission denied to create a VPN service</string> <string name="profile_invalid_input">"No valid configuration file found."</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>
<!-- alert category --> <!-- alert category -->
<string name="profile_empty">Please select a profile</string> <string name="profile_empty">"Please select a profile"</string>
<string name="proxy_empty">Proxy/Password should not be empty</string> <string name="proxy_empty">"The proxy server address and password cannot be empty"</string>
<string name="connect">Connect</string> <string name="connect">"Connect"</string>
<!-- menu category --> <!-- menu category -->
<string name="profiles">Profiles</string> <string name="profiles">"Profiles"</string>
<string name="settings">Settings</string> <string name="settings">"Settings"</string>
<string name="about">About</string> <string name="faq">"FAQ"</string>
<string name="about_title">Shadowsocks %s</string> <string name="about">"About"</string>
<string name="edit">Edit</string> <string name="about_title">"Shadowsocks %s"</string>
<string name="share">Share</string> <string name="edit">"Edit"</string>
<string name="add_profile">Add Profile</string> <string name="share">"Share"</string>
<string name="action_apply_all">Apply Settings to All Profiles</string> <string name="add_profile">"Add Profile"</string>
<string name="action_export_more">Export…</string> <string name="action_apply_all">"Apply settings to all profiles"</string>
<string name="action_export_file">Export to file…</string> <string name="action_export">"Export to clipboard"</string>
<string name="action_export">Export to Clipboard</string> <string name="action_import">"Import from clipboard"</string>
<string name="action_import">Import from Clipboard</string> <string name="action_export_msg">"Export to clipboard succeeded"</string>
<string name="action_import_file">Import from file…</string> <string name="action_export_err">"Export to clipboard failed"</string>
<string name="action_replace_file">Replace from file…</string> <string name="action_import_msg">"Import successful"</string>
<string name="action_export_msg">Successfully export!</string> <string name="action_import_err">"Import failed"</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>
<!-- profile --> <!-- profile -->
<string name="profile_config">Profile config</string> <string name="profile_config">"Profile Config"</string>
<string name="delete">Remove</string> <string name="delete">"Delete"</string>
<string name="delete_confirm_prompt">Are you sure you want to remove this profile?</string> <string name="delete_confirm_prompt">"Are you sure you want to delete this profile?"</string>
<string name="share_qr_nfc">QR code</string> <string name="share_qr_nfc">"QR code / NFC"</string>
<string name="add_profile_dialog">Add this Shadowsocks Profile?</string> <string name="add_profile_dialog">"Add this profile for Shadowsock?"</string>
<string name="add_profile_methods_scan_qr_code">Scan QR code</string> <string name="add_profile_methods_scan_qr_code">"Scan QR code"</string>
<string name="add_profile_methods_manual_settings">Manual Settings</string> <plurals name="removed">
<string name="add_profile_scanner_permission_required">Camera permission is required for scanning QR code.</string> <item quantity="other">"%d items deleted"</item>
<string name="undo">Undo</string> </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 --> <!-- status -->
<string name="connecting">Connecting…</string> <string name="sent">"Send: "</string>
<string name="vpn_connected">Connected, tap to check connection</string> <string name="received">"Received"</string>
<string name="not_connected">Not connected</string>
<string name="sent">Sent</string> <!-- status -->
<string name="received">Received</string> <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 --> <!-- misc -->
<string name="add_first_profile">There is no profile currently, would you like to add it now?</string> <string name="advanced">"Advanced options"</string>
<string name="port_proxy">SOCKS5 proxy port</string>
<string name="port_local_dns">Local DNS port</string>
<string name="quick_toggle">Toggle</string> <!-- misc -->
<string name="remote_dns">Remote DNS</string> <string name="service_mode">"Service mode"</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="service_mode_proxy">"Proxy only"</string>
<string name="connection_test_pending">Check Connectivity</string> <string name="service_mode_transproxy">"Transparent proxy"</string>
<string name="connection_test_testing">Testing…</string> <string name="port_proxy">"SOCKS5 proxy port"</string>
<string name="connection_test_available">Success: HTTPS handshake took %dms</string> <string name="port_local_dns">"local DNS port"</string>
<string name="connection_test_error">Fail to detect internet connection: %s</string> <string name="port_transproxy">"Transparent proxy port"</string>
<string name="connection_test_fail">Internet Unavailable</string> <string name="service_proxy">"Proxy mode"</string>
<string name="connection_test_error_status_code">Error code: #%d</string> <string name="service_transproxy">"Transparent proxy mode"</string>
<string name="vpn_permission_denied">"Insufficient permission to create VPN service"</string>
<string name="speed" translatable="false">%s/s</string> <string name="auto_connect_summary_v24">"Allow Shadowsocks to start with the system, an always-on VPN is recommended"</string>
<string name="traffic" translatable="false">%1$s↑\t%2$s↓</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>
<plurals name="removed"> <string name="acl_rule_online_config">"Online Rules File URL"</string>
<item quantity="one">Removed</item> <string name="action_import_file">"Import from file…"</string>
<item quantity="other">%d items removed</item> <string name="night_mode">"Night Mode"</string>
</plurals> <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> </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()}") Log.e(tag, "Wireguard Version ${wgVersion()}")
mOpenVPNThreadv3 = OpenVPNThreadv3(this) mOpenVPNThreadv3 = OpenVPNThreadv3(this)
mAlreadyInitialised = true mAlreadyInitialised = true
} }
override fun onUnbind(intent: Intent?): Boolean { override fun onUnbind(intent: Intent?): Boolean {
@ -82,10 +81,7 @@ class VPNService : android.net.VpnService() {
val lastConfString = prefs.getString("lastConf", "") val lastConfString = prefs.getString("lastConf", "")
if (lastConfString.isNullOrEmpty()) { if (lastConfString.isNullOrEmpty()) {
// We have nothing to connect to -> Exit // We have nothing to connect to -> Exit
Log.e( Log.e(tag,"VPN service was triggered without defining a Server or having a tunnel")
tag,
"VPN service was triggered without defining a Server or having a tunnel"
)
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
this.mConfig = JSONObject(lastConfString) this.mConfig = JSONObject(lastConfString)
@ -156,10 +152,13 @@ class VPNService : android.net.VpnService() {
} }
Log.i(tag, "Permission okay") Log.i(tag, "Permission okay")
mConfig = json!! mConfig = json!!
Log.i(tag, "Config: " + mConfig)
mProtocol = mConfig!!.getString("protocol") mProtocol = mConfig!!.getString("protocol")
Log.i(tag, "Protocol: " + mProtocol)
when (mProtocol) { when (mProtocol) {
"openvpn" -> startOpenVpn() "openvpn" -> startOpenVpn()
"wireguard" -> startWireGuard() "wireguard" -> startWireGuard()
"shadowsocks" -> startShadowsocks()
else -> { else -> {
Log.e(tag, "No protocol") Log.e(tag, "No protocol")
return 0 return 0
@ -365,6 +364,19 @@ class VPNService : android.net.VpnService() {
return mConfig!! 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() { private fun startOpenVpn() {
mOpenVPNThreadv3 = OpenVPNThreadv3(this) mOpenVPNThreadv3 = OpenVPNThreadv3(this)
Thread({ Thread({

View file

@ -54,7 +54,7 @@ class VPNServiceBinder(service: VPNService) : Binder() {
val json = buffer?.let { String(it) } val json = buffer?.let { String(it) }
val config = JSONObject(json) val config = JSONObject(json)
Log.v(tag, "Stored new Tunnel config in Service") Log.v(tag, "Stored new Tunnel config in Service")
Log.i(tag, "Config: $config")
if (!mService.checkPermissions()) { if (!mService.checkPermissions()) {
mResumeConfig = config mResumeConfig = config
// The Permission prompt was already // 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; package org.amnezia.vpn.qt;
import android.app.Activity; import android.content.res.Configuration;
import android.os.Bundle; import androidx.annotation.NonNull;
import org.amnezia.vpn.shadowsocks.core.Core;
import org.amnezia.vpn.BuildConfig; import org.amnezia.vpn.shadowsocks.core.VpnManager;
public class VPNApplication extends org.qtproject.qt5.android.bindings.QtApplication { public class VPNApplication extends org.qtproject.qt5.android.bindings.QtApplication {
private static VPNApplication instance; private static VPNApplication instance;
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
VPNApplication.instance = this; 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.Application
import android.app.NotificationChannel import android.app.NotificationChannel
@ -31,7 +31,6 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.os.Build import android.os.Build
import android.os.UserManager import android.os.UserManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@ -39,17 +38,17 @@ import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.work.Configuration import androidx.work.Configuration
import androidx.work.WorkManager import androidx.work.WorkManager
import com.github.shadowsocks.aidl.ShadowsocksConnection
import com.github.shadowsocks.database.Profile import org.amnezia.vpn.R
import com.github.shadowsocks.database.ProfileManager import org.amnezia.vpn.shadowsocks.core.acl.Acl
import com.github.shadowsocks.net.TcpFastOpen import org.amnezia.vpn.shadowsocks.core.aidl.ShadowsocksConnection
import com.github.shadowsocks.preference.DataStore import org.amnezia.vpn.shadowsocks.core.database.Profile
import com.github.shadowsocks.utils.* 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_NAME
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON 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.File
import java.io.IOException import java.io.IOException
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -59,16 +58,16 @@ object Core {
lateinit var app: Application lateinit var app: Application
lateinit var configureIntent: (Context) -> PendingIntent lateinit var configureIntent: (Context) -> PendingIntent
val connectivity by lazy { app.getSystemService<ConnectivityManager>()!! }
val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) } val packageInfo: PackageInfo by lazy { getPackageInfo(app.packageName) }
val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) } val deviceStorage by lazy { if (Build.VERSION.SDK_INT < 24) app else DeviceStorageApp(app) }
val directBootSupported by lazy { 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 val activeProfileIds
get() = ProfileManager.getProfile(DataStore.profileId).let { 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?>? val currentProfile: Pair<Profile, Profile?>?
get() { get() {
@ -84,37 +83,34 @@ object Core {
} }
fun init(app: Application, configureClass: KClass<out Any>) { fun init(app: Application, configureClass: KClass<out Any>) {
this.app = app Core.app = app
this.configureIntent = { configureIntent = {
PendingIntent.getActivity(it, PendingIntent.getActivity(it, 0, Intent(it, configureClass.java)
0, .setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)
Intent(it,
configureClass.java).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
0)
} }
if (Build.VERSION.SDK_INT >= 24) { // migrate old files if (Build.VERSION.SDK_INT >= 24) { // migrate old files
deviceStorage.moveDatabaseFrom(app, Key.DB_PUBLIC) 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 // 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) System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
// Fabric.with(deviceStorage, Crashlytics()) // multiple processes needs manual set-up WorkManager.initialize(deviceStorage, Configuration.Builder().build())
// FirebaseApp.initializeApp(deviceStorage)
WorkManager.initialize(deviceStorage, Configuration.Builder().apply {
setExecutor { GlobalScope.launch { it.run() } }
setTaskExecutor { GlobalScope.launch { it.run() } }
}.build())
// handle data restored/crash // 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.tcpFastOpen && !TcpFastOpen.sendEnabled) TcpFastOpen.enableTimeout()
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) { if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != packageInfo.lastUpdateTime) {
val assetManager = app.assets val assetManager = app.assets
try { try {
for (file in assetManager.list("acl")!!) assetManager.open("acl/$file").use { input -> for (file in assetManager.list("acl")!!) assetManager.open("acl/$file").use { input ->
File(deviceStorage.noBackupFilesDir, file).outputStream() File(ContextCompat.getNoBackupFilesDir(deviceStorage), file).outputStream().use { output -> input.copyTo(output) }
.use { output -> input.copyTo(output) }
} }
} catch (e: IOException) { } catch (e: IOException) {
printLog(e) printLog(e)
@ -127,11 +123,13 @@ object Core {
fun updateNotificationChannels() { fun updateNotificationChannels() {
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) { if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
val nm = app.getSystemService<NotificationManager>()!! val nm = app.getSystemService<NotificationManager>()!!
nm.createNotificationChannels(listOf(NotificationChannel("service-vpn", nm.createNotificationChannels(listOf(
app.getText(R.string.service_vpn), NotificationChannel("service-vpn", app.getText(R.string.service_vpn),
if (Build.VERSION.SDK_INT >= 28) NotificationManager.IMPORTANCE_MIN NotificationManager.IMPORTANCE_LOW),
else NotificationManager.IMPORTANCE_LOW) // #1355 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 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 if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!! else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES)!!
fun startService() = fun startService() = ContextCompat.startForegroundService(app, Intent(app, ShadowsocksConnection.serviceClass))
ContextCompat.startForegroundService(app, Intent(app, ShadowsocksConnection.serviceClass))
fun reloadService() = app.sendBroadcast(Intent(Action.RELOAD)) fun reloadService() = app.sendBroadcast(Intent(Action.RELOAD))
fun stopService() = app.sendBroadcast(Intent(Action.CLOSE)) fun stopService() = app.sendBroadcast(Intent(Action.CLOSE))
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) = fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) = object : BroadcastReceiver() {
object : BroadcastReceiver() {
init { init {
app.registerReceiver(this, IntentFilter().apply { app.registerReceiver(this, IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED) 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.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.DeadObjectException
import android.os.Handler import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.RemoteException import android.os.RemoteException
import com.github.shadowsocks.LocalVpnService import org.amnezia.vpn.shadowsocks.core.bg.BaseService
import com.github.shadowsocks.bg.BaseService import org.amnezia.vpn.shadowsocks.core.bg.ProxyService
import com.github.shadowsocks.utils.Action 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. * This object should be compact as it will not get GC-ed.
*/ */
class ShadowsocksConnection( class ShadowsocksConnection(private val handler: Handler = Handler(),
private val handler: Handler = Handler(), private var listenForDeath: Boolean = false) :
private var listenForDeath: Boolean = false ServiceConnection, IBinder.DeathRecipient {
) : ServiceConnection,
IBinder.DeathRecipient {
companion object { 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 { interface Callback {
fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)
fun trafficUpdated(profileId: Long, stats: TrafficStats) {} fun trafficUpdated(profileId: Long, stats: TrafficStats) { }
fun trafficPersisted(profileId: Long) {} fun trafficPersisted(profileId: Long) { }
fun onServiceConnected(service: IShadowsocksService) fun onServiceConnected(service: IShadowsocksService)
/** /**
* Different from Android framework, this method will be called even when you call `detachService`. * Different from Android framework, this method will be called even when you call `detachService`.
*/ */
fun onServiceDisconnected() {} fun onServiceDisconnected() { }
fun onBinderDied() { }
fun onBinderDied() {}
} }
private var connectionActive = false private var connectionActive = false
@ -64,16 +70,14 @@ class ShadowsocksConnection(
private val serviceCallback = object : IShadowsocksServiceCallback.Stub() { private val serviceCallback = object : IShadowsocksServiceCallback.Stub() {
override fun stateChanged(state: Int, profileName: String?, msg: String?) { override fun stateChanged(state: Int, profileName: String?, msg: String?) {
val callback = callback ?: return val callback = callback ?: return
handler.post { handler.post { callback.stateChanged(BaseService.State.values()[state], profileName, msg) }
callback.stateChanged(BaseService.State.values()[state], profileName, msg)
} }
}
override fun trafficUpdated(profileId: Long, stats: TrafficStats) { override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
val callback = callback ?: return val callback = callback ?: return
handler.post { callback.trafficUpdated(profileId, stats) } handler.post {
callback.trafficUpdated(profileId, stats)
}
} }
override fun trafficPersisted(profileId: Long) { override fun trafficPersisted(profileId: Long) {
val callback = callback ?: return val callback = callback ?: return
handler.post { callback.trafficPersisted(profileId) } handler.post { callback.trafficPersisted(profileId) }
@ -83,30 +87,25 @@ class ShadowsocksConnection(
var bandwidthTimeout = 0L var bandwidthTimeout = 0L
set(value) { set(value) {
try { val service = service
if (value > 0) service?.startListeningForBandwidth(serviceCallback, value) if (bandwidthTimeout != value && service != null)
else service?.stopListeningForBandwidth(serviceCallback) if (value > 0) service.startListeningForBandwidth(serviceCallback, value) else try {
} catch (_: RemoteException) { service.stopListeningForBandwidth(serviceCallback)
} } catch (_: DeadObjectException) { }
field = value field = value
} }
var service: IShadowsocksService? = null var service: IShadowsocksService? = null
override fun onServiceConnected(name: ComponentName?, binder: IBinder) { override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
this.binder = binder this.binder = binder
if (listenForDeath) binder.linkToDeath(this, 0)
val service = IShadowsocksService.Stub.asInterface(binder)!! val service = IShadowsocksService.Stub.asInterface(binder)!!
this.service = service this.service = service
try { if (!callbackRegistered) try {
if (listenForDeath) binder.linkToDeath(this, 0)
check(!callbackRegistered)
service.registerCallback(serviceCallback) service.registerCallback(serviceCallback)
callbackRegistered = true callbackRegistered = true
if (bandwidthTimeout > 0) service.startListeningForBandwidth( if (bandwidthTimeout > 0) service.startListeningForBandwidth(serviceCallback, bandwidthTimeout)
serviceCallback, } catch (_: RemoteException) { }
bandwidthTimeout
)
} catch (_: RemoteException) {
}
callback!!.onServiceConnected(service) callback!!.onServiceConnected(service)
} }
@ -119,7 +118,6 @@ class ShadowsocksConnection(
override fun binderDied() { override fun binderDied() {
service = null service = null
callbackRegistered = false
callback?.also { handler.post(it::onBinderDied) } callback?.also { handler.post(it::onBinderDied) }
} }
@ -127,8 +125,7 @@ class ShadowsocksConnection(
val service = service val service = service
if (service != null && callbackRegistered) try { if (service != null && callbackRegistered) try {
service.unregisterCallback(serviceCallback) service.unregisterCallback(serviceCallback)
} catch (_: RemoteException) { } catch (_: RemoteException) { }
}
callbackRegistered = false callbackRegistered = false
} }
@ -145,15 +142,11 @@ class ShadowsocksConnection(
unregisterCallback() unregisterCallback()
if (connectionActive) try { if (connectionActive) try {
context.unbindService(this) context.unbindService(this)
} catch (_: IllegalArgumentException) { } catch (_: IllegalArgumentException) { } // ignore
} // ignore
connectionActive = false connectionActive = false
if (listenForDeath) binder?.unlinkToDeath(this, 0) if (listenForDeath) binder?.unlinkToDeath(this, 0)
binder = null binder = null
try {
service?.stopListeningForBandwidth(serviceCallback) service?.stopListeningForBandwidth(serviceCallback)
} catch (_: RemoteException) {
}
service = null service = null
callback = 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.Parcel
import android.os.Parcelable import android.os.Parcelable
data class TrafficStats( data class TrafficStats(
// Bytes per second // Bytes per second
var txRate: Long = 0L, var rxRate: Long = 0L, var txRate: Long = 0L,
var rxRate: Long = 0L,
// Bytes for the current session // Bytes for the current session
var txTotal: Long = 0L, var rxTotal: Long = 0L) : Parcelable { var txTotal: Long = 0L,
operator fun plus(other: TrafficStats) = TrafficStats(txRate + other.txRate, var rxTotal: Long = 0L
rxRate + other.rxRate, ) : Parcelable {
txTotal + other.txTotal, operator fun plus(other: TrafficStats) = TrafficStats(
rxTotal + other.rxTotal) txRate + other.txRate, rxRate + other.rxRate,
txTotal + other.txTotal, rxTotal + other.rxTotal)
constructor(parcel: Parcel) : this(parcel.readLong(),
parcel.readLong(),
parcel.readLong(),
parcel.readLong())
constructor(parcel: Parcel) : this(parcel.readLong(), parcel.readLong(), parcel.readLong(), parcel.readLong())
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeLong(txRate) parcel.writeLong(txRate)
parcel.writeLong(rxRate) parcel.writeLong(rxRate)
parcel.writeLong(txTotal) parcel.writeLong(txTotal)
parcel.writeLong(rxTotal) parcel.writeLong(rxTotal)
} }
override fun describeContents() = 0 override fun describeContents() = 0
companion object CREATOR : Parcelable.Creator<TrafficStats> { 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.app.Service
import android.content.Context import android.content.Context
@ -26,16 +26,21 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.* import android.os.*
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.github.shadowsocks.Core
import com.github.shadowsocks.Core.app import org.amnezia.vpn.shadowsocks.core.Core
import com.github.shadowsocks.aidl.IShadowsocksService import org.amnezia.vpn.shadowsocks.core.Core.app
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksService
import com.github.shadowsocks.aidl.TrafficStats import org.amnezia.vpn.shadowsocks.core.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.net.HostsFile import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
import com.github.shadowsocks.preference.DataStore import org.amnezia.vpn.shadowsocks.core.plugin.PluginManager
import com.github.shadowsocks.utils.* 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 kotlinx.coroutines.*
import java.io.File import java.io.File
import java.net.BindException
import java.net.InetAddress
import java.net.URL import java.net.URL
import java.net.UnknownHostException import java.net.UnknownHostException
import java.util.* import java.util.*
@ -50,26 +55,24 @@ object BaseService {
* Idle state is only used by UI and will never be returned by BaseService. * Idle state is only used by UI and will never be returned by BaseService.
*/ */
Idle, Idle,
Connecting(true), Connected(true), Stopping, Stopped, Connecting(true),
Connected(true),
Stopping,
Stopped,
} }
const val CONFIG_FILE = "shadowsocks.conf" const val CONFIG_FILE = "shadowsocks.conf"
const val CONFIG_FILE_UDP = "shadowsocks-udp.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) { class Data internal constructor(private val service: Interface) {
var state = State.Stopped var state = State.Stopped
var processes: GuardedProcessPool? = null var processes: GuardedProcessPool? = null
var proxy: ProxyInstance? = null var proxy: ProxyInstance? = null
// no udpFallback. xinlake var udpFallback: ProxyInstance? = null
var notification: ServiceNotification? = null var notification: ServiceNotification? = null
val closeReceiver = broadcastReceiver { _, intent -> val closeReceiver = broadcastReceiver { _, intent ->
when (intent.action) { when (intent.action) {
Intent.ACTION_SHUTDOWN -> service.persistStats()
Action.RELOAD -> service.forceLoad() Action.RELOAD -> service.forceLoad()
else -> service.stopRunner() else -> service.stopRunner()
} }
@ -86,16 +89,15 @@ object BaseService {
} }
} }
class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), CoroutineScope, AutoCloseable { class Binder(private var data: Data? = null) : IShadowsocksService.Stub(), AutoCloseable {
private val callbacks = object : RemoteCallbackList<IShadowsocksServiceCallback>() { val callbacks = object : RemoteCallbackList<IShadowsocksServiceCallback>() {
override fun onCallbackDied(callback: IShadowsocksServiceCallback?, cookie: Any?) { override fun onCallbackDied(callback: IShadowsocksServiceCallback?, cookie: Any?) {
super.onCallbackDied(callback, cookie) super.onCallbackDied(callback, cookie)
stopListeningForBandwidth(callback ?: return) stopListeningForBandwidth(callback ?: return)
} }
} }
private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier
override val coroutineContext = Dispatchers.Main.immediate + Job() private val handler = Handler()
private var looper: Job? = null
override fun getState(): Int = (data?.state ?: State.Idle).ordinal override fun getState(): Int = (data?.state ?: State.Idle).ordinal
override fun getProfileName(): String = data?.proxy?.profile?.name ?: "Idle" override fun getProfileName(): String = data?.proxy?.profile?.name ?: "Idle"
@ -105,27 +107,24 @@ object BaseService {
} }
private fun broadcast(work: (IShadowsocksServiceCallback) -> Unit) { private fun broadcast(work: (IShadowsocksServiceCallback) -> Unit) {
val count = callbacks.beginBroadcast() repeat(callbacks.beginBroadcast()) {
try {
repeat(count) {
try { try {
work(callbacks.getBroadcastItem(it)) work(callbacks.getBroadcastItem(it))
} catch (_: RemoteException) { } catch (_: DeadObjectException) {
} catch (e: Exception) { } catch (e: Exception) {
printLog(e) printLog(e)
} }
} }
} finally {
callbacks.finishBroadcast() callbacks.finishBroadcast()
} }
}
private suspend fun loop() { private fun registerTimeout() {
while (true) { handler.postDelayed(this::onTimeout, bandwidthListeners.values.min() ?: return)
// delay(bandwidthListeners.values.min() ?: return) }
delay(5000) private fun onTimeout() {
val proxies = listOfNotNull(data?.proxy) val proxies = listOfNotNull(data?.proxy, data?.udpFallback)
val stats = proxies.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) } val stats = proxies
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
.filter { it.second != null } .filter { it.second != null }
.map { Triple(it.first, it.second!!.first, it.second!!.second) } .map { Triple(it.first, it.second!!.first, it.second!!.second) }
if (stats.any { it.third } && data?.state == State.Connected && bandwidthListeners.isNotEmpty()) { 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) { override fun startListeningForBandwidth(cb: IShadowsocksServiceCallback, timeout: Long) {
launch { val wasEmpty = bandwidthListeners.isEmpty()
if (bandwidthListeners.isEmpty() and (bandwidthListeners.put(cb.asBinder(), timeout) == null)) { if (bandwidthListeners.put(cb.asBinder(), timeout) == null) {
check(looper == null) if (wasEmpty) registerTimeout()
looper = launch { loop() } if (data?.state != State.Connected) return
}
if (data?.state != State.Connected) return@launch
var sum = TrafficStats() var sum = TrafficStats()
val data = data val data = data
val proxy = data?.proxy ?: return@launch val proxy = data?.proxy ?: return
proxy.trafficMonitor?.out.also { stats -> proxy.trafficMonitor?.out.also { stats ->
cb.trafficUpdated(proxy.profile.id, if (stats == null) sum else { cb.trafficUpdated(proxy.profile.id, if (stats == null) sum else {
sum += stats sum += stats
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) cb.trafficUpdated(0, sum)
} }
} }
override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) { override fun stopListeningForBandwidth(cb: IShadowsocksServiceCallback) {
launch {
if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) { if (bandwidthListeners.remove(cb.asBinder()) != null && bandwidthListeners.isEmpty()) {
looper!!.cancel() handler.removeCallbacksAndMessages(null)
looper = null
}
} }
} }
@ -188,7 +189,7 @@ object BaseService {
override fun close() { override fun close() {
callbacks.kill() callbacks.kill()
cancel() handler.removeCallbacksAndMessages(null)
data = null data = null
} }
} }
@ -198,13 +199,13 @@ object BaseService {
val tag: String val tag: String
fun createNotification(profileName: String): ServiceNotification fun createNotification(profileName: String): ServiceNotification
fun onBind(intent: Intent): IBinder? = fun onBind(intent: Intent): IBinder? = if (intent.action == Action.SERVICE) data.binder else null
if (intent.action == Action.SERVICE) data.binder else null
fun forceLoad() { fun forceLoad() {
val (profile, fallback) = Core.currentProfile val (profile, fallback) = Core.currentProfile
?: return stopRunner(false, (this as Context).getString(R.string.profile_empty)) ?: return stopRunner(false, (this as Context).getString(R.string.profile_empty))
if (profile.host.isEmpty() || profile.password.isEmpty() || fallback != null && (fallback.host.isEmpty() || fallback.password.isEmpty())) { 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)) stopRunner(false, (this as Context).getString(R.string.proxy_empty))
return return
} }
@ -212,21 +213,25 @@ object BaseService {
when { when {
s == State.Stopped -> startRunner() s == State.Stopped -> startRunner()
s.canStop -> stopRunner(true) 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 fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> = cmd
suspend fun startProcesses(hosts: HostsFile) { suspend fun startProcesses() {
val configRoot = val configRoot = (if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
(if (Build.VERSION.SDK_INT < 24 || app.getSystemService<UserManager>()
?.isUserUnlocked != false) app else Core.deviceStorage).noBackupFilesDir ?.isUserUnlocked != false) app else Core.deviceStorage).noBackupFilesDir
val udpFallback = data.udpFallback
data.proxy!!.start(this, data.proxy!!.start(this,
File(Core.deviceStorage.noBackupFilesDir, "stat_main"), File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
File(configRoot, CONFIG_FILE), 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() { fun startRunner() {
@ -247,7 +252,6 @@ object BaseService {
// channge the state // channge the state
data.changeState(State.Stopping) data.changeState(State.Stopping)
GlobalScope.launch(Dispatchers.Main.immediate) { GlobalScope.launch(Dispatchers.Main.immediate) {
// Core.analytics.logEvent("stop", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.connectingJob?.cancelAndJoin() // ensure stop connecting first data.connectingJob?.cancelAndJoin() // ensure stop connecting first
this@Interface as Service this@Interface as Service
// we use a coroutineScope here to allow clean-up in parallel // we use a coroutineScope here to allow clean-up in parallel
@ -263,11 +267,12 @@ object BaseService {
data.notification?.destroy() data.notification?.destroy()
data.notification = null data.notification = null
val ids = listOfNotNull(data.proxy).map { val ids = listOfNotNull(data.proxy, data.udpFallback).map {
it.shutdown(this) it.shutdown(this)
it.profile.id it.profile.id
} }
data.proxy = null data.proxy = null
data.udpFallback = null
data.binder.trafficPersisted(ids) data.binder.trafficPersisted(ids)
} }
@ -275,17 +280,12 @@ object BaseService {
data.changeState(State.Stopped, msg) data.changeState(State.Stopped, msg)
// stop the service if nothing has bound to it // stop the service if nothing has bound to it
if (restart) startRunner() else { if (restart) startRunner() else stopSelf()
stopSelf()
}
} }
} }
fun persistStats() = suspend fun preInit() { }
listOfNotNull(data.proxy).forEach { it.trafficMonitor?.persistStats(it.profile.id) } suspend fun resolver(host: String) = InetAddress.getAllByName(host)
suspend fun preInit() {}
suspend fun resolver(host: String) = DnsResolverCompat.resolveOnActiveNetwork(host)
suspend fun openConnection(url: URL) = url.openConnection() suspend fun openConnection(url: URL) = url.openConnection()
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -299,10 +299,11 @@ object BaseService {
stopRunner(false, getString(R.string.profile_empty)) stopRunner(false, getString(R.string.profile_empty))
return Service.START_NOT_STICKY return Service.START_NOT_STICKY
} }
val (profile, _) = profilePair val (profile, fallback) = profilePair
profile.name = profile.formattedName // save name for later queries profile.name = profile.formattedName // save name for later queries
val proxy = ProxyInstance(profile) val proxy = ProxyInstance(profile)
data.proxy = proxy data.proxy = proxy
data.udpFallback = if (fallback == null) null else ProxyInstance(fallback, profile.route)
if (!data.closeReceiverRegistered) { if (!data.closeReceiverRegistered) {
registerReceiver(data.closeReceiver, IntentFilter().apply { registerReceiver(data.closeReceiver, IntentFilter().apply {
@ -314,29 +315,35 @@ object BaseService {
} }
data.notification = createNotification(profile.formattedName) data.notification = createNotification(profile.formattedName)
// Core.analytics.logEvent("start", bundleOf(Pair(FirebaseAnalytics.Param.METHOD, tag)))
data.changeState(State.Connecting) data.changeState(State.Connecting)
data.connectingJob = GlobalScope.launch(Dispatchers.Main) { data.connectingJob = GlobalScope.launch(Dispatchers.Main) {
try { try {
Executable.killAll() // clean up old processes Executable.killAll() // clean up old processes
preInit() preInit()
val hosts = HostsFile(DataStore.publicStore.getString(Key.hosts) ?: "") proxy.init(this@Interface)
proxy.init(this@Interface, hosts) data.udpFallback?.init(this@Interface)
data.processes = GuardedProcessPool { data.processes = GuardedProcessPool {
printLog(it) printLog(it)
stopRunner(false, it.readableMessage) stopRunner(false, it.readableMessage)
} }
startProcesses(hosts) startProcesses()
// proxy.scheduleUpdate() // XinLake. Bypass-LAN only
proxy.scheduleUpdate()
data.udpFallback?.scheduleUpdate()
data.changeState(State.Connected) data.changeState(State.Connected)
} catch (_: CancellationException) { } catch (_: CancellationException) {
// if the job was cancelled, it is canceller's responsibility to call stopRunner // if the job was cancelled, it is canceller's responsibility to call stopRunner
} catch (_: UnknownHostException) { } catch (_: UnknownHostException) {
stopRunner(false, getString(R.string.invalid_server)) stopRunner(false, getString(R.string.invalid_server))
} catch (exc: Throwable) { } 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}") stopRunner(false, "${getString(R.string.service_failed)}: ${exc.readableMessage}")
} finally { } finally {
data.connectingJob = null 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.ErrnoException
import android.system.Os import android.system.Os
import android.system.OsConstants import android.system.OsConstants
import android.text.TextUtils import android.text.TextUtils
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
object Executable { object Executable {
// libredsocks.so is not required. xinlake const val REDSOCKS = "libredsocks.so"
const val SS_LOCAL = "libss-local.so" const val SS_LOCAL = "libss-local.so"
const val TUN2SOCKS = "libtun2socks.so" const val TUN2SOCKS = "libtun2socks.so"
private val EXECUTABLES = setOf(SS_LOCAL, TUN2SOCKS) private val EXECUTABLES = setOf(SS_LOCAL, REDSOCKS, TUN2SOCKS)
fun killAll() { fun killAll() {
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) } for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
?: return) {
val exe = File(try { val exe = File(try {
File(process, "cmdline").inputStream().bufferedReader().readText() File(process, "cmdline").inputStream().bufferedReader().readText()
} catch (_: IOException) { } catch (_: IOException) {
@ -47,8 +47,6 @@ object Executable {
} catch (e: ErrnoException) { } catch (e: ErrnoException) {
if (e.errno != OsConstants.ESRCH) { if (e.errno != OsConstants.ESRCH) {
e.printStackTrace() 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.Build
import android.os.SystemClock import android.os.SystemClock
import android.system.ErrnoException import android.system.ErrnoException
import android.system.Os import android.system.Os
import android.system.OsConstants import android.system.OsConstants
import android.util.Log
import androidx.annotation.MainThread import androidx.annotation.MainThread
import com.github.shadowsocks.Core
import org.amnezia.vpn.shadowsocks.core.Core
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import java.io.File import java.io.File
@ -60,33 +62,22 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
val exitChannel = Channel<Int>() val exitChannel = Channel<Int>()
try { try {
while (true) { while (true) {
thread(name = "stderr-$cmdName") { thread(name = "stderr-$cmdName") { streamLogger(process.errorStream) { Log.e(cmdName, it) } }
streamLogger(process.errorStream) {
// Crashlytics.log(Log.ERROR, cmdName, it)
}
}
thread(name = "stdout-$cmdName") { thread(name = "stdout-$cmdName") {
streamLogger(process.inputStream) { streamLogger(process.inputStream) { Log.i(cmdName, it) }
// Crashlytics.log(Log.VERBOSE, cmdName, it)
}
// this thread also acts as a daemon thread for waitFor // this thread also acts as a daemon thread for waitFor
runBlocking { exitChannel.send(process.waitFor()) } runBlocking { exitChannel.send(process.waitFor()) }
} }
val startTime = SystemClock.elapsedRealtime() val startTime = SystemClock.elapsedRealtime()
val exitCode = exitChannel.receive() val exitCode = exitChannel.receive()
running = false running = false
when { if (SystemClock.elapsedRealtime() - startTime < 1000) {
SystemClock.elapsedRealtime() - startTime < 1000 -> throw IOException("$cmdName exits too fast (exit code: $exitCode)") 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"))
} }
// Crashlytics.log(Log.DEBUG, TAG, "restart process: ${Commandline.toString(cmd)} (last exit code: $exitCode)")
start() start()
running = true
onRestartCallback?.invoke() onRestartCallback?.invoke()
} }
} catch (e: IOException) { } catch (e: IOException) {
// Crashlytics.log(Log.WARN, TAG, "error occurred. stop guard: " + Commandline.toString(cmd))
GlobalScope.launch(Dispatchers.Main) { onFatal(e) } GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
} finally { } finally {
if (running) withContext(NonCancellable) { if (running) withContext(NonCancellable) {
@ -114,7 +105,6 @@ class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : C
@MainThread @MainThread
fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) { fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) {
// Crashlytics.log(Log.DEBUG, TAG, "start process: " + Commandline.toString(cmd))
Guard(cmd).apply { Guard(cmd).apply {
start() // if start fails, IOException will be thrown directly start() // if start fails, IOException will be thrown directly
launch { looper(onRestartCallback) } 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 org.amnezia.vpn.shadowsocks.core.Core.app
import com.github.shadowsocks.net.LocalDnsServer import org.amnezia.vpn.shadowsocks.core.acl.Acl
import com.github.shadowsocks.net.Socks5Endpoint import org.amnezia.vpn.shadowsocks.core.net.LocalDnsServer
import com.github.shadowsocks.preference.DataStore 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 kotlinx.coroutines.CoroutineScope
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.URI import java.net.URI
import java.net.URISyntaxException
import java.util.* import java.util.*
import org.amnezia.vpn.R
object LocalDnsService { object LocalDnsService {
private val googleApisTester = private val googleApisTester =
"(^|\\.)googleapis(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?){1,2}\$".toRegex() "(^|\\.)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>() private val servers = WeakHashMap<Interface, LocalDnsServer>()
interface Interface : BaseService.Interface { interface Interface : BaseService.Interface {
override suspend fun startProcesses(hosts: HostsFile) { override suspend fun startProcesses() {
super.startProcesses(hosts) super.startProcesses()
val profile = data.proxy!!.profile val profile = data.proxy!!.profile
val dns = try { val dns = URI("dns://${profile.remoteDns}")
URI("dns://${profile.remoteDns}")
} catch (e: URISyntaxException) {
throw BaseService.ExpectedExceptionWrapper(e)
}
LocalDnsServer(this::resolver, LocalDnsServer(this::resolver,
Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port), Socks5Endpoint(dns.host, if (dns.port < 0) 53 else dns.port),
DataStore.proxyAddress, DataStore.proxyAddress).apply {
hosts).apply {
tcp = !profile.udpdns 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)) }.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) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> * * Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* * * *
* This program is free software: you can redistribute it and/or modify * * 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 * * 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.app.Service
import android.text.InputFilter import android.content.Intent
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.preference.EditTextPreference
object EditTextPreferenceModifiers { /**
object Monospace : EditTextPreference.OnBindEditTextListener { * Shadowsocks service at its minimum.
override fun onBindEditText(editText: EditText) { */
editText.typeface = Typeface.MONOSPACE 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 { override fun onBind(intent: Intent) = super.onBind(intent)
private val portLengthFilter = arrayOf(InputFilter.LengthFilter(5)) override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
super<BaseService.Interface>.onStartCommand(intent, flags, startId)
override fun onBindEditText(editText: EditText) { override fun onDestroy() {
editText.inputType = EditorInfo.TYPE_CLASS_NUMBER super.onDestroy()
editText.filters = portLengthFilter data.binder.close()
editText.setSingleLine()
editText.setSelection(editText.text.length)
}
} }
} }

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.net.LocalSocket
import android.os.SystemClock import android.os.SystemClock
import com.github.shadowsocks.aidl.TrafficStats import org.amnezia.vpn.shadowsocks.core.aidl.TrafficStats
import com.github.shadowsocks.database.ProfileManager import org.amnezia.vpn.shadowsocks.core.net.LocalSocketListener
import com.github.shadowsocks.net.LocalSocketListener
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.nio.ByteBuffer import java.nio.ByteBuffer
@ -55,7 +52,6 @@ class TrafficMonitor(statFile: File) {
var out = TrafficStats() var out = TrafficStats()
private var timestampLast = 0L private var timestampLast = 0L
private var dirty = false private var dirty = false
private var persisted: TrafficStats? = null
fun requestUpdate(): Pair<TrafficStats, Boolean> { fun requestUpdate(): Pair<TrafficStats, Boolean> {
val now = SystemClock.elapsedRealtime() val now = SystemClock.elapsedRealtime()
@ -83,26 +79,4 @@ class TrafficMonitor(statFile: File) {
} }
return Pair(out, updated) 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.app.Service
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.LocalSocket import android.net.LocalSocket
import android.net.LocalSocketAddress import android.net.LocalSocketAddress
import android.net.Network import android.net.Network
@ -29,14 +30,16 @@ import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.system.ErrnoException import android.system.ErrnoException
import android.system.Os import android.system.Os
import com.github.shadowsocks.Core import org.amnezia.vpn.shadowsocks.core.Core
import org.amnezia.vpn.R import org.amnezia.vpn.R
import com.github.shadowsocks.net.ConcurrentLocalSocketListener import org.amnezia.vpn.shadowsocks.core.VpnRequestActivity
import com.github.shadowsocks.net.DefaultNetworkListener import org.amnezia.vpn.shadowsocks.core.acl.Acl
import com.github.shadowsocks.net.HostsFile import org.amnezia.vpn.shadowsocks.core.net.ConcurrentLocalSocketListener
import com.github.shadowsocks.net.Subnet import org.amnezia.vpn.shadowsocks.core.net.DefaultNetworkListener
import com.github.shadowsocks.preference.DataStore import org.amnezia.vpn.shadowsocks.core.net.Subnet
import com.github.shadowsocks.utils.printLog 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.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -45,7 +48,6 @@ import java.io.File
import java.io.FileDescriptor import java.io.FileDescriptor
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
import java.util.* import java.util.*
import android.net.VpnService as BaseVpnService 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) override fun getLocalizedMessage() = getString(R.string.reboot_required)
} }
@ -99,15 +101,16 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private var conn: ParcelFileDescriptor? = null private var conn: ParcelFileDescriptor? = null
private var worker: ProtectWorker? = null private var worker: ProtectWorker? = null
private var active = false private var active = false
// metered = false. xinlake private var metered = false
private var underlyingNetwork: Network? = null private var underlyingNetwork: Network? = null
set(value) { set(value) {
field = value field = value
if (active) setUnderlyingNetworks(underlyingNetworks) if (active && Build.VERSION.SDK_INT >= 22) setUnderlyingNetworks(underlyingNetworks)
} }
private val underlyingNetworks private val underlyingNetworks
get() = // clearing underlyingNetworks makes Android 9 consider the network to be metered get() =
underlyingNetwork?.let { arrayOf(it) } // 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) { override fun onBind(intent: Intent) = when (intent.action) {
SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent) 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 { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (DataStore.serviceMode == Key.modeVpn) {
if (prepare(this) != null) { if (prepare(this) != null) {
// startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) startActivity(Intent(this, VpnRequestActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
} else { } else return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
return super<LocalDnsService.Interface>.onStartCommand(intent, flags, startId)
} }
stopRunner() stopRunner()
return Service.START_NOT_STICKY return Service.START_NOT_STICKY
} }
override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it } override suspend fun preInit() = DefaultNetworkListener.start(this) { underlyingNetwork = it }
override suspend fun resolver(host: String) = override suspend fun resolver(host: String) = DefaultNetworkListener.get().getAllByName(host)
DnsResolverCompat.resolve(DefaultNetworkListener.get(), host)
override suspend fun openConnection(url: URL) = DefaultNetworkListener.get().openConnection(url) override suspend fun openConnection(url: URL) = DefaultNetworkListener.get().openConnection(url)
override suspend fun startProcesses(hosts: HostsFile) { override suspend fun startProcesses() {
worker = ProtectWorker().apply { start() } worker = ProtectWorker().apply { start() }
super.startProcesses(hosts) super.startProcesses()
sendFd(startVpn()) sendFd(startVpn())
} }
@ -167,18 +167,39 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126) builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
builder.addRoute("::", 0) 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 { resources.getStringArray(R.array.bypass_private_route).forEach {
val subnet = Subnet.fromString(it)!! val subnet = Subnet.fromString(it)!!
builder.addRoute(subnet.address.hostAddress, subnet.prefixSize) builder.addRoute(subnet.address.hostAddress, subnet.prefixSize)
} }
builder.addRoute(PRIVATE_VLAN4_ROUTER, 32) 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() val conn = builder.establish() ?: throw NullConnectionException()
this.conn = conn this.conn = conn
@ -199,6 +220,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
try { try {
sendFd(conn.fileDescriptor) sendFd(conn.fileDescriptor)
} catch (e: ErrnoException) { } catch (e: ErrnoException) {
e.printStackTrace()
stopRunner(false, e.message) 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 androidx.room.*
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@ -62,8 +62,7 @@ class KeyValuePair() {
@Deprecated("Use long.", ReplaceWith("long")) @Deprecated("Use long.", ReplaceWith("long"))
val int: Int? val int: Int?
get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null
val long: Long? val long: Long? get() = when (valueType) {
get() = when (valueType) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
TYPE_INT -> ByteBuffer.wrap(value).int.toLong() TYPE_INT -> ByteBuffer.wrap(value).int.toLong()
TYPE_LONG -> ByteBuffer.wrap(value).long 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() this.value = ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte()).array()
return this return this
} }
fun put(value: Float): KeyValuePair { fun put(value: Float): KeyValuePair {
valueType = TYPE_FLOAT valueType = TYPE_FLOAT
this.value = ByteBuffer.allocate(4).putFloat(value).array() this.value = ByteBuffer.allocate(4).putFloat(value).array()
return this return this
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@Deprecated("Use long.") @Deprecated("Use long.")
fun put(value: Int): KeyValuePair { fun put(value: Int): KeyValuePair {
@ -108,26 +105,21 @@ class KeyValuePair() {
this.value = ByteBuffer.allocate(4).putInt(value).array() this.value = ByteBuffer.allocate(4).putInt(value).array()
return this return this
} }
fun put(value: Long): KeyValuePair { fun put(value: Long): KeyValuePair {
valueType = TYPE_LONG valueType = TYPE_LONG
this.value = ByteBuffer.allocate(8).putLong(value).array() this.value = ByteBuffer.allocate(8).putLong(value).array()
return this return this
} }
fun put(value: String): KeyValuePair { fun put(value: String): KeyValuePair {
valueType = TYPE_STRING valueType = TYPE_STRING
this.value = value.toByteArray() this.value = value.toByteArray()
return this return this
} }
fun put(value: Set<String>): KeyValuePair { fun put(value: Set<String>): KeyValuePair {
valueType = TYPE_STRING_SET valueType = TYPE_STRING_SET
val stream = ByteArrayOutputStream() val stream = ByteArrayOutputStream()
val intBuffer = ByteBuffer.allocate(4)
for (v in value) { for (v in value) {
intBuffer.rewind() stream.write(ByteBuffer.allocate(4).putInt(v.length).array())
stream.write(intBuffer.putInt(v.length).array())
stream.write(v.toByteArray()) stream.write(v.toByteArray())
} }
this.value = stream.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.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.github.shadowsocks.Core.app import org.amnezia.vpn.shadowsocks.core.Core.app
import com.github.shadowsocks.database.migration.RecreateSchemaMigration import org.amnezia.vpn.shadowsocks.core.database.migration.RecreateSchemaMigration
import com.github.shadowsocks.utils.Key import org.amnezia.vpn.shadowsocks.core.utils.Key
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@Database(entities = [Profile::class, KeyValuePair::class], version = 1000) @Database(entities = [Profile::class, KeyValuePair::class], version = 29)
abstract class PrivateDatabase : RoomDatabase() { abstract class PrivateDatabase : RoomDatabase() {
companion object { companion object {
private val instance by lazy { private val instance by lazy {
Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE).apply { Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE)
addMigrations(Migration1000) .addMigrations(
allowMainThreadQueries() Migration26,
enableMultiInstanceInvalidation() Migration27,
fallbackToDestructiveMigration() Migration28
setQueryExecutor { GlobalScope.launch { it.run() } } )
}.build() .fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
} }
val profileDao get() = instance.profileDao() val profileDao get() = instance.profileDao()
val kvPairDao get() = instance.keyValuePairDao() val kvPairDao get() = instance.keyValuePairDao()
} }
abstract fun profileDao(): Profile.Dao abstract fun profileDao(): Profile.Dao
abstract fun keyValuePairDao(): KeyValuePair.Dao abstract fun keyValuePairDao(): KeyValuePair.Dao
object Migration1000 : RecreateSchemaMigration(999, object Migration26 : RecreateSchemaMigration(25, 26, "Profile",
1000, "(`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)",
"Profile", "`id`, `name`, `host`, `remotePort`, `password`, `method`, `route`, `remoteDns`, `proxyApps`, `bypass`, `udpdns`, `ipv6`, `individual`, `tx`, `rx`, `userOrder`, `plugin`") {
"(`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`") {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
super.migrate(database) super.migrate(database)
PublicDatabase.Migration3.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.database.sqlite.SQLiteCantOpenDatabaseException
import android.util.LongSparseArray import android.util.LongSparseArray
import com.github.shadowsocks.Core import org.amnezia.vpn.shadowsocks.core.Core
import com.github.shadowsocks.preference.DataStore import org.amnezia.vpn.shadowsocks.core.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
import com.github.shadowsocks.utils.forEachTry import org.amnezia.vpn.shadowsocks.core.utils.printLog
import com.github.shadowsocks.utils.printLog
import com.google.gson.JsonStreamParser
import org.json.JSONArray import org.json.JSONArray
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -43,7 +41,6 @@ object ProfileManager {
fun onRemove(profileId: Long) fun onRemove(profileId: Long)
fun onCleared() fun onCleared()
} }
var listener: Listener? = null var listener: Listener? = null
@Throws(SQLException::class) @Throws(SQLException::class)
@ -61,8 +58,9 @@ object ProfileManager {
profiles?.values?.singleOrNull { it.id == DataStore.profileId } profiles?.values?.singleOrNull { it.id == DataStore.profileId }
} else Core.currentProfile?.first } else Core.currentProfile?.first
val lazyClear = lazy { clear() } val lazyClear = lazy { clear() }
jsons.asIterable().forEachTry { json -> var result: Exception? = null
Profile.parseJson(JsonStreamParser(json.bufferedReader()).asSequence().single(), feature) { for (json in jsons) try {
Profile.parseJson(json.bufferedReader().readText(), feature) {
if (replace) { if (replace) {
lazyClear.value lazyClear.value
// if two profiles has the same address, treat them as the same profile and copy stats over // 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) 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? { fun serializeToJson(profiles: List<Profile>? = getAllProfiles()): JSONArray? {
if (profiles == null) return null if (profiles == null) return null
val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } } val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } }
@ -99,7 +99,7 @@ object ProfileManager {
} }
@Throws(IOException::class) @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) @Throws(SQLException::class)
fun delProfile(id: Long) { 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.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import com.github.shadowsocks.Core import org.amnezia.vpn.shadowsocks.core.Core
import com.github.shadowsocks.database.migration.RecreateSchemaMigration import org.amnezia.vpn.shadowsocks.core.database.migration.RecreateSchemaMigration
import com.github.shadowsocks.utils.Key import org.amnezia.vpn.shadowsocks.core.utils.Key
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@Database(entities = [KeyValuePair::class], version = 3) @Database(entities = [KeyValuePair::class], version = 4)
abstract class PublicDatabase : RoomDatabase() { abstract class PublicDatabase : RoomDatabase() {
companion object { companion object {
private val instance by lazy { private val instance by lazy {
Room.databaseBuilder(Core.deviceStorage, PublicDatabase::class.java, Key.DB_PUBLIC) Room.databaseBuilder(Core.deviceStorage, PublicDatabase::class.java, Key.DB_PUBLIC)
.apply { .allowMainThreadQueries()
addMigrations(Migration3) .addMigrations(
allowMainThreadQueries() Migration3
enableMultiInstanceInvalidation() )
fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
setQueryExecutor { GlobalScope.launch { it.run() } } .build()
}.build()
} }
val kvPairDao get() = instance.keyValuePairDao() val kvPairDao get() = instance.keyValuePairDao()
} }
abstract fun keyValuePairDao(): KeyValuePair.Dao abstract fun keyValuePairDao(): KeyValuePair.Dao
internal object Migration3 : RecreateSchemaMigration(2, internal object Migration3 : RecreateSchemaMigration(2, 3, "KeyValuePair",
3,
"KeyValuePair",
"(`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))", "(`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
"`key`, `valueType`, `value`") "`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.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
open class RecreateSchemaMigration(oldVersion: Int, newVersion: Int, private val table: String, open class RecreateSchemaMigration(oldVersion: Int, newVersion: Int, private val table: String,
private val schema: String, private val keys: String) private val schema: String, private val keys: String) :
: Migration(oldVersion, newVersion) { Migration(oldVersion, newVersion) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE `tmp` $schema") database.execSQL("CREATE TABLE `tmp` $schema")
database.execSQL("INSERT INTO `tmp` ($keys) SELECT $keys FROM `$table`") 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 android.os.Build
import com.github.shadowsocks.utils.printLog import org.amnezia.vpn.shadowsocks.core.utils.printLog
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.sendBlocking import kotlinx.coroutines.channels.sendBlocking
@ -86,13 +86,11 @@ class ChannelMonitor : Thread("ChannelMonitor") {
return registration.result.await() return registration.result.await()
} }
suspend fun wait(channel: SelectableChannel, ops: Int) = suspend fun wait(channel: SelectableChannel, ops: Int) = CompletableDeferred<SelectionKey>().run {
CompletableDeferred<SelectionKey>().run {
register(channel, ops) { register(channel, ops) {
if (it.isValid) try { if (it.isValid) try {
it.interestOps(0) // stop listening it.interestOps(0) // stop listening
} catch (_: CancelledKeyException) { } catch (_: CancelledKeyException) { }
}
complete(it) complete(it)
} }
await() await()

View file

@ -18,17 +18,16 @@
* * * *
*******************************************************************************/ *******************************************************************************/
package com.github.shadowsocks.net package org.amnezia.vpn.shadowsocks.core.net
import android.net.LocalSocket import android.net.LocalSocket
import com.github.shadowsocks.utils.printLog import org.amnezia.vpn.shadowsocks.core.utils.printLog
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : abstract class ConcurrentLocalSocketListener(name: String, socketFile: File) : LocalSocketListener(name, socketFile),
LocalSocketListener(name, socketFile), CoroutineScope { CoroutineScope {
override val coroutineContext = override val coroutineContext = Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
Dispatchers.IO + SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
override fun accept(socket: LocalSocket) { override fun accept(socket: LocalSocket) {
launch { super.accept(socket) } 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.annotation.TargetApi
import android.net.ConnectivityManager import android.net.ConnectivityManager
@ -26,12 +26,11 @@ import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.Build import android.os.Build
import com.github.shadowsocks.Core import androidx.core.content.getSystemService
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers import org.amnezia.vpn.shadowsocks.core.Core.app
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.*
import kotlinx.coroutines.channels.actor import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.runBlocking
import java.net.UnknownHostException import java.net.UnknownHostException
object DefaultNetworkListener { object DefaultNetworkListener {
@ -40,14 +39,13 @@ object DefaultNetworkListener {
class Get : NetworkMessage() { class Get : NetworkMessage() {
val response = CompletableDeferred<Network>() val response = CompletableDeferred<Network>()
} }
class Stop(val key: Any) : NetworkMessage() class Stop(val key: Any) : NetworkMessage()
class Put(val network: Network) : NetworkMessage() class Put(val network: Network) : NetworkMessage()
class Update(val network: Network) : NetworkMessage() class Update(val network: Network) : NetworkMessage()
class Lost(val network: Network) : NetworkMessage() class Lost(val network: Network) : NetworkMessage()
} }
@ObsoleteCoroutinesApi
private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) { private val networkActor = GlobalScope.actor<NetworkMessage>(Dispatchers.Unconfined) {
val listeners = mutableMapOf<Any, (Network?) -> Unit>() val listeners = mutableMapOf<Any, (Network?) -> Unit>()
var network: Network? = null var network: Network? = null
@ -74,9 +72,7 @@ object DefaultNetworkListener {
pendingRequests.clear() pendingRequests.clear()
listeners.values.forEach { it(network) } listeners.values.forEach { it(network) }
} }
is NetworkMessage.Update -> if (network == message.network) listeners.values.forEach { is NetworkMessage.Update -> if (network == message.network) listeners.values.forEach { it(network) }
it(network)
}
is NetworkMessage.Lost -> if (network == message.network) { is NetworkMessage.Lost -> if (network == message.network) {
network = null network = null
listeners.values.forEach { it(null) } listeners.values.forEach { it(null) }
@ -84,39 +80,55 @@ object DefaultNetworkListener {
} }
} }
suspend fun start(key: Any, listener: (Network?) -> Unit) = suspend fun start(key: Any, listener: (Network?) -> Unit) = networkActor.send(DefaultNetworkListener.NetworkMessage.Start(key, listener))
networkActor.send(NetworkMessage.Start(key, listener))
suspend fun get() = if (fallback) @TargetApi(23) { suspend fun get() = if (fallback) @TargetApi(23) {
Core.connectivity.activeNetwork connectivity.activeNetwork ?: throw UnknownHostException() // failed to listen, return current if available
?: throw UnknownHostException() // failed to listen, return current if available } else DefaultNetworkListener.NetworkMessage.Get().run {
} else NetworkMessage.Get().run {
networkActor.send(this) networkActor.send(this)
response.await() 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 // NB: this runs in ConnectivityThread, and this behavior cannot be changed until API 26
// private object Callback : ConnectivityManager.NetworkCallback() { // private object Callback : ConnectivityManager.NetworkCallback() {
// override fun onAvailable(network: Network) = // override fun onAvailable(network: Network) = runBlocking { networkActor.send(DefaultNetworkListener.NetworkMessage.Put(network)) }
// runBlocking { networkActor.send(NetworkMessage.Put(network)) }
//
// override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) { // override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities?) {
// // it's a good idea to refresh capabilities // // 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(DefaultNetworkListener.NetworkMessage.Lost(network)) }
// override fun onLost(network: Network) =
// runBlocking { networkActor.send(NetworkMessage.Lost(network)) }
// } // }
private var fallback = false private var fallback = false
private val connectivity = app.getSystemService<ConnectivityManager>()!!
private val request = NetworkRequest.Builder().apply { private val request = NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
}.build() }.build()
/** /**
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1: * Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e * 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 * Source: https://android.googlesource.com/platform/frameworks/base/+/2df4c7d/services/core/java/com/android/server/ConnectivityService.java#887
*/ */
private fun register() { private fun register() {
// if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) { if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
// Core.connectivity.registerDefaultNetworkCallback(Callback) connectivity.registerDefaultNetworkCallback(Callback)
// } else try { } else try {
// fallback = false fallback = false
// // we want REQUEST here instead of LISTEN // we want REQUEST here instead of LISTEN
// Core.connectivity.requestNetwork(request, Callback) connectivity.requestNetwork(request, Callback)
// } catch (e: SecurityException) { } catch (e: SecurityException) {
// // known bug: https://stackoverflow.com/a/33509180/2245107 fallback = true
// // if (Build.VERSION.SDK_INT != 23) Crashlytics.logException(e)
// 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.Build
import android.os.SystemClock import android.os.SystemClock
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel 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 org.amnezia.vpn.R
import com.github.shadowsocks.utils.useCancellable import org.amnezia.vpn.shadowsocks.core.preference.DataStore
import kotlinx.coroutines.Dispatchers import org.amnezia.vpn.shadowsocks.core.utils.Key
import kotlinx.coroutines.GlobalScope import org.amnezia.vpn.shadowsocks.core.utils.disconnectFromMain
import kotlinx.coroutines.Job import kotlinx.coroutines.*
import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.Proxy
import java.net.URL import java.net.URL
import java.net.URLConnection import java.net.URLConnection
@ -42,21 +44,17 @@ import java.net.URLConnection
class HttpsTest : ViewModel() { class HttpsTest : ViewModel() {
sealed class Status { sealed class Status {
protected abstract val status: CharSequence protected abstract val status: CharSequence
open fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) = open fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) = setStatus(status)
setStatus(status)
object Idle : Status() { object Idle : Status() {
override val status get() = app.getText(R.string.vpn_connected) override val status get() = app.getText(R.string.vpn_connected)
} }
object Testing : Status() { object Testing : Status() {
override val status get() = app.getText(R.string.connection_test_testing) override val status get() = app.getText(R.string.connection_test_testing)
} }
class Success(private val elapsed: Long) : Status() { class Success(private val elapsed: Long) : Status() {
override val status get() = app.getString(R.string.connection_test_available, elapsed) override val status get() = app.getString(R.string.connection_test_available, elapsed)
} }
sealed class Error : Status() { sealed class Error : Status() {
override val status get() = app.getText(R.string.connection_test_fail) override val status get() = app.getText(R.string.connection_test_fail)
protected abstract val error: String protected abstract val error: String
@ -71,43 +69,48 @@ class HttpsTest : ViewModel() {
class UnexpectedResponseCode(private val code: Int) : Error() { class UnexpectedResponseCode(private val code: Int) : Error() {
override val error get() = app.getString(R.string.connection_test_error_status_code, code) override val error get() = app.getString(R.string.connection_test_error_status_code, code)
} }
class IOFailure(private val e: IOException) : Error() { class IOFailure(private val e: IOException) : Error() {
override val error get() = app.getString(R.string.connection_test_error, e.message) 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 } val status = MutableLiveData<Status>().apply { value = Status.Idle }
fun testConnection() { fun testConnection() {
cancelTest() cancelTest()
status.value = Status.Testing status.value = Status.Testing
val url = URL("https", "www.google.com", "/generate_204") val url = URL("https", when ((Core.currentProfile ?: return).first.route) {
val conn = (url.openConnection()) as HttpURLConnection 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.setRequestProperty("Connection", "close")
conn.instanceFollowRedirects = false conn.instanceFollowRedirects = false
conn.useCaches = false conn.useCaches = false
running = GlobalScope.launch(Dispatchers.Main.immediate) { running = conn to GlobalScope.launch(Dispatchers.Main.immediate) {
status.value = conn.useCancellable { status.value = withContext(Dispatchers.IO) {
try { try {
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
val code = responseCode val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start 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) else Status.Error.UnexpectedResponseCode(code)
} catch (e: IOException) { } catch (e: IOException) {
Status.Error.IOFailure(e) Status.Error.IOFailure(e)
} finally { } finally {
disconnect() conn.disconnect()
} }
} }
} }
} }
private fun cancelTest() { private fun cancelTest() = running?.let { (conn, job) ->
running?.cancel() job.cancel() // ensure job is cancelled before interrupting
conn.disconnectFromMain()
running = null 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 org.amnezia.vpn.shadowsocks.core.utils.printLog
import com.github.shadowsocks.utils.printLog
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.xbill.DNS.* import org.xbill.DNS.*
import java.io.IOException import java.io.IOException
@ -30,6 +29,7 @@ import java.nio.ByteBuffer
import java.nio.channels.DatagramChannel import java.nio.channels.DatagramChannel
import java.nio.channels.SelectionKey import java.nio.channels.SelectionKey
import java.nio.channels.SocketChannel import java.nio.channels.SocketChannel
import org.amnezia.vpn.R
/** /**
* A simple DNS conditional forwarder. * A simple DNS conditional forwarder.
@ -41,9 +41,7 @@ import java.nio.channels.SocketChannel
* https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04 * https://github.com/shadowsocks/overture/tree/874f22613c334a3b78e40155a55479b7b69fee04
*/ */
class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>, class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAddress>,
private val remoteDns: Socks5Endpoint, private val remoteDns: Socks5Endpoint, private val proxy: SocketAddress) : CoroutineScope {
private val proxy: SocketAddress,
private val hosts: HostsFile) : CoroutineScope {
/** /**
* Forward all requests to remote and ignore localResolver. * 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()) if (request.header.getFlag(Flags.RD.toInt())) header.setFlag(Flags.RD.toInt())
request.question?.also { addRecord(it, Section.QUESTION) } 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() private val monitor = ChannelMonitor()
override val coroutineContext = override val coroutineContext = SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
SupervisorJob() + CoroutineExceptionHandler { _, t -> printLog(t) }
suspend fun start(listen: SocketAddress) = DatagramChannel.open().run { suspend fun start(listen: SocketAddress) = DatagramChannel.open().run {
configureBlocking(false) configureBlocking(false)
try {
socket().bind(listen) socket().bind(listen)
} catch (e: BindException) {
throw BaseService.ExpectedExceptionWrapper(e)
}
monitor.register(this, SelectionKey.OP_READ) { handlePacket(this) } monitor.register(this, SelectionKey.OP_READ) { handlePacket(this) }
} }
@ -111,27 +93,20 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
val request = try { val request = try {
Message(packet) Message(packet)
} catch (e: IOException) { // we cannot parse the message, do not attempt to handle it at all } 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 forward(packet)
} }
return supervisorScope { return supervisorScope {
val remote = async { withTimeout(TIMEOUT) { forward(packet) } } val remote = async { withTimeout(TIMEOUT) { forward(packet) } }
try { 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 val question = request.question
if (question?.type != Type.A) return@supervisorScope remote.await() if (question?.type != Type.A) return@supervisorScope remote.await()
val host = question.name.toString(true) 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() if (remoteDomainMatcher?.containsMatchIn(host) == true) return@supervisorScope remote.await()
val localResults = try { val localResults = try {
withTimeout(TIMEOUT) { localResolver(host) } withTimeout(TIMEOUT) { GlobalScope.async(Dispatchers.IO) { localResolver(host) }.await() }
} catch (_: TimeoutCancellationException) { } catch (_: TimeoutCancellationException) {
// Crashlytics.log(Log.WARN, TAG, "Local resolving timed out, falling back to remote resolving")
return@supervisorScope remote.await() return@supervisorScope remote.await()
} catch (_: UnknownHostException) { } catch (_: UnknownHostException) {
return@supervisorScope remote.await() 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 (localResults.isEmpty()) return@supervisorScope remote.await()
if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) { if (localIpMatcher.isEmpty() || localIpMatcher.any { subnet -> localResults.any(subnet::matches) }) {
remote.cancel() 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() } else remote.await()
} catch (e: Exception) { } catch (e: Exception) {
remote.cancel() remote.cancel()
when (e) { when (e) {
// is TimeoutCancellationException -> Crashlytics.log(Log.WARN, TAG, "Remote resolving timed out") is CancellationException -> { } // ignore
is CancellationException -> {
} // ignore
// is IOException -> Crashlytics.log(Log.WARN, TAG, e.message)
else -> printLog(e) else -> printLog(e)
} }
ByteBuffer.wrap(prepareDnsResponse(request).apply { 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 { private suspend fun forward(packet: ByteBuffer): ByteBuffer {
packet.position(0) // the packet might have been parsed, reset to beginning packet.position(0) // the packet might have been parsed, reset to beginning
return if (tcp) SocketChannel.open().use { channel -> 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.finishConnect()) monitor.wait(channel, SelectionKey.OP_CONNECT)
while (channel.write(wrapped) >= 0 && wrapped.hasRemaining()) monitor.wait(channel, SelectionKey.OP_WRITE) while (channel.write(wrapped) >= 0 && wrapped.hasRemaining()) monitor.wait(channel, SelectionKey.OP_WRITE)
val result = remoteDns.tcpReceiveBuffer(UDP_PACKET_SIZE) val result = remoteDns.tcpReceiveBuffer(UDP_PACKET_SIZE)
remoteDns.tcpUnwrap(result, channel::read) { remoteDns.tcpUnwrap(result, channel::read) { monitor.wait(channel, SelectionKey.OP_READ) }
monitor.wait(channel, SelectionKey.OP_READ)
}
result result
} else DatagramChannel.open().use { channel -> } else DatagramChannel.open().use { channel ->
channel.configureBlocking(false) 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.LocalServerSocket
import android.net.LocalSocket import android.net.LocalSocket
@ -26,7 +26,7 @@ import android.net.LocalSocketAddress
import android.system.ErrnoException import android.system.ErrnoException
import android.system.Os import android.system.Os
import android.system.OsConstants 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.CoroutineScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.sendBlocking 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. * 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 open fun accept(socket: LocalSocket) = socket.use { acceptInternal(socket) }
protected abstract fun acceptInternal(socket: LocalSocket) protected abstract fun acceptInternal(socket: LocalSocket)
final override fun run() { final override fun run() {
localSocket.use { 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.Socks4Message
import net.sourceforge.jsocks.Socks5Message import net.sourceforge.jsocks.Socks5Message
import java.io.EOFException import java.io.EOFException
@ -32,13 +32,12 @@ import kotlin.math.max
class Socks5Endpoint(host: String, port: Int) { class Socks5Endpoint(host: String, port: Int) {
private val dest = host.parseNumericAddress().let { numeric -> private val dest = host.parseNumericAddress().let { numeric ->
val bytes = numeric?.address val bytes = numeric?.address ?: host.toByteArray().apply { check(size < 256) { "Hostname too long" } }
?: host.toByteArray().apply { check(size < 256) { "Hostname too long" } }
val type = when (numeric) { val type = when (numeric) {
null -> Socks5Message.SOCKS_ATYP_DOMAINNAME null -> Socks5Message.SOCKS_ATYP_DOMAINNAME
is Inet4Address -> Socks5Message.SOCKS_ATYP_IPV4 is Inet4Address -> Socks5Message.SOCKS_ATYP_IPV4
is Inet6Address -> Socks5Message.SOCKS_ATYP_IPV6 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 { ByteBuffer.allocate(bytes.size + (if (numeric == null) 1 else 0) + 3).apply {
put(type.toByte()) put(type.toByte())
@ -66,35 +65,34 @@ class Socks5Endpoint(host: String, port: Int) {
flip() flip()
} }
} }
fun tcpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + 4 + size) fun tcpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + 4 + size)
@ExperimentalUnsignedTypes
suspend fun tcpUnwrap(buffer: ByteBuffer, reader: (ByteBuffer) -> Int, wait: suspend () -> Unit) { suspend fun tcpUnwrap(buffer: ByteBuffer, reader: (ByteBuffer) -> Int, wait: suspend () -> Unit) {
suspend fun readBytes(till: Int) { suspend fun readBytes(till: Int) {
if (buffer.position() >= till) return if (buffer.position() >= till) return
while (reader(buffer) >= 0 && buffer.position() < till) wait() while (reader(buffer) >= 0 && buffer.position() < till) wait()
if (buffer.position() < till) throw EOFException("${buffer.position()} < $till") if (buffer.position() < till) throw EOFException("${buffer.position()} < $till")
} }
suspend fun read(index: Int): Byte { suspend fun read(index: Int): Byte {
readBytes(index + 1) readBytes(index + 1)
return buffer[index] 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(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]}") 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_IPV4.toByte() -> 4
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + read(6) Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + read(6)
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16 Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
else -> throw IOException("Unsupported address type $type") else -> throw IllegalStateException("Unsupported address type ${buffer[5]}")
} + 8 } + 8
readBytes(dataOffset + 2) readBytes(dataOffset + 2)
buffer.limit(buffer.position()) // store old position to update mark buffer.limit(buffer.position()) // store old position to update mark
buffer.position(dataOffset) buffer.position(dataOffset)
val dataLength = buffer.short.toUShort().toInt() val dataLength = buffer.short.toUShort().toInt()
val end = buffer.position() + dataLength 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.mark()
buffer.position(buffer.limit()) // restore old position buffer.position(buffer.limit()) // restore old position
buffer.limit(end) buffer.limit(end)
@ -102,13 +100,7 @@ class Socks5Endpoint(host: String, port: Int) {
buffer.reset() buffer.reset()
} }
private fun ByteBuffer.tryPosition(newPosition: Int) { fun udpWrap(packet: ByteBuffer) = ByteBuffer.allocateDirect(3 + dest.size + packet.remaining()).apply {
if (limit() < newPosition) throw EOFException("${limit()} < $newPosition")
position(newPosition)
}
fun udpWrap(packet: ByteBuffer) =
ByteBuffer.allocateDirect(3 + dest.size + packet.remaining()).apply {
// header // header
putShort(0) // reserved putShort(0) // reserved
put(0) // fragment number put(0) // fragment number
@ -117,15 +109,14 @@ class Socks5Endpoint(host: String, port: Int) {
put(packet) put(packet)
flip() flip()
} }
fun udpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + size) fun udpReceiveBuffer(size: Int) = ByteBuffer.allocateDirect(headerReserved + size)
fun udpUnwrap(packet: ByteBuffer) { fun udpUnwrap(packet: ByteBuffer) {
packet.tryPosition(3) packet.position(3)
packet.tryPosition(6 + when (val type = packet.get()) { packet.position(6 + when (packet.get()) {
Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4 Socks5Message.SOCKS_ATYP_IPV4.toByte() -> 4
Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + packet.get() Socks5Message.SOCKS_ATYP_DOMAINNAME.toByte() -> 1 + packet.get()
Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16 Socks5Message.SOCKS_ATYP_IPV6.toByte() -> 16
else -> throw IOException("Unsupported address type $type") else -> throw IllegalStateException("Unsupported address type")
}) })
packet.mark() 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.net.InetAddress
import java.util.* 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 private val addressLength get() = address.address.size shl 3
init { 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 { fun matches(other: InetAddress): Boolean {
@ -80,6 +80,5 @@ class Subnet(val address: InetAddress, val prefixSize: Int) : Comparable<Subnet>
val that = other as? Subnet val that = other as? Subnet
return address == that?.address && prefixSize == that.prefixSize return address == that?.address && prefixSize == that.prefixSize
} }
override fun hashCode(): Int = Objects.hash(address, 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.runBlocking
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import java.io.File import java.io.File
@ -34,8 +34,7 @@ object TcpFastOpen {
*/ */
val supported by lazy { val supported by lazy {
if (File(PATH).canRead()) return@lazy true if (File(PATH).canRead()) return@lazy true
val match = val match = """^(\d+)\.(\d+)\.(\d+)""".toRegex().find(System.getProperty("os.version") ?: "")
"""^(\d+)\.(\d+)\.(\d+)""".toRegex().find(System.getProperty("os.version") ?: "")
if (match == null) false else when (match.groupValues[1].toInt()) { if (match == null) false else when (match.groupValues[1].toInt()) {
in Int.MIN_VALUE..2 -> false in Int.MIN_VALUE..2 -> false
3 -> when (match.groupValues[2].toInt()) { 3 -> when (match.groupValues[2].toInt()) {
@ -47,8 +46,7 @@ object TcpFastOpen {
} }
} }
val sendEnabled: Boolean val sendEnabled: Boolean get() {
get() {
val file = File(PATH) val file = File(PATH)
// File.readText doesn't work since this special file will return length 0 // 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 // 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 e.readableMessage
} }
} }
fun enableTimeout() = runBlocking { withTimeoutOrNull(1000) { enable() } } 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) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be> * * Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* * * *
* This program is free software: you can redistribute it and/or modify * * 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 * * 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 android.content.pm.ResolveInfo
import com.github.shadowsocks.utils.parseNumericAddress import android.os.Bundle
import java.net.InetAddress
class HostsFile(input: String = "") {
private val map = mutableMapOf<String, MutableSet<InetAddress>>()
class NativePlugin(resolveInfo: ResolveInfo) : ResolvedPlugin(resolveInfo) {
init { init {
for (line in input.lineSequence()) { check(resolveInfo.providerInfo != null)
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)
}
} }
val configuredHostnames get() = map.size override val metaData: Bundle get() = resolveInfo.providerInfo.metaData
fun resolve(hostname: String) = map[hostname]?.shuffled() ?: emptyList() 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 android.os.Binder
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
import com.github.shadowsocks.Core import org.amnezia.vpn.shadowsocks.core.Core
import com.github.shadowsocks.database.PrivateDatabase import org.amnezia.vpn.shadowsocks.core.database.PrivateDatabase
import com.github.shadowsocks.database.PublicDatabase import org.amnezia.vpn.shadowsocks.core.database.PublicDatabase
import com.github.shadowsocks.net.TcpFastOpen import org.amnezia.vpn.shadowsocks.core.net.TcpFastOpen
import com.github.shadowsocks.utils.DirectBoot import org.amnezia.vpn.shadowsocks.core.utils.DirectBoot
import com.github.shadowsocks.utils.Key import org.amnezia.vpn.shadowsocks.core.utils.Key
import com.github.shadowsocks.utils.parsePort import org.amnezia.vpn.shadowsocks.core.utils.parsePort
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.net.SocketException
object DataStore : OnPreferenceDataStoreChangeListener { object DataStore : OnPreferenceDataStoreChangeListener {
val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao) val publicStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
@ -40,7 +42,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
publicStore.registerChangeListener(this) publicStore.registerChangeListener(this)
} }
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) { override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String?) {
when (key) { when (key) {
Key.id -> if (directBootAware) DirectBoot.update() 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 // hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() } private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
private fun getLocalPort(key: String, default: Int): Int { private fun getLocalPort(key: String, default: Int): Int {
val value = publicStore.getInt(key) val value = publicStore.getInt(key)
return if (value != null) { return if (value != null) {
@ -62,8 +63,29 @@ object DataStore : OnPreferenceDataStoreChangeListener {
set(value) = publicStore.putLong(Key.id, value) set(value) = publicStore.putLong(Key.id, value)
val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true val canToggleLocked: Boolean get() = publicStore.getBoolean(Key.directBootAware) == true
val directBootAware: Boolean get() = Core.directBootSupported && canToggleLocked val directBootAware: Boolean get() = Core.directBootSupported && canToggleLocked
val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && publicStore.getBoolean(Key.tfo, false) val tcpFastOpen: Boolean get() = TcpFastOpen.sendEnabled && publicStore.getBoolean(Key.tfo, true)
val listenAddress get() = "127.0.0.1" 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 var portProxy: Int
get() = getLocalPort(Key.portProxy, 1080) get() = getLocalPort(Key.portProxy, 1080)
set(value) = publicStore.putString(Key.portProxy, value.toString()) set(value) = publicStore.putString(Key.portProxy, value.toString())
@ -71,6 +93,9 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var portLocalDns: Int var portLocalDns: Int
get() = getLocalPort(Key.portLocalDns, 5450) get() = getLocalPort(Key.portLocalDns, 5450)
set(value) = publicStore.putString(Key.portLocalDns, value.toString()) 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. * 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.getBoolean(Key.tfo) == null) publicStore.putBoolean(Key.tfo, tcpFastOpen)
if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy if (publicStore.getString(Key.portProxy) == null) portProxy = portProxy
if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns if (publicStore.getString(Key.portLocalDns) == null) portLocalDns = portLocalDns
if (publicStore.getString(Key.portTransproxy) == null) portTransproxy = portTransproxy
} }
var editingId: Long? var editingId: Long?
get() = privateStore.getLong(Key.id) get() = privateStore.getLong(Key.id)
set(value) = privateStore.putLong(Key.id, value) 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 var dirty: Boolean
get() = privateStore.getBoolean(Key.dirty) ?: false get() = privateStore.getBoolean(Key.dirty) ?: false
set(value) = privateStore.putBoolean(Key.dirty, value) 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 import androidx.preference.PreferenceDataStore
interface OnPreferenceDataStoreChangeListener { 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 androidx.preference.PreferenceDataStore
import com.github.shadowsocks.database.KeyValuePair import org.amnezia.vpn.shadowsocks.core.database.KeyValuePair
import java.util.* import java.util.HashSet
@Suppress("MemberVisibilityCanBePrivate", "unused") @Suppress("MemberVisibilityCanBePrivate", "unused")
open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) : open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) : PreferenceDataStore() {
PreferenceDataStore() {
fun getBoolean(key: String) = kvPairDao[key]?.boolean fun getBoolean(key: String) = kvPairDao[key]?.boolean
fun getFloat(key: String) = kvPairDao[key]?.float fun getFloat(key: String) = kvPairDao[key]?.float
fun getInt(key: String) = kvPairDao[key]?.long?.toInt() 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 getInt(key: String, defValue: Int) = getInt(key) ?: defValue
override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue
override fun getString(key: String, defValue: String?) = getString(key) ?: defValue override fun getString(key: String, defValue: String?) = getString(key) ?: defValue
override fun getStringSet(key: String, defValue: MutableSet<String>?) = override fun getStringSet(key: String, defValue: MutableSet<String>?) = getStringSet(key) ?: defValue
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 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) fun putLong(key: String, value: Long?) = if (value == null) remove(key) else putLong(key, value)
override fun putBoolean(key: String, value: Boolean) { override fun putBoolean(key: String, value: Boolean) {
kvPairDao.put(KeyValuePair(key).put(value)) kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putFloat(key: String, value: Float) { override fun putFloat(key: String, value: Float) {
kvPairDao.put(KeyValuePair(key).put(value)) kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putInt(key: String, value: Int) { override fun putInt(key: String, value: Int) {
kvPairDao.put(KeyValuePair(key).put(value.toLong())) kvPairDao.put(KeyValuePair(key).put(value.toLong()))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putLong(key: String, value: Long) { override fun putLong(key: String, value: Long) {
kvPairDao.put(KeyValuePair(key).put(value)) kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key) fireChangeListener(key)
} }
override fun putString(key: String, value: String?) = if (value == null) remove(key) else { override fun putString(key: String, value: String?) = if (value == null) remove(key) else {
kvPairDao.put(KeyValuePair(key).put(value)) kvPairDao.put(KeyValuePair(key).put(value))
fireChangeListener(key) 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)) kvPairDao.put(KeyValuePair(key).put(values))
fireChangeListener(key) fireChangeListener(key)
} }
@ -89,12 +75,7 @@ open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) :
} }
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>() private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
private fun fireChangeListener(key: String) = private fun fireChangeListener(key: String) = listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
listeners.forEach { it.onPreferenceDataStoreChanged(this, key) } fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.add(listener)
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) = listeners.remove(listener)
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 android.content.ClipData
import androidx.recyclerview.widget.SortedList import androidx.recyclerview.widget.SortedList
import org.json.JSONArray
private sealed class ArrayIterator<out T> : Iterator<T> { private sealed class ArrayIterator<out T> : Iterator<T> {
abstract val size: Int abstract val size: Int
@ -35,12 +36,16 @@ private class ClipDataIterator(private val data: ClipData) : ArrayIterator<ClipD
override val size get() = data.itemCount override val size get() = data.itemCount
override fun get(index: Int) = data.getItemAt(index) override fun get(index: Int) = data.getItemAt(index)
} }
fun ClipData.asIterable() = Iterable { ClipDataIterator(this) } 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>() { private class SortedListIterator<out T>(private val list: SortedList<T>) : ArrayIterator<T>() {
override val size get() = list.size() override val size get() = list.size()
override fun get(index: Int) = list[index] override fun get(index: Int) = list[index]
} }
fun <T> SortedList<T>.asIterable() = Iterable { SortedListIterator(this) } 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 { object Key {
/** /**
@ -30,13 +30,27 @@ object Key {
const val id = "profileId" const val id = "profileId"
const val name = "profileName" 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 portProxy = "portProxy"
const val portLocalDns = "portLocalDns" const val portLocalDns = "portLocalDns"
const val portTransproxy = "portTransproxy"
const val route = "route"
const val isAutoConnect = "isAutoConnect"
const val directBootAware = "directBootAware" const val directBootAware = "directBootAware"
const val proxyApps = "isProxyApps"
const val bypass = "isBypassApps"
const val udpdns = "isUdpDns" const val udpdns = "isUdpDns"
const val ipv6 = "isIpv6" const val ipv6 = "isIpv6"
const val metered = "metered"
const val host = "proxy" const val host = "proxy"
const val password = "sitekey" const val password = "sitekey"
@ -44,17 +58,26 @@ object Key {
const val remotePort = "remotePortNum" const val remotePort = "remotePortNum"
const val remoteDns = "remoteDns" const val remoteDns = "remoteDns"
const val plugin = "plugin"
const val pluginConfigure = "plugin.configure"
const val udpFallback = "udpFallback"
const val dirty = "profileDirty" const val dirty = "profileDirty"
const val tfo = "tcp_fastopen" const val tfo = "tcp_fastopen"
const val hosts = "hosts"
const val assetUpdateTime = "assetUpdateTime" 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 { object Action {
const val SERVICE = "com.github.shadowsocks.SERVICE" const val SERVICE = "com.kyle.shadowsocks.SERVICE"
const val CLOSE = "com.github.shadowsocks.CLOSE" const val CLOSE = "com.kyle.shadowsocks.CLOSE"
const val RELOAD = "com.github.shadowsocks.RELOAD" 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.SuppressLint
import android.annotation.TargetApi 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.annotation.TargetApi
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import com.github.shadowsocks.Core import org.amnezia.vpn.shadowsocks.core.Core
import com.github.shadowsocks.Core.app import org.amnezia.vpn.shadowsocks.core.Core.app
import com.github.shadowsocks.bg.BaseService import org.amnezia.vpn.shadowsocks.core.bg.BaseService
import com.github.shadowsocks.database.Profile import org.amnezia.vpn.shadowsocks.core.database.Profile
import com.github.shadowsocks.database.ProfileManager import org.amnezia.vpn.shadowsocks.core.database.ProfileManager
import com.github.shadowsocks.preference.DataStore import org.amnezia.vpn.shadowsocks.core.preference.DataStore
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.ObjectInputStream import java.io.ObjectInputStream
@ -23,9 +23,7 @@ object DirectBoot : BroadcastReceiver() {
fun getDeviceProfile(): Pair<Profile, Profile?>? = try { fun getDeviceProfile(): Pair<Profile, Profile?>? = try {
ObjectInputStream(file.inputStream()).use { it.readObject() as? Pair<Profile, Profile?> } ObjectInputStream(file.inputStream()).use { it.readObject() as? Pair<Profile, Profile?> }
} catch (_: IOException) { } catch (_: IOException) { null }
null
}
fun clean() { fun clean() {
file.delete() file.delete()
@ -38,9 +36,7 @@ object DirectBoot : BroadcastReceiver() {
*/ */
fun update(profile: Profile? = ProfileManager.getProfile(DataStore.profileId)) = fun update(profile: Profile? = ProfileManager.getProfile(DataStore.profileId)) =
if (profile == null) clean() if (profile == null) clean()
else ObjectOutputStream(file.outputStream()).use { else ObjectOutputStream(file.outputStream()).use { it.writeObject(ProfileManager.expand(profile)) }
it.writeObject(ProfileManager.expand(profile))
}
fun flushTrafficStats() { fun flushTrafficStats() {
getDeviceProfile()?.also { (profile, fallback) -> getDeviceProfile()?.also { (profile, fallback) ->
@ -55,7 +51,6 @@ object DirectBoot : BroadcastReceiver() {
app.registerReceiver(this, IntentFilter(Intent.ACTION_BOOT_COMPLETED)) app.registerReceiver(this, IntentFilter(Intent.ACTION_BOOT_COMPLETED))
registered = true registered = true
} }
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
flushTrafficStats() flushTrafficStats()
app.unregisterReceiver(this) 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.BroadcastReceiver
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
@ -36,64 +35,30 @@ import android.system.OsConstants
import android.util.TypedValue import android.util.TypedValue
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.preference.Preference import androidx.preference.Preference
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.InetAddress 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 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 { InetAddress::class.java.getDeclaredMethod("parseNumericAddress", String::class.java).apply {
isAccessible = true 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? = fun String?.parseNumericAddress(): InetAddress? = Os.inet_pton(OsConstants.AF_INET, this)
Os.inet_pton(OsConstants.AF_INET, this) ?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { ?: Os.inet_pton(OsConstants.AF_INET6, this)?.let { parseNumericAddress.invoke(null, this) as InetAddress }
if (Build.VERSION.SDK_INT >= 29) it else parseNumericAddress.invoke(null,
this) as InetAddress
}
fun <K, V> MutableMap<K, V>.computeIfAbsentCompat(key: K, value: () -> V) = fun HttpURLConnection.disconnectFromMain() {
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 {
if (Build.VERSION.SDK_INT >= 26) disconnect() else GlobalScope.launch(Dispatchers.IO) { disconnect() } 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 { 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 return if (value < min || value > 65535) default else value
} }
fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = object : BroadcastReceiver() {
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = callback(context, intent) override fun onReceive(context: Context, intent: Intent) = callback(context, intent)
} }
fun ContentResolver.openBitmap(uri: Uri) = fun ContentResolver.openBitmap(uri: Uri) =
if (Build.VERSION.SDK_INT >= 28) ImageDecoder.decodeBitmap(ImageDecoder.createSource(this, uri)) if (Build.VERSION.SDK_INT >= 28) ImageDecoder.decodeBitmap(ImageDecoder.createSource(this, uri))
else BitmapFactory.decodeStream(openInputStream(uri)) else BitmapFactory.decodeStream(openInputStream(uri))
val PackageInfo.signaturesCompat val PackageInfo.signaturesCompat get() =
get() = if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures if (Build.VERSION.SDK_INT >= 28) signingInfo.apkContentsSigners else @Suppress("DEPRECATION") signatures
/** /**
* Based on: https://stackoverflow.com/a/26348729/2245107 * Based on: https://stackoverflow.com/a/26348729/2245107
@ -122,11 +86,9 @@ fun Resources.Theme.resolveResourceId(@AttrRes resId: Int): Int {
return typedValue.resourceId return typedValue.resourceId
} }
val Intent.datas val Intent.datas get() = listOfNotNull(data) + (clipData?.asIterable()?.mapNotNull { it.uri } ?: emptyList())
get() = listOfNotNull(data) + (clipData?.asIterable()?.mapNotNull { it.uri } ?: emptyList())
fun printLog(t: Throwable) { fun printLog(t: Throwable) {
// Crashlytics.logException(t)
t.printStackTrace() 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}/libss-local.so
ANDROID_EXTRA_LIBS += $$PWD/android/lib/shadowsocks/$${abi}/libtun2socks.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 # %{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) { switch (c) {
case DockerContainer::WireGuard: return true; case DockerContainer::WireGuard: return true;
case DockerContainer::OpenVpn: return true; case DockerContainer::OpenVpn: return true;
case DockerContainer::ShadowSocks: return true;
default: return false; default: return false;
} }