mirror of
https://github.com/MatsuriDayo/NekoBoxForAndroid.git
synced 2025-12-18 22:20:06 +08:00
upload code
This commit is contained in:
commit
7d9798cc27
22
.github/ISSUE_TEMPLATE/bug-report-zh_cn.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/bug-report-zh_cn.md
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report zh_CN
|
||||||
|
about: 问题反馈,在提出问题前请先自行排除服务器端问题和升级到最新客户端。
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**描述问题**
|
||||||
|
|
||||||
|
预期行为:
|
||||||
|
|
||||||
|
实际行为:
|
||||||
|
|
||||||
|
**如何复现**
|
||||||
|
|
||||||
|
提供有帮助的截图,录像,文字说明,订阅链接等。
|
||||||
|
|
||||||
|
**日志**
|
||||||
|
|
||||||
|
如果有日志,请上传。请在文档内查看导出日志的详细步骤。
|
||||||
12
.github/ISSUE_TEMPLATE/feature_request-zh_cn.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/feature_request-zh_cn.md
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request zh_CN
|
||||||
|
about: 功能请求,提出建议。
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**描述建议**
|
||||||
|
|
||||||
|
**建议的必要性**
|
||||||
171
.github/workflows/release.yml
vendored
Normal file
171
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
name: Release Build
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Release Tag'
|
||||||
|
required: true
|
||||||
|
upload:
|
||||||
|
description: 'Upload: If want ignore'
|
||||||
|
required: false
|
||||||
|
publish:
|
||||||
|
description: 'Publish: If want ignore'
|
||||||
|
required: false
|
||||||
|
play:
|
||||||
|
description: 'Play: If want ignore'
|
||||||
|
required: false
|
||||||
|
jobs:
|
||||||
|
libcore:
|
||||||
|
name: Native Build (LibCore)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Golang Status
|
||||||
|
run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status
|
||||||
|
- name: Libcore Status
|
||||||
|
run: git ls-files libcore | xargs cat | sha1sum > libcore_status
|
||||||
|
- name: LibCore Cache
|
||||||
|
id: cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
app/libs/libcore.aar
|
||||||
|
key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }}
|
||||||
|
- name: Golang Cache
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: build/golang
|
||||||
|
key: go-${{ hashFiles('.github/workflows/*', 'golang_status') }}
|
||||||
|
- name: Native Build
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
run: ./run init action go && ./run lib core
|
||||||
|
build:
|
||||||
|
name: Build OSS APK
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- libcore
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Golang Status
|
||||||
|
run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status
|
||||||
|
- name: Libcore Status
|
||||||
|
run: git ls-files libcore | xargs cat | sha1sum > libcore_status
|
||||||
|
- name: LibCore Cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
app/libs/libcore.aar
|
||||||
|
key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }}
|
||||||
|
- name: Gradle cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.gradle
|
||||||
|
key: gradle-oss-${{ hashFiles('**/*.gradle.kts') }}
|
||||||
|
- name: Gradle Build
|
||||||
|
env:
|
||||||
|
BUILD_PLUGIN: none
|
||||||
|
run: |
|
||||||
|
echo "sdk.dir=${ANDROID_HOME}" > local.properties
|
||||||
|
echo "ndk.dir=${ANDROID_HOME}/ndk/25.0.8775105" >> local.properties
|
||||||
|
export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}"
|
||||||
|
./run init action gradle
|
||||||
|
./gradlew app:assembleOssRelease
|
||||||
|
APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk')
|
||||||
|
APK=$(dirname $APK)
|
||||||
|
echo "APK=$APK" >> $GITHUB_ENV
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: APKs
|
||||||
|
path: ${{ env.APK }}
|
||||||
|
publish:
|
||||||
|
name: Publish Release
|
||||||
|
if: github.event.inputs.publish != 'y'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Donwload Artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: APKs
|
||||||
|
path: artifacts
|
||||||
|
- name: Release
|
||||||
|
run: |
|
||||||
|
wget -O ghr.tar.gz https://github.com/tcnksm/ghr/releases/download/v0.13.0/ghr_v0.13.0_linux_amd64.tar.gz
|
||||||
|
tar -xvf ghr.tar.gz
|
||||||
|
mv ghr*linux_amd64/ghr .
|
||||||
|
mkdir apks
|
||||||
|
find artifacts -name "*.apk" -exec cp {} apks \;
|
||||||
|
./ghr -delete -t "${{ github.token }}" -n "${{ github.event.inputs.tag }}" "${{ github.event.inputs.tag }}" apks
|
||||||
|
upload:
|
||||||
|
name: Upload Release
|
||||||
|
if: github.event.inputs.upload != 'y'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Donwload Artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: APKs
|
||||||
|
path: artifacts
|
||||||
|
- name: Release
|
||||||
|
run: |
|
||||||
|
mkdir apks
|
||||||
|
find artifacts -name "*.apk" -exec cp {} apks \;
|
||||||
|
|
||||||
|
function upload() {
|
||||||
|
for apk in $@; do
|
||||||
|
echo ">> Uploading $apk"
|
||||||
|
curl https://api.telegram.org/bot${{ secrets.TELEGRAM_TOKEN }}/sendDocument \
|
||||||
|
-X POST \
|
||||||
|
-F chat_id="${{ secrets.TELEGRAM_CHANNEL }}" \
|
||||||
|
-F document="@$apk" \
|
||||||
|
--silent --show-error --fail >/dev/null &
|
||||||
|
done
|
||||||
|
for job in $(jobs -p); do
|
||||||
|
wait $job || exit 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
upload apks/*
|
||||||
|
play:
|
||||||
|
name: Build Play Bundle
|
||||||
|
if: github.event.inputs.play != 'y'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- libcore
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Golang Status
|
||||||
|
run: find buildScript libcore/*.sh | xargs cat | sha1sum > golang_status
|
||||||
|
- name: Libcore Status
|
||||||
|
run: git ls-files libcore | xargs cat | sha1sum > libcore_status
|
||||||
|
- name: LibCore Cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
app/libs/libcore.aar
|
||||||
|
key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }}
|
||||||
|
- name: Gradle cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.gradle
|
||||||
|
key: gradle-play-${{ hashFiles('**/*.gradle.kts') }}
|
||||||
|
- name: Checkout Library
|
||||||
|
run: |
|
||||||
|
git submodule update --init 'app/*'
|
||||||
|
- name: Gradle Build
|
||||||
|
run: |
|
||||||
|
echo "sdk.dir=${ANDROID_HOME}" > local.properties
|
||||||
|
echo "ndk.dir=${ANDROID_HOME}/ndk/25.0.8775105" >> local.properties
|
||||||
|
export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}"
|
||||||
|
./run init action gradle
|
||||||
|
./gradlew bundlePlayRelease
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: AAB
|
||||||
|
path: app/build/outputs/bundle/playRelease/app-play-release.aab
|
||||||
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
build/
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
|
/app/libs/
|
||||||
|
/app/src/main/assets/sing-box
|
||||||
|
/service_account_credentials.json
|
||||||
|
jniLibs/
|
||||||
|
/library/libcore_build/
|
||||||
|
.idea/deploymentTargetDropDown.xml
|
||||||
|
/nkmr
|
||||||
|
|
||||||
|
# submodules
|
||||||
|
/external
|
||||||
8
AUTHORS
Normal file
8
AUTHORS
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
SagerNet was originally created in late 2021, by
|
||||||
|
nekohasekai <contact-sagernet@sekai.icu>.
|
||||||
|
|
||||||
|
Here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS --
|
||||||
|
people who have submitted patches, fixed bugs, added translations, and
|
||||||
|
generally made SagerNet that much better:
|
||||||
|
|
||||||
|
https://github.com/SagerNet/SagerNet/graphs/contributors
|
||||||
14
LICENSE
Normal file
14
LICENSE
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
Copyright (C) 2021 by nekohasekai <contact-sagernet@sekai.icu>
|
||||||
|
|
||||||
|
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/>.
|
||||||
60
README.md
Normal file
60
README.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# NekoBox for Android
|
||||||
|
|
||||||
|
[](https://android-arsenal.com/api?level=21)
|
||||||
|
[](https://github.com/MatsuriDayo/NekoBoxForAndroid/releases)
|
||||||
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
|
|
||||||
|
sing-box / universal proxy toolchain for Android.
|
||||||
|
|
||||||
|
## 下载 / Downloads
|
||||||
|
|
||||||
|
### GitHub Releases
|
||||||
|
|
||||||
|
[](https://github.com/Matsuridayo/NekoBoxForAndroid/releases)
|
||||||
|
|
||||||
|
[下载](https://github.com/Matsuridayo/NekoBoxForAndroid/releases)
|
||||||
|
|
||||||
|
## 更改记录 & 发布频道 / Changelog & Telegram channel
|
||||||
|
|
||||||
|
https://t.me/Matsuridayo
|
||||||
|
|
||||||
|
## 项目主页 & 文档 / Homepage & Documents
|
||||||
|
|
||||||
|
https://matsuridayo.github.io
|
||||||
|
|
||||||
|
## 代理 / Proxy
|
||||||
|
|
||||||
|
* SOCKS (4/4a/5)
|
||||||
|
* HTTP(S)
|
||||||
|
* SSH
|
||||||
|
* Shadowsocks
|
||||||
|
* VMess
|
||||||
|
* VLESS
|
||||||
|
* WireGuard
|
||||||
|
* Trojan
|
||||||
|
* Trojan-Go ( trojan-go-plugin )
|
||||||
|
* NaïveProxy ( naive-plugin )
|
||||||
|
* Hysteria ( hysteria-plugin )
|
||||||
|
|
||||||
|
请到项目主页下载插件。
|
||||||
|
|
||||||
|
Please go to the project homepage to download plugins.
|
||||||
|
|
||||||
|
### 订阅 / Subscription
|
||||||
|
|
||||||
|
* Raw: some widely used formats (like shadowsocks, clash and v2rayN)
|
||||||
|
* 原始格式:一些广泛使用的格式(如 shadowsocks、clash 和 v2rayN)
|
||||||
|
* [Open Online Config](https://github.com/Shadowsocks-NET/OpenOnlineConfig)
|
||||||
|
* [Shadowsocks SIP008](https://shadowsocks.org/guide/sip008.html)
|
||||||
|
|
||||||
|
### 捐助 / Donate
|
||||||
|
|
||||||
|
欢迎捐赠以支持项目开发。
|
||||||
|
|
||||||
|
USDT TRC20
|
||||||
|
|
||||||
|
`TRhnA7SXE5Sap5gSG3ijxRmdYFiD4KRhPs`
|
||||||
|
|
||||||
|
XMR
|
||||||
|
|
||||||
|
`49bwESYQjoRL3xmvTcjZKHEKaiGywjLYVQJMUv79bXonGiyDCs8AzE3KiGW2ytTybBCpWJUvov8SjZZEGg66a4e59GXa6k5`
|
||||||
2
app/.gitignore
vendored
Normal file
2
app/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/build
|
||||||
|
/schemas
|
||||||
81
app/build.gradle.kts
Normal file
81
app/build.gradle.kts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
id("kotlin-kapt")
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
}
|
||||||
|
|
||||||
|
setupApp()
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
}
|
||||||
|
kapt.arguments {
|
||||||
|
arg("room.incremental", true)
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
|
bundle {
|
||||||
|
language {
|
||||||
|
enableSplit = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
namespace = "io.nekohasekai.sagernet"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation(fileTree("libs"))
|
||||||
|
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3")
|
||||||
|
implementation("androidx.core:core-ktx:1.7.0")
|
||||||
|
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||||
|
implementation("androidx.activity:activity-ktx:1.4.0")
|
||||||
|
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
||||||
|
implementation("androidx.browser:browser:1.4.0")
|
||||||
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
|
implementation("androidx.navigation:navigation-fragment-ktx:2.4.2")
|
||||||
|
implementation("androidx.navigation:navigation-ui-ktx:2.4.2")
|
||||||
|
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||||
|
implementation("androidx.work:work-runtime-ktx:2.7.1")
|
||||||
|
implementation("androidx.work:work-multiprocess:2.7.1")
|
||||||
|
|
||||||
|
implementation(project(":external:preferencex:preferencex"))
|
||||||
|
implementation(project(":external:preferencex:preferencex-simplemenu"))
|
||||||
|
|
||||||
|
implementation("com.google.android.material:material:1.6.0")
|
||||||
|
implementation("com.google.code.gson:gson:2.8.9")
|
||||||
|
|
||||||
|
implementation("com.github.jenly1314:zxing-lite:2.1.1")
|
||||||
|
implementation("com.afollestad.material-dialogs:core:3.3.0")
|
||||||
|
implementation("com.afollestad.material-dialogs:input:3.3.0")
|
||||||
|
|
||||||
|
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.3")
|
||||||
|
implementation("org.yaml:snakeyaml:1.30")
|
||||||
|
implementation("com.github.daniel-stoneuk:material-about-library:3.2.0-rc01")
|
||||||
|
implementation("com.jakewharton:process-phoenix:2.1.2")
|
||||||
|
implementation("com.esotericsoftware:kryo:5.2.1")
|
||||||
|
implementation("com.google.guava:guava:31.0.1-android")
|
||||||
|
implementation("org.ini4j:ini4j:0.5.4")
|
||||||
|
|
||||||
|
implementation("com.simplecityapps:recyclerview-fastscroll:2.0.1") {
|
||||||
|
exclude(group = "androidx.recyclerview")
|
||||||
|
exclude(group = "androidx.appcompat")
|
||||||
|
}
|
||||||
|
implementation("org.smali:dexlib2:2.5.2") {
|
||||||
|
exclude(group = "com.google.guava", module = "guava")
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation("androidx.room:room-runtime:2.4.2")
|
||||||
|
kapt("androidx.room:room-compiler:2.4.2")
|
||||||
|
implementation("androidx.room:room-ktx:2.4.2")
|
||||||
|
implementation("com.github.MatrixDev.Roomigrant:RoomigrantLib:0.3.4")
|
||||||
|
kapt("com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4")
|
||||||
|
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
||||||
|
}
|
||||||
47
app/proguard-rules.pro
vendored
Normal file
47
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
-repackageclasses ''
|
||||||
|
-allowaccessmodification
|
||||||
|
|
||||||
|
-keep class io.nekohasekai.sagernet.** { *;}
|
||||||
|
-keep class moe.matsuri.nb4a.** { *;}
|
||||||
|
|
||||||
|
# Clean Kotlin
|
||||||
|
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||||
|
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
|
||||||
|
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
|
||||||
|
static void checkNotNullExpressionValue(java.lang.Object, java.lang.String);
|
||||||
|
static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String, java.lang.String);
|
||||||
|
static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String);
|
||||||
|
static void checkFieldIsNotNull(java.lang.Object, java.lang.String, java.lang.String);
|
||||||
|
static void checkFieldIsNotNull(java.lang.Object, java.lang.String);
|
||||||
|
static void checkNotNull(java.lang.Object);
|
||||||
|
static void checkNotNull(java.lang.Object, java.lang.String);
|
||||||
|
static void checkNotNullParameter(java.lang.Object, java.lang.String);
|
||||||
|
static void throwUninitializedPropertyAccessException(java.lang.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
# ini4j
|
||||||
|
-keep public class org.ini4j.spi.** { <init>(); }
|
||||||
|
|
||||||
|
# SnakeYaml
|
||||||
|
-keep class org.yaml.snakeyaml.** { *; }
|
||||||
|
|
||||||
|
-dontobfuscate
|
||||||
|
-keepattributes SourceFile
|
||||||
|
|
||||||
|
-dontwarn java.beans.BeanInfo
|
||||||
|
-dontwarn java.beans.FeatureDescriptor
|
||||||
|
-dontwarn java.beans.IntrospectionException
|
||||||
|
-dontwarn java.beans.Introspector
|
||||||
|
-dontwarn java.beans.PropertyDescriptor
|
||||||
|
-dontwarn java.beans.Transient
|
||||||
|
-dontwarn java.beans.VetoableChangeListener
|
||||||
|
-dontwarn java.beans.VetoableChangeSupport
|
||||||
|
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
|
||||||
|
-dontwarn org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
|
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||||
|
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||||
|
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||||
|
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||||
|
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||||
|
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||||
|
-dontwarn java.beans.PropertyVetoException
|
||||||
@ -0,0 +1,330 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "f66fd943df1d9e86d281a2e32c9fdd47",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "proxy_groups",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userOrder",
|
||||||
|
"columnName": "userOrder",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ungrouped",
|
||||||
|
"columnName": "ungrouped",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscription",
|
||||||
|
"columnName": "subscription",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "proxy_entities",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `vmessBean` BLOB, `trojanBean` BLOB, `trojanGoBean` BLOB, `naiveBean` BLOB, `hysteriaBean` BLOB, `tuicBean` BLOB, `sshBean` BLOB, `wgBean` BLOB, `chainBean` BLOB, `nekoBean` BLOB, `configBean` BLOB)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "groupId",
|
||||||
|
"columnName": "groupId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userOrder",
|
||||||
|
"columnName": "userOrder",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tx",
|
||||||
|
"columnName": "tx",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "rx",
|
||||||
|
"columnName": "rx",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "status",
|
||||||
|
"columnName": "status",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ping",
|
||||||
|
"columnName": "ping",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uuid",
|
||||||
|
"columnName": "uuid",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "error",
|
||||||
|
"columnName": "error",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "socksBean",
|
||||||
|
"columnName": "socksBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "httpBean",
|
||||||
|
"columnName": "httpBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ssBean",
|
||||||
|
"columnName": "ssBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "vmessBean",
|
||||||
|
"columnName": "vmessBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "trojanBean",
|
||||||
|
"columnName": "trojanBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "trojanGoBean",
|
||||||
|
"columnName": "trojanGoBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "naiveBean",
|
||||||
|
"columnName": "naiveBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hysteriaBean",
|
||||||
|
"columnName": "hysteriaBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tuicBean",
|
||||||
|
"columnName": "tuicBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sshBean",
|
||||||
|
"columnName": "sshBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "wgBean",
|
||||||
|
"columnName": "wgBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chainBean",
|
||||||
|
"columnName": "chainBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "nekoBean",
|
||||||
|
"columnName": "nekoBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "configBean",
|
||||||
|
"columnName": "configBean",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "groupId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"groupId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "rules",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `packages` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userOrder",
|
||||||
|
"columnName": "userOrder",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "enabled",
|
||||||
|
"columnName": "enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domains",
|
||||||
|
"columnName": "domains",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ip",
|
||||||
|
"columnName": "ip",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "port",
|
||||||
|
"columnName": "port",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sourcePort",
|
||||||
|
"columnName": "sourcePort",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "network",
|
||||||
|
"columnName": "network",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "protocol",
|
||||||
|
"columnName": "protocol",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "outbound",
|
||||||
|
"columnName": "outbound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "packages",
|
||||||
|
"columnName": "packages",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"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, 'f66fd943df1d9e86d281a2e32c9fdd47')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/schemas/moe.matsuri.nb4a.TempDatabase/1.json
Normal file
46
app/schemas/moe.matsuri.nb4a.TempDatabase/1.json
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
316
app/src/main/AndroidManifest.xml
Normal file
316
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
|
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
|
||||||
|
|
||||||
|
<permission
|
||||||
|
android:name="${applicationId}.SERVICE"
|
||||||
|
android:protectionLevel="signature" />
|
||||||
|
|
||||||
|
<uses-permission android:name="${applicationId}.SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.touchscreen"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera.autofocus"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="com.github.shadowsocks.plugin.ACTION_NATIVE_PLUGIN" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name="io.nekohasekai.sagernet.SagerNet"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:autoRevokePermissions="allowed"
|
||||||
|
android:banner="@mipmap/ic_launcher"
|
||||||
|
android:dataExtractionRules="@xml/backup_rules"
|
||||||
|
android:extractNativeLibs="true"
|
||||||
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
|
android:fullBackupOnly="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:hasFragileUserData="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:largeHeap="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.Start">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.BlankActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.MainActivity"
|
||||||
|
android:configChanges="uiMode"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:label="@string/subscription_import">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="subscription"
|
||||||
|
android:scheme="sn" />
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:label="@string/subscription_import">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="install-config"
|
||||||
|
android:scheme="clash" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:label="@string/profile_import">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="sn" />
|
||||||
|
<data android:scheme="ss" />
|
||||||
|
<data android:scheme="ssr" />
|
||||||
|
<data android:scheme="socks" />
|
||||||
|
<data android:scheme="socks4" />
|
||||||
|
<data android:scheme="socksa" />
|
||||||
|
<data android:scheme="sock5" />
|
||||||
|
<data android:scheme="vmess" />
|
||||||
|
<data android:scheme="trojan" />
|
||||||
|
<data android:scheme="trojan-go" />
|
||||||
|
<data android:scheme="naive+https" />
|
||||||
|
<data android:scheme="naive+quic" />
|
||||||
|
<data android:scheme="hysteria" />
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/shortcuts" />
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.VpnRequestActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:taskAffinity="" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.SocksSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.HttpSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.ShadowsocksSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.ShadowsocksRSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.VMessSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.TrojanSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.TrojanGoSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.NaiveSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.HysteriaSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.SSHSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.WireGuardSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.TuicSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.profile.ChainSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="moe.matsuri.nb4a.proxy.neko.NekoSettingActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="moe.matsuri.nb4a.proxy.config.ConfigSettingActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.GroupSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.RouteSettingsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.AssetsActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.AppListActivity"
|
||||||
|
android:configChanges="uiMode" />
|
||||||
|
<activity
|
||||||
|
android:name=".QuickToggleShortcut"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/quick_toggle"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:process=":bg"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.AppManagerActivity"
|
||||||
|
android:configChanges="uiMode"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:label="@string/proxied_apps"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:parentActivityName=".ui.MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.ScannerActivity"
|
||||||
|
android:configChanges="uiMode"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:label="@string/add_profile_methods_scan_qr_code"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:parentActivityName="io.nekohasekai.sagernet.ui.MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.ProfileSelectActivity"
|
||||||
|
android:configChanges="uiMode"
|
||||||
|
android:label="@string/select_profile"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:parentActivityName="io.nekohasekai.sagernet.ui.MainActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.StunActivity"
|
||||||
|
android:configChanges="uiMode"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:parentActivityName="io.nekohasekai.sagernet.ui.MainActivity" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="io.nekohasekai.sagernet.ui.SwitchActivity"
|
||||||
|
android:configChanges="uiMode"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:launchMode="singleInstance"
|
||||||
|
android:theme="@style/Theme.SagerNet.Dialog" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="io.nekohasekai.sagernet.bg.ProxyService"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":bg" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="io.nekohasekai.sagernet.bg.VpnService"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
|
android:process=":bg">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.net.VpnService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="io.nekohasekai.sagernet.bg.TileService"
|
||||||
|
android:exported="true"
|
||||||
|
android:icon="@drawable/ic_service_active"
|
||||||
|
android:label="@string/tile_title"
|
||||||
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
|
android:process=":bg"
|
||||||
|
tools:targetApi="n">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||||
|
android:value="true" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.cache"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/cache_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name="io.nekohasekai.sagernet.BootReceiver"
|
||||||
|
android:enabled="false"
|
||||||
|
android:exported="true"
|
||||||
|
android:process=":bg">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.room.MultiInstanceInvalidationService"
|
||||||
|
android:process=":bg" />
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package io.nekohasekai.sagernet.aidl;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.aidl.ISagerNetServiceCallback;
|
||||||
|
|
||||||
|
interface ISagerNetService {
|
||||||
|
int getState();
|
||||||
|
String getProfileName();
|
||||||
|
|
||||||
|
void registerCallback(in ISagerNetServiceCallback cb);
|
||||||
|
oneway void unregisterCallback(in ISagerNetServiceCallback cb);
|
||||||
|
|
||||||
|
int urlTest();
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package io.nekohasekai.sagernet.aidl;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.aidl.SpeedDisplayData;
|
||||||
|
import io.nekohasekai.sagernet.aidl.TrafficData;
|
||||||
|
|
||||||
|
oneway interface ISagerNetServiceCallback {
|
||||||
|
void stateChanged(int state, String profileName, String msg);
|
||||||
|
void missingPlugin(String profileName, String pluginName);
|
||||||
|
void routeAlert(int type, String routeName);
|
||||||
|
void updateWakeLockStatus(boolean acquired);
|
||||||
|
void cbSpeedUpdate(in SpeedDisplayData stats);
|
||||||
|
void cbTrafficUpdate(in TrafficData stats);
|
||||||
|
void cbLogUpdate(String str);
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
package io.nekohasekai.sagernet.aidl;
|
||||||
|
|
||||||
|
parcelable SpeedDisplayData;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
package io.nekohasekai.sagernet.aidl;
|
||||||
|
|
||||||
|
parcelable TrafficData;
|
||||||
19
app/src/main/assets/LICENSE
Normal file
19
app/src/main/assets/LICENSE
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (C) 2021 by nekohasekai
|
||||||
|
<contact-sagernet@sekai.icu>
|
||||||
|
|
||||||
|
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/>.
|
||||||
5
app/src/main/assets/analysis.txt
Normal file
5
app/src/main/assets/analysis.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
domain:appcenter.ms
|
||||||
|
domain:app-measurement.com
|
||||||
|
domain:firebase.io
|
||||||
|
domain:crashlytics.com
|
||||||
|
domain:google-analytics.com
|
||||||
1
app/src/main/assets/yacd.version.txt
Normal file
1
app/src/main/assets/yacd.version.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
BIN
app/src/main/assets/yacd.zip
Normal file
BIN
app/src/main/assets/yacd.zip
Normal file
Binary file not shown.
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
9
app/src/main/java/com/github/shadowsocks/plugin/Utils.kt
Normal file
9
app/src/main/java/com/github/shadowsocks/plugin/Utils.kt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
@file:JvmName("Utils")
|
||||||
|
|
||||||
|
package com.github.shadowsocks.plugin
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class Empty : Parcelable
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.github.shadowsocks.plugin.fragment
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatDialogFragment
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.setFragmentResult
|
||||||
|
import androidx.fragment.app.setFragmentResultListener
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on: https://android.googlesource.com/platform/
|
||||||
|
packages/apps/ExactCalculator/+/8c43f06/src/com/android/calculator2/AlertDialogFragment.java
|
||||||
|
*/
|
||||||
|
abstract class AlertDialogFragment<Arg : Parcelable, Ret : Parcelable?> :
|
||||||
|
AppCompatDialogFragment(), DialogInterface.OnClickListener {
|
||||||
|
companion object {
|
||||||
|
private const val KEY_RESULT = "result"
|
||||||
|
private const val KEY_ARG = "arg"
|
||||||
|
private const val KEY_RET = "ret"
|
||||||
|
private const val KEY_WHICH = "which"
|
||||||
|
|
||||||
|
fun <Ret : Parcelable> setResultListener(fragment: Fragment, requestKey: String,
|
||||||
|
listener: (Int, Ret?) -> Unit) {
|
||||||
|
fragment.setFragmentResultListener(requestKey) { _, bundle ->
|
||||||
|
listener(bundle.getInt(KEY_WHICH, Activity.RESULT_CANCELED), bundle.getParcelable(KEY_RET))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inline fun <reified T : AlertDialogFragment<*, Ret>, Ret : Parcelable?> setResultListener(
|
||||||
|
fragment: Fragment, noinline listener: (Int, Ret?) -> Unit) =
|
||||||
|
setResultListener(fragment, T::class.java.name, listener)
|
||||||
|
}
|
||||||
|
protected abstract fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener)
|
||||||
|
|
||||||
|
private val resultKey get() = requireArguments().getString(KEY_RESULT)
|
||||||
|
protected val arg by lazy { requireArguments().getParcelable<Arg>(KEY_ARG)!! }
|
||||||
|
protected open fun ret(which: Int): Ret? = null
|
||||||
|
|
||||||
|
private fun args() = arguments ?: Bundle().also { arguments = it }
|
||||||
|
fun arg(arg: Arg) = args().putParcelable(KEY_ARG, arg)
|
||||||
|
fun key(resultKey: String = javaClass.name) = args().putString(KEY_RESULT, resultKey)
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog =
|
||||||
|
MaterialAlertDialogBuilder(requireContext()).also { it.prepare(this) }.create()
|
||||||
|
|
||||||
|
override fun onClick(dialog: DialogInterface?, which: Int) {
|
||||||
|
setFragmentResult(resultKey ?: return, Bundle().apply {
|
||||||
|
putInt(KEY_WHICH, which)
|
||||||
|
putParcelable(KEY_RET, ret(which) ?: return@apply)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
|
super.onDismiss(dialog)
|
||||||
|
onClick(null, Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
}
|
||||||
497
app/src/main/java/com/wireguard/crypto/Curve25519.java
Normal file
497
app/src/main/java/com/wireguard/crypto/Curve25519.java
Normal file
@ -0,0 +1,497 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2016 Southern Storm Software, Pty Ltd.
|
||||||
|
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.crypto;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of Curve25519 ECDH.
|
||||||
|
* <p>
|
||||||
|
* This implementation was imported to WireGuard from noise-java:
|
||||||
|
* https://github.com/rweather/noise-java
|
||||||
|
* <p>
|
||||||
|
* This implementation is based on that from arduinolibs:
|
||||||
|
* https://github.com/rweather/arduinolibs
|
||||||
|
* <p>
|
||||||
|
* Differences in this version are due to using 26-bit limbs for the
|
||||||
|
* representation instead of the 8/16/32-bit limbs in the original.
|
||||||
|
* <p>
|
||||||
|
* References: http://cr.yp.to/ecdh.html, RFC 7748
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"})
|
||||||
|
public final class Curve25519 {
|
||||||
|
// Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
|
||||||
|
private static final int NUM_LIMBS_255BIT = 10;
|
||||||
|
private static final int NUM_LIMBS_510BIT = 20;
|
||||||
|
|
||||||
|
private final int[] A;
|
||||||
|
private final int[] AA;
|
||||||
|
private final int[] B;
|
||||||
|
private final int[] BB;
|
||||||
|
private final int[] C;
|
||||||
|
private final int[] CB;
|
||||||
|
private final int[] D;
|
||||||
|
private final int[] DA;
|
||||||
|
private final int[] E;
|
||||||
|
private final long[] t1;
|
||||||
|
private final int[] t2;
|
||||||
|
private final int[] x_1;
|
||||||
|
private final int[] x_2;
|
||||||
|
private final int[] x_3;
|
||||||
|
private final int[] z_2;
|
||||||
|
private final int[] z_3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the temporary state holder for Curve25519 evaluation.
|
||||||
|
*/
|
||||||
|
private Curve25519() {
|
||||||
|
// Allocate memory for all of the temporary variables we will need.
|
||||||
|
x_1 = new int[NUM_LIMBS_255BIT];
|
||||||
|
x_2 = new int[NUM_LIMBS_255BIT];
|
||||||
|
x_3 = new int[NUM_LIMBS_255BIT];
|
||||||
|
z_2 = new int[NUM_LIMBS_255BIT];
|
||||||
|
z_3 = new int[NUM_LIMBS_255BIT];
|
||||||
|
A = new int[NUM_LIMBS_255BIT];
|
||||||
|
B = new int[NUM_LIMBS_255BIT];
|
||||||
|
C = new int[NUM_LIMBS_255BIT];
|
||||||
|
D = new int[NUM_LIMBS_255BIT];
|
||||||
|
E = new int[NUM_LIMBS_255BIT];
|
||||||
|
AA = new int[NUM_LIMBS_255BIT];
|
||||||
|
BB = new int[NUM_LIMBS_255BIT];
|
||||||
|
DA = new int[NUM_LIMBS_255BIT];
|
||||||
|
CB = new int[NUM_LIMBS_255BIT];
|
||||||
|
t1 = new long[NUM_LIMBS_510BIT];
|
||||||
|
t2 = new int[NUM_LIMBS_510BIT];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditional swap of two values.
|
||||||
|
*
|
||||||
|
* @param select Set to 1 to swap, 0 to leave as-is.
|
||||||
|
* @param x The first value.
|
||||||
|
* @param y The second value.
|
||||||
|
*/
|
||||||
|
private static void cswap(int select, final int[] x, final int[] y) {
|
||||||
|
select = -select;
|
||||||
|
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
final int dummy = select & (x[index] ^ y[index]);
|
||||||
|
x[index] ^= dummy;
|
||||||
|
y[index] ^= dummy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the Curve25519 curve.
|
||||||
|
*
|
||||||
|
* @param result Buffer to place the result of the evaluation into.
|
||||||
|
* @param offset Offset into the result buffer.
|
||||||
|
* @param privateKey The private key to use in the evaluation.
|
||||||
|
* @param publicKey The public key to use in the evaluation, or null
|
||||||
|
* if the base point of the curve should be used.
|
||||||
|
*/
|
||||||
|
public static void eval(final byte[] result, final int offset,
|
||||||
|
final byte[] privateKey, @Nullable final byte[] publicKey) {
|
||||||
|
final Curve25519 state = new Curve25519();
|
||||||
|
try {
|
||||||
|
// Unpack the public key value. If null, use 9 as the base point.
|
||||||
|
Arrays.fill(state.x_1, 0);
|
||||||
|
if (publicKey != null) {
|
||||||
|
// Convert the input value from little-endian into 26-bit limbs.
|
||||||
|
for (int index = 0; index < 32; ++index) {
|
||||||
|
final int bit = (index * 8) % 26;
|
||||||
|
final int word = (index * 8) / 26;
|
||||||
|
final int value = publicKey[index] & 0xFF;
|
||||||
|
if (bit <= (26 - 8)) {
|
||||||
|
state.x_1[word] |= value << bit;
|
||||||
|
} else {
|
||||||
|
state.x_1[word] |= value << bit;
|
||||||
|
state.x_1[word] &= 0x03FFFFFF;
|
||||||
|
state.x_1[word + 1] |= value >> (26 - bit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just in case, we reduce the number modulo 2^255 - 19 to
|
||||||
|
// make sure that it is in range of the field before we start.
|
||||||
|
// This eliminates values between 2^255 - 19 and 2^256 - 1.
|
||||||
|
state.reduceQuick(state.x_1);
|
||||||
|
state.reduceQuick(state.x_1);
|
||||||
|
} else {
|
||||||
|
state.x_1[0] = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the other temporary variables.
|
||||||
|
Arrays.fill(state.x_2, 0); // x_2 = 1
|
||||||
|
state.x_2[0] = 1;
|
||||||
|
Arrays.fill(state.z_2, 0); // z_2 = 0
|
||||||
|
System.arraycopy(state.x_1, 0, state.x_3, 0, state.x_1.length); // x_3 = x_1
|
||||||
|
Arrays.fill(state.z_3, 0); // z_3 = 1
|
||||||
|
state.z_3[0] = 1;
|
||||||
|
|
||||||
|
// Evaluate the curve for every bit of the private key.
|
||||||
|
state.evalCurve(privateKey);
|
||||||
|
|
||||||
|
// Compute x_2 * (z_2 ^ (p - 2)) where p = 2^255 - 19.
|
||||||
|
state.recip(state.z_3, state.z_2);
|
||||||
|
state.mul(state.x_2, state.x_2, state.z_3);
|
||||||
|
|
||||||
|
// Convert x_2 into little-endian in the result buffer.
|
||||||
|
for (int index = 0; index < 32; ++index) {
|
||||||
|
final int bit = (index * 8) % 26;
|
||||||
|
final int word = (index * 8) / 26;
|
||||||
|
if (bit <= (26 - 8))
|
||||||
|
result[offset + index] = (byte) (state.x_2[word] >> bit);
|
||||||
|
else
|
||||||
|
result[offset + index] = (byte) ((state.x_2[word] >> bit) | (state.x_2[word + 1] << (26 - bit)));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Clean up all temporary state before we exit.
|
||||||
|
state.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtracts two numbers modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result.
|
||||||
|
* @param x The first number to subtract.
|
||||||
|
* @param y The second number to subtract.
|
||||||
|
*/
|
||||||
|
private static void sub(final int[] result, final int[] x, final int[] y) {
|
||||||
|
int index;
|
||||||
|
int borrow;
|
||||||
|
|
||||||
|
// Subtract y from x to generate the intermediate result.
|
||||||
|
borrow = 0;
|
||||||
|
for (index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
borrow = x[index] - y[index] - ((borrow >> 26) & 0x01);
|
||||||
|
result[index] = borrow & 0x03FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we had a borrow, then the result has gone negative and we
|
||||||
|
// have to add 2^255 - 19 to the result to make it positive again.
|
||||||
|
// The top bits of "borrow" will be all 1's if there is a borrow
|
||||||
|
// or it will be all 0's if there was no borrow. Easiest is to
|
||||||
|
// conditionally subtract 19 and then mask off the high bits.
|
||||||
|
borrow = result[0] - ((-((borrow >> 26) & 0x01)) & 19);
|
||||||
|
result[0] = borrow & 0x03FFFFFF;
|
||||||
|
for (index = 1; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
borrow = result[index] - ((borrow >> 26) & 0x01);
|
||||||
|
result[index] = borrow & 0x03FFFFFF;
|
||||||
|
}
|
||||||
|
result[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds two numbers modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result.
|
||||||
|
* @param x The first number to add.
|
||||||
|
* @param y The second number to add.
|
||||||
|
*/
|
||||||
|
private void add(final int[] result, final int[] x, final int[] y) {
|
||||||
|
int carry = x[0] + y[0];
|
||||||
|
result[0] = carry & 0x03FFFFFF;
|
||||||
|
for (int index = 1; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
carry = (carry >> 26) + x[index] + y[index];
|
||||||
|
result[index] = carry & 0x03FFFFFF;
|
||||||
|
}
|
||||||
|
reduceQuick(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy all sensitive data in this object.
|
||||||
|
*/
|
||||||
|
private void destroy() {
|
||||||
|
// Destroy all temporary variables.
|
||||||
|
Arrays.fill(x_1, 0);
|
||||||
|
Arrays.fill(x_2, 0);
|
||||||
|
Arrays.fill(x_3, 0);
|
||||||
|
Arrays.fill(z_2, 0);
|
||||||
|
Arrays.fill(z_3, 0);
|
||||||
|
Arrays.fill(A, 0);
|
||||||
|
Arrays.fill(B, 0);
|
||||||
|
Arrays.fill(C, 0);
|
||||||
|
Arrays.fill(D, 0);
|
||||||
|
Arrays.fill(E, 0);
|
||||||
|
Arrays.fill(AA, 0);
|
||||||
|
Arrays.fill(BB, 0);
|
||||||
|
Arrays.fill(DA, 0);
|
||||||
|
Arrays.fill(CB, 0);
|
||||||
|
Arrays.fill(t1, 0L);
|
||||||
|
Arrays.fill(t2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates the curve for every bit in a secret key.
|
||||||
|
*
|
||||||
|
* @param s The 32-byte secret key.
|
||||||
|
*/
|
||||||
|
private void evalCurve(final byte[] s) {
|
||||||
|
int sposn = 31;
|
||||||
|
int sbit = 6;
|
||||||
|
int svalue = s[sposn] | 0x40;
|
||||||
|
int swap = 0;
|
||||||
|
|
||||||
|
// Iterate over all 255 bits of "s" from the highest to the lowest.
|
||||||
|
// We ignore the high bit of the 256-bit representation of "s".
|
||||||
|
while (true) {
|
||||||
|
// Conditional swaps on entry to this bit but only if we
|
||||||
|
// didn't swap on the previous bit.
|
||||||
|
final int select = (svalue >> sbit) & 0x01;
|
||||||
|
swap ^= select;
|
||||||
|
cswap(swap, x_2, x_3);
|
||||||
|
cswap(swap, z_2, z_3);
|
||||||
|
swap = select;
|
||||||
|
|
||||||
|
// Evaluate the curve.
|
||||||
|
add(A, x_2, z_2); // A = x_2 + z_2
|
||||||
|
square(AA, A); // AA = A^2
|
||||||
|
sub(B, x_2, z_2); // B = x_2 - z_2
|
||||||
|
square(BB, B); // BB = B^2
|
||||||
|
sub(E, AA, BB); // E = AA - BB
|
||||||
|
add(C, x_3, z_3); // C = x_3 + z_3
|
||||||
|
sub(D, x_3, z_3); // D = x_3 - z_3
|
||||||
|
mul(DA, D, A); // DA = D * A
|
||||||
|
mul(CB, C, B); // CB = C * B
|
||||||
|
add(x_3, DA, CB); // x_3 = (DA + CB)^2
|
||||||
|
square(x_3, x_3);
|
||||||
|
sub(z_3, DA, CB); // z_3 = x_1 * (DA - CB)^2
|
||||||
|
square(z_3, z_3);
|
||||||
|
mul(z_3, z_3, x_1);
|
||||||
|
mul(x_2, AA, BB); // x_2 = AA * BB
|
||||||
|
mulA24(z_2, E); // z_2 = E * (AA + a24 * E)
|
||||||
|
add(z_2, z_2, AA);
|
||||||
|
mul(z_2, z_2, E);
|
||||||
|
|
||||||
|
// Move onto the next lower bit of "s".
|
||||||
|
if (sbit > 0) {
|
||||||
|
--sbit;
|
||||||
|
} else if (sposn == 0) {
|
||||||
|
break;
|
||||||
|
} else if (sposn == 1) {
|
||||||
|
--sposn;
|
||||||
|
svalue = s[sposn] & 0xF8;
|
||||||
|
sbit = 7;
|
||||||
|
} else {
|
||||||
|
--sposn;
|
||||||
|
svalue = s[sposn];
|
||||||
|
sbit = 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final conditional swaps.
|
||||||
|
cswap(swap, x_2, x_3);
|
||||||
|
cswap(swap, z_2, z_3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplies two numbers modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result.
|
||||||
|
* @param x The first number to multiply.
|
||||||
|
* @param y The second number to multiply.
|
||||||
|
*/
|
||||||
|
private void mul(final int[] result, final int[] x, final int[] y) {
|
||||||
|
// Multiply the two numbers to create the intermediate result.
|
||||||
|
long v = x[0];
|
||||||
|
for (int i = 0; i < NUM_LIMBS_255BIT; ++i) {
|
||||||
|
t1[i] = v * y[i];
|
||||||
|
}
|
||||||
|
for (int i = 1; i < NUM_LIMBS_255BIT; ++i) {
|
||||||
|
v = x[i];
|
||||||
|
for (int j = 0; j < (NUM_LIMBS_255BIT - 1); ++j) {
|
||||||
|
t1[i + j] += v * y[j];
|
||||||
|
}
|
||||||
|
t1[i + NUM_LIMBS_255BIT - 1] = v * y[NUM_LIMBS_255BIT - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate carries and convert back into 26-bit words.
|
||||||
|
v = t1[0];
|
||||||
|
t2[0] = ((int) v) & 0x03FFFFFF;
|
||||||
|
for (int i = 1; i < NUM_LIMBS_510BIT; ++i) {
|
||||||
|
v = (v >> 26) + t1[i];
|
||||||
|
t2[i] = ((int) v) & 0x03FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce the result modulo 2^255 - 19.
|
||||||
|
reduce(result, t2, NUM_LIMBS_255BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplies a number by the a24 constant, modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result.
|
||||||
|
* @param x The number to multiply by a24.
|
||||||
|
*/
|
||||||
|
private void mulA24(final int[] result, final int[] x) {
|
||||||
|
final long a24 = 121665;
|
||||||
|
long carry = 0;
|
||||||
|
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
carry += a24 * x[index];
|
||||||
|
t2[index] = ((int) carry) & 0x03FFFFFF;
|
||||||
|
carry >>= 26;
|
||||||
|
}
|
||||||
|
t2[NUM_LIMBS_255BIT] = ((int) carry) & 0x03FFFFFF;
|
||||||
|
reduce(result, t2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raise x to the power of (2^250 - 1).
|
||||||
|
*
|
||||||
|
* @param result The result. Must not overlap with x.
|
||||||
|
* @param x The argument.
|
||||||
|
*/
|
||||||
|
private void pow250(final int[] result, final int[] x) {
|
||||||
|
// The big-endian hexadecimal expansion of (2^250 - 1) is:
|
||||||
|
// 03FFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
|
||||||
|
//
|
||||||
|
// The naive implementation needs to do 2 multiplications per 1 bit and
|
||||||
|
// 1 multiplication per 0 bit. We can improve upon this by creating a
|
||||||
|
// pattern 0000000001 ... 0000000001. If we square and multiply the
|
||||||
|
// pattern by itself we can turn the pattern into the partial results
|
||||||
|
// 0000000011 ... 0000000011, 0000000111 ... 0000000111, etc.
|
||||||
|
// This averages out to about 1.1 multiplications per 1 bit instead of 2.
|
||||||
|
|
||||||
|
// Build a pattern of 250 bits in length of repeated copies of 0000000001.
|
||||||
|
square(A, x);
|
||||||
|
for (int j = 0; j < 9; ++j)
|
||||||
|
square(A, A);
|
||||||
|
mul(result, A, x);
|
||||||
|
for (int i = 0; i < 23; ++i) {
|
||||||
|
for (int j = 0; j < 10; ++j)
|
||||||
|
square(A, A);
|
||||||
|
mul(result, result, A);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply bit-shifted versions of the 0000000001 pattern into
|
||||||
|
// the result to "fill in" the gaps in the pattern.
|
||||||
|
square(A, result);
|
||||||
|
mul(result, result, A);
|
||||||
|
for (int j = 0; j < 8; ++j) {
|
||||||
|
square(A, A);
|
||||||
|
mul(result, result, A);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the reciprocal of a number modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result. Must not overlap with x.
|
||||||
|
* @param x The argument.
|
||||||
|
*/
|
||||||
|
private void recip(final int[] result, final int[] x) {
|
||||||
|
// The reciprocal is the same as x ^ (p - 2) where p = 2^255 - 19.
|
||||||
|
// The big-endian hexadecimal expansion of (p - 2) is:
|
||||||
|
// 7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFEB
|
||||||
|
// Start with the 250 upper bits of the expansion of (p - 2).
|
||||||
|
pow250(result, x);
|
||||||
|
|
||||||
|
// Deal with the 5 lowest bits of (p - 2), 01011, from highest to lowest.
|
||||||
|
square(result, result);
|
||||||
|
square(result, result);
|
||||||
|
mul(result, result, x);
|
||||||
|
square(result, result);
|
||||||
|
square(result, result);
|
||||||
|
mul(result, result, x);
|
||||||
|
square(result, result);
|
||||||
|
mul(result, result, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce a number modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result.
|
||||||
|
* @param x The value to be reduced. This array will be
|
||||||
|
* modified during the reduction.
|
||||||
|
* @param size The number of limbs in the high order half of x.
|
||||||
|
*/
|
||||||
|
private void reduce(final int[] result, final int[] x, final int size) {
|
||||||
|
// Calculate (x mod 2^255) + ((x / 2^255) * 19) which will
|
||||||
|
// either produce the answer we want or it will produce a
|
||||||
|
// value of the form "answer + j * (2^255 - 19)". There are
|
||||||
|
// 5 left-over bits in the top-most limb of the bottom half.
|
||||||
|
int carry = 0;
|
||||||
|
int limb = x[NUM_LIMBS_255BIT - 1] >> 21;
|
||||||
|
x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
|
||||||
|
for (int index = 0; index < size; ++index) {
|
||||||
|
limb += x[NUM_LIMBS_255BIT + index] << 5;
|
||||||
|
carry += (limb & 0x03FFFFFF) * 19 + x[index];
|
||||||
|
x[index] = carry & 0x03FFFFFF;
|
||||||
|
limb >>= 26;
|
||||||
|
carry >>= 26;
|
||||||
|
}
|
||||||
|
if (size < NUM_LIMBS_255BIT) {
|
||||||
|
// The high order half of the number is short; e.g. for mulA24().
|
||||||
|
// Propagate the carry through the rest of the low order part.
|
||||||
|
for (int index = size; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
carry += x[index];
|
||||||
|
x[index] = carry & 0x03FFFFFF;
|
||||||
|
carry >>= 26;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "j" value may still be too large due to the final carry-out.
|
||||||
|
// We must repeat the reduction. If we already have the answer,
|
||||||
|
// then this won't do any harm but we must still do the calculation
|
||||||
|
// to preserve the overall timing. The "j" value will be between
|
||||||
|
// 0 and 19, which means that the carry we care about is in the
|
||||||
|
// top 5 bits of the highest limb of the bottom half.
|
||||||
|
carry = (x[NUM_LIMBS_255BIT - 1] >> 21) * 19;
|
||||||
|
x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
|
||||||
|
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
carry += x[index];
|
||||||
|
result[index] = carry & 0x03FFFFFF;
|
||||||
|
carry >>= 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point "x" will either be the answer or it will be the
|
||||||
|
// answer plus (2^255 - 19). Perform a trial subtraction to
|
||||||
|
// complete the reduction process.
|
||||||
|
reduceQuick(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduces a number modulo 2^255 - 19 where it is known that the
|
||||||
|
* number can be reduced with only 1 trial subtraction.
|
||||||
|
*
|
||||||
|
* @param x The number to reduce, and the result.
|
||||||
|
*/
|
||||||
|
private void reduceQuick(final int[] x) {
|
||||||
|
// Perform a trial subtraction of (2^255 - 19) from "x" which is
|
||||||
|
// equivalent to adding 19 and subtracting 2^255. We add 19 here;
|
||||||
|
// the subtraction of 2^255 occurs in the next step.
|
||||||
|
int carry = 19;
|
||||||
|
for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
|
||||||
|
carry += x[index];
|
||||||
|
t2[index] = carry & 0x03FFFFFF;
|
||||||
|
carry >>= 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was a borrow, then the original "x" is the correct answer.
|
||||||
|
// If there was no borrow, then "t2" is the correct answer. Select the
|
||||||
|
// correct answer but do it in a way that instruction timing will not
|
||||||
|
// reveal which value was selected. Borrow will occur if bit 21 of
|
||||||
|
// "t2" is zero. Turn the bit into a selection mask.
|
||||||
|
final int mask = -((t2[NUM_LIMBS_255BIT - 1] >> 21) & 0x01);
|
||||||
|
final int nmask = ~mask;
|
||||||
|
t2[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
|
||||||
|
for (int index = 0; index < NUM_LIMBS_255BIT; ++index)
|
||||||
|
x[index] = (x[index] & nmask) | (t2[index] & mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Squares a number modulo 2^255 - 19.
|
||||||
|
*
|
||||||
|
* @param result The result.
|
||||||
|
* @param x The number to square.
|
||||||
|
*/
|
||||||
|
private void square(final int[] result, final int[] x) {
|
||||||
|
mul(result, x, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
2508
app/src/main/java/com/wireguard/crypto/Ed25519.java
Normal file
2508
app/src/main/java/com/wireguard/crypto/Ed25519.java
Normal file
File diff suppressed because it is too large
Load Diff
288
app/src/main/java/com/wireguard/crypto/Key.java
Normal file
288
app/src/main/java/com/wireguard/crypto/Key.java
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.crypto;
|
||||||
|
|
||||||
|
import com.wireguard.crypto.KeyFormatException.Type;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a WireGuard public or private key. This class uses specialized constant-time base64
|
||||||
|
* and hexadecimal codec implementations that resist side-channel attacks.
|
||||||
|
* <p>
|
||||||
|
* Instances of this class are immutable.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("MagicNumber")
|
||||||
|
public final class Key {
|
||||||
|
private final byte[] key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an object encapsulating the supplied key.
|
||||||
|
*
|
||||||
|
* @param key an array of bytes containing a binary key. Callers of this constructor are
|
||||||
|
* responsible for ensuring that the array is of the correct length.
|
||||||
|
*/
|
||||||
|
private Key(final byte[] key) {
|
||||||
|
// Defensively copy to ensure immutability.
|
||||||
|
this.key = Arrays.copyOf(key, key.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a single 4-character base64 chunk to an integer in constant time.
|
||||||
|
*
|
||||||
|
* @param src an array of at least 4 characters in base64 format
|
||||||
|
* @param srcOffset the offset of the beginning of the chunk in {@code src}
|
||||||
|
* @return the decoded 3-byte integer, or some arbitrary integer value if the input was not
|
||||||
|
* valid base64
|
||||||
|
*/
|
||||||
|
private static int decodeBase64(final char[] src, final int srcOffset) {
|
||||||
|
int val = 0;
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
final char c = src[i + srcOffset];
|
||||||
|
val |= (-1
|
||||||
|
+ ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64))
|
||||||
|
+ ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70))
|
||||||
|
+ ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5))
|
||||||
|
+ ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63)
|
||||||
|
+ ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64)
|
||||||
|
) << (18 - 6 * i);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time.
|
||||||
|
*
|
||||||
|
* @param src an array of at least 3 bytes
|
||||||
|
* @param srcOffset the offset of the beginning of the chunk in {@code src}
|
||||||
|
* @param dest an array of at least 4 characters
|
||||||
|
* @param destOffset the offset of the beginning of the chunk in {@code dest}
|
||||||
|
*/
|
||||||
|
private static void encodeBase64(final byte[] src, final int srcOffset,
|
||||||
|
final char[] dest, final int destOffset) {
|
||||||
|
final byte[] input = {
|
||||||
|
(byte) ((src[srcOffset] >>> 2) & 63),
|
||||||
|
(byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63),
|
||||||
|
(byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63),
|
||||||
|
(byte) ((src[2 + srcOffset]) & 63),
|
||||||
|
};
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
dest[i + destOffset] = (char) (input[i] + 'A'
|
||||||
|
+ (((25 - input[i]) >>> 8) & 6)
|
||||||
|
- (((51 - input[i]) >>> 8) & 75)
|
||||||
|
- (((61 - input[i]) >>> 8) & 15)
|
||||||
|
+ (((62 - input[i]) >>> 8) & 3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a WireGuard public or private key from its base64 string representation. This
|
||||||
|
* function throws a {@link KeyFormatException} if the source string is not well-formed.
|
||||||
|
*
|
||||||
|
* @param str the base64 string representation of a WireGuard key
|
||||||
|
* @return the decoded key encapsulated in an immutable container
|
||||||
|
*/
|
||||||
|
public static Key fromBase64(final String str) throws KeyFormatException {
|
||||||
|
final char[] input = str.toCharArray();
|
||||||
|
if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=')
|
||||||
|
throw new KeyFormatException(Format.BASE64, Type.LENGTH);
|
||||||
|
final byte[] key = new byte[Format.BINARY.length];
|
||||||
|
int i;
|
||||||
|
int ret = 0;
|
||||||
|
for (i = 0; i < key.length / 3; ++i) {
|
||||||
|
final int val = decodeBase64(input, i * 4);
|
||||||
|
ret |= val >>> 31;
|
||||||
|
key[i * 3] = (byte) ((val >>> 16) & 0xff);
|
||||||
|
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
|
||||||
|
key[i * 3 + 2] = (byte) (val & 0xff);
|
||||||
|
}
|
||||||
|
final char[] endSegment = {
|
||||||
|
input[i * 4],
|
||||||
|
input[i * 4 + 1],
|
||||||
|
input[i * 4 + 2],
|
||||||
|
'A',
|
||||||
|
};
|
||||||
|
final int val = decodeBase64(endSegment, 0);
|
||||||
|
ret |= (val >>> 31) | (val & 0xff);
|
||||||
|
key[i * 3] = (byte) ((val >>> 16) & 0xff);
|
||||||
|
key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
|
||||||
|
|
||||||
|
if (ret != 0)
|
||||||
|
throw new KeyFormatException(Format.BASE64, Type.CONTENTS);
|
||||||
|
return new Key(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a WireGuard public or private key in an immutable container. This function throws a
|
||||||
|
* {@link KeyFormatException} if the source data is not the correct length.
|
||||||
|
*
|
||||||
|
* @param bytes an array of bytes containing a WireGuard key in binary format
|
||||||
|
* @return the key encapsulated in an immutable container
|
||||||
|
*/
|
||||||
|
public static Key fromBytes(final byte[] bytes) throws KeyFormatException {
|
||||||
|
if (bytes.length != Format.BINARY.length)
|
||||||
|
throw new KeyFormatException(Format.BINARY, Type.LENGTH);
|
||||||
|
return new Key(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a WireGuard public or private key from its hexadecimal string representation. This
|
||||||
|
* function throws a {@link KeyFormatException} if the source string is not well-formed.
|
||||||
|
*
|
||||||
|
* @param str the hexadecimal string representation of a WireGuard key
|
||||||
|
* @return the decoded key encapsulated in an immutable container
|
||||||
|
*/
|
||||||
|
public static Key fromHex(final String str) throws KeyFormatException {
|
||||||
|
final char[] input = str.toCharArray();
|
||||||
|
if (input.length != Format.HEX.length)
|
||||||
|
throw new KeyFormatException(Format.HEX, Type.LENGTH);
|
||||||
|
final byte[] key = new byte[Format.BINARY.length];
|
||||||
|
int ret = 0;
|
||||||
|
for (int i = 0; i < key.length; ++i) {
|
||||||
|
int c;
|
||||||
|
int cNum;
|
||||||
|
int cNum0;
|
||||||
|
int cAlpha;
|
||||||
|
int cAlpha0;
|
||||||
|
int cVal;
|
||||||
|
final int cAcc;
|
||||||
|
|
||||||
|
c = input[i * 2];
|
||||||
|
cNum = c ^ 48;
|
||||||
|
cNum0 = ((cNum - 10) >>> 8) & 0xff;
|
||||||
|
cAlpha = (c & ~32) - 55;
|
||||||
|
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
|
||||||
|
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
|
||||||
|
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
|
||||||
|
cAcc = cVal * 16;
|
||||||
|
|
||||||
|
c = input[i * 2 + 1];
|
||||||
|
cNum = c ^ 48;
|
||||||
|
cNum0 = ((cNum - 10) >>> 8) & 0xff;
|
||||||
|
cAlpha = (c & ~32) - 55;
|
||||||
|
cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
|
||||||
|
ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
|
||||||
|
cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
|
||||||
|
key[i] = (byte) (cAcc | cVal);
|
||||||
|
}
|
||||||
|
if (ret != 0)
|
||||||
|
throw new KeyFormatException(Format.HEX, Type.CONTENTS);
|
||||||
|
return new Key(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a private key using the system's {@link SecureRandom} number generator.
|
||||||
|
*
|
||||||
|
* @return a well-formed random private key
|
||||||
|
*/
|
||||||
|
static Key generatePrivateKey() {
|
||||||
|
final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
final byte[] privateKey = new byte[Format.BINARY.getLength()];
|
||||||
|
secureRandom.nextBytes(privateKey);
|
||||||
|
privateKey[0] &= 248;
|
||||||
|
privateKey[31] &= 127;
|
||||||
|
privateKey[31] |= 64;
|
||||||
|
return new Key(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a public key from an existing private key.
|
||||||
|
*
|
||||||
|
* @param privateKey a private key
|
||||||
|
* @return a well-formed public key that corresponds to the supplied private key
|
||||||
|
*/
|
||||||
|
static Key generatePublicKey(final Key privateKey) {
|
||||||
|
final byte[] publicKey = new byte[Format.BINARY.getLength()];
|
||||||
|
Curve25519.eval(publicKey, 0, privateKey.getBytes(), null);
|
||||||
|
return new Key(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object obj) {
|
||||||
|
if (obj == this)
|
||||||
|
return true;
|
||||||
|
if (obj == null || obj.getClass() != getClass())
|
||||||
|
return false;
|
||||||
|
final Key other = (Key) obj;
|
||||||
|
return MessageDigest.isEqual(key, other.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the key as an array of bytes.
|
||||||
|
*
|
||||||
|
* @return an array of bytes containing the raw binary key
|
||||||
|
*/
|
||||||
|
public byte[] getBytes() {
|
||||||
|
// Defensively copy to ensure immutability.
|
||||||
|
return Arrays.copyOf(key, key.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int ret = 0;
|
||||||
|
for (int i = 0; i < key.length / 4; ++i)
|
||||||
|
ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the key to base64.
|
||||||
|
*
|
||||||
|
* @return a string containing the encoded key
|
||||||
|
*/
|
||||||
|
public String toBase64() {
|
||||||
|
final char[] output = new char[Format.BASE64.length];
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < key.length / 3; ++i)
|
||||||
|
encodeBase64(key, i * 3, output, i * 4);
|
||||||
|
final byte[] endSegment = {
|
||||||
|
key[i * 3],
|
||||||
|
key[i * 3 + 1],
|
||||||
|
0,
|
||||||
|
};
|
||||||
|
encodeBase64(endSegment, 0, output, i * 4);
|
||||||
|
output[Format.BASE64.length - 1] = '=';
|
||||||
|
return new String(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the key to hexadecimal ASCII characters.
|
||||||
|
*
|
||||||
|
* @return a string containing the encoded key
|
||||||
|
*/
|
||||||
|
public String toHex() {
|
||||||
|
final char[] output = new char[Format.HEX.length];
|
||||||
|
for (int i = 0; i < key.length; ++i) {
|
||||||
|
output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf)
|
||||||
|
+ ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38));
|
||||||
|
output[i * 2 + 1] = (char) (87 + (key[i] & 0xf)
|
||||||
|
+ ((((key[i] & 0xf) - 10) >> 8) & ~38));
|
||||||
|
}
|
||||||
|
return new String(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The supported formats for encoding a WireGuard key.
|
||||||
|
*/
|
||||||
|
public enum Format {
|
||||||
|
BASE64(44),
|
||||||
|
BINARY(32),
|
||||||
|
HEX(64);
|
||||||
|
|
||||||
|
private final int length;
|
||||||
|
|
||||||
|
Format(final int length) {
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.crypto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception thrown when attempting to parse an invalid key (too short, too long, or byte
|
||||||
|
* data inappropriate for the format). The format being parsed can be accessed with the
|
||||||
|
* {@link #getFormat} method.
|
||||||
|
*/
|
||||||
|
public final class KeyFormatException extends Exception {
|
||||||
|
private final Key.Format format;
|
||||||
|
private final Type type;
|
||||||
|
|
||||||
|
KeyFormatException(final Key.Format format, final Type type) {
|
||||||
|
this.format = format;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Key.Format getFormat() {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
CONTENTS,
|
||||||
|
LENGTH
|
||||||
|
}
|
||||||
|
}
|
||||||
51
app/src/main/java/com/wireguard/crypto/KeyPair.java
Normal file
51
app/src/main/java/com/wireguard/crypto/KeyPair.java
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.crypto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Curve25519 key pair as used by WireGuard.
|
||||||
|
* <p>
|
||||||
|
* Instances of this class are immutable.
|
||||||
|
*/
|
||||||
|
public class KeyPair {
|
||||||
|
private final Key privateKey;
|
||||||
|
private final Key publicKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a key pair using a newly-generated private key.
|
||||||
|
*/
|
||||||
|
public KeyPair() {
|
||||||
|
this(Key.generatePrivateKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a key pair using an existing private key.
|
||||||
|
*
|
||||||
|
* @param privateKey a private key, used to derive the public key
|
||||||
|
*/
|
||||||
|
public KeyPair(final Key privateKey) {
|
||||||
|
this.privateKey = privateKey;
|
||||||
|
publicKey = Key.generatePublicKey(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the private key from the key pair.
|
||||||
|
*
|
||||||
|
* @return the private key
|
||||||
|
*/
|
||||||
|
public Key getPrivateKey() {
|
||||||
|
return privateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the public key from the key pair.
|
||||||
|
*
|
||||||
|
* @return the public key
|
||||||
|
*/
|
||||||
|
public Key getPublicKey() {
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
182
app/src/main/java/io/nekohasekai/sagernet/Constants.kt
Normal file
182
app/src/main/java/io/nekohasekai/sagernet/Constants.kt
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package io.nekohasekai.sagernet
|
||||||
|
|
||||||
|
const val CONNECTION_TEST_URL = "http://cp.cloudflare.com/"
|
||||||
|
|
||||||
|
object Key {
|
||||||
|
|
||||||
|
const val DB_PUBLIC = "configuration.db"
|
||||||
|
const val DB_PROFILE = "sager_net.db"
|
||||||
|
|
||||||
|
const val APP_EXPERT = "isExpert"
|
||||||
|
const val APP_THEME = "appTheme"
|
||||||
|
const val NIGHT_THEME = "nightTheme"
|
||||||
|
const val SERVICE_MODE = "serviceMode"
|
||||||
|
const val MODE_VPN = "vpn"
|
||||||
|
const val MODE_PROXY = "proxy"
|
||||||
|
|
||||||
|
const val REMOTE_DNS = "remoteDns"
|
||||||
|
const val DIRECT_DNS = "directDns"
|
||||||
|
const val DIRECT_DNS_USE_SYSTEM = "directDnsUseSystem"
|
||||||
|
const val ENABLE_DNS_ROUTING = "enableDnsRouting"
|
||||||
|
const val ENABLE_FAKEDNS = "enableFakeDns"
|
||||||
|
const val DNS_NETWORK = "dnsNetwork"
|
||||||
|
|
||||||
|
const val IPV6_MODE = "ipv6Mode"
|
||||||
|
|
||||||
|
const val PROXY_APPS = "proxyApps"
|
||||||
|
const val BYPASS_MODE = "bypassMode"
|
||||||
|
const val INDIVIDUAL = "individual"
|
||||||
|
const val METERED_NETWORK = "meteredNetwork"
|
||||||
|
|
||||||
|
const val DOMAIN_STRATEGY = "domainStrategy"
|
||||||
|
const val TRAFFIC_SNIFFING = "trafficSniffing"
|
||||||
|
const val RESOLVE_DESTINATION = "resolveDestination"
|
||||||
|
|
||||||
|
const val BYPASS_LAN = "bypassLan"
|
||||||
|
const val BYPASS_LAN_IN_CORE_ONLY = "bypassLanInCoreOnly"
|
||||||
|
|
||||||
|
const val MIXED_PORT = "mixedPort"
|
||||||
|
const val ALLOW_ACCESS = "allowAccess"
|
||||||
|
const val SPEED_INTERVAL = "speedInterval"
|
||||||
|
const val SHOW_DIRECT_SPEED = "showDirectSpeed"
|
||||||
|
const val LOCAL_DNS_PORT = "portLocalDns"
|
||||||
|
|
||||||
|
const val APPEND_HTTP_PROXY = "appendHttpProxy"
|
||||||
|
|
||||||
|
const val REQUIRE_TRANSPROXY = "requireTransproxy"
|
||||||
|
const val TRANSPROXY_MODE = "transproxyMode"
|
||||||
|
const val TRANSPROXY_PORT = "transproxyPort"
|
||||||
|
const val CONNECTION_TEST_URL = "connectionTestURL"
|
||||||
|
|
||||||
|
const val TCP_KEEP_ALIVE_INTERVAL = "tcpKeepAliveInterval"
|
||||||
|
const val RULES_PROVIDER = "rulesProvider"
|
||||||
|
const val ENABLE_LOG = "enableLog"
|
||||||
|
const val LOG_BUF_SIZE = "logBufSize"
|
||||||
|
const val MTU = "mtu"
|
||||||
|
const val ALWAYS_SHOW_ADDRESS = "alwaysShowAddress"
|
||||||
|
|
||||||
|
// Protocol Settings
|
||||||
|
const val MUX_PROTOCOLS = "mux"
|
||||||
|
const val MUX_CONCURRENCY = "muxConcurrency"
|
||||||
|
|
||||||
|
const val ACQUIRE_WAKE_LOCK = "acquireWakeLock"
|
||||||
|
const val SHOW_BOTTOM_BAR = "showBottomBar"
|
||||||
|
|
||||||
|
const val TUN_IMPLEMENTATION = "tunImplementation"
|
||||||
|
const val PROFILE_TRAFFIC_STATISTICS = "profileTrafficStatistics"
|
||||||
|
|
||||||
|
const val PROFILE_DIRTY = "profileDirty"
|
||||||
|
const val PROFILE_ID = "profileId"
|
||||||
|
const val PROFILE_NAME = "profileName"
|
||||||
|
const val PROFILE_GROUP = "profileGroup"
|
||||||
|
const val PROFILE_CURRENT = "profileCurrent"
|
||||||
|
|
||||||
|
const val SERVER_ADDRESS = "serverAddress"
|
||||||
|
const val SERVER_PORT = "serverPort"
|
||||||
|
const val SERVER_USERNAME = "serverUsername"
|
||||||
|
const val SERVER_PASSWORD = "serverPassword"
|
||||||
|
const val SERVER_METHOD = "serverMethod"
|
||||||
|
const val SERVER_PLUGIN = "serverPlugin"
|
||||||
|
const val SERVER_PLUGIN_CONFIGURE = "serverPluginConfigure"
|
||||||
|
const val SERVER_PASSWORD1 = "serverPassword1"
|
||||||
|
|
||||||
|
const val SERVER_PROTOCOL = "serverProtocol"
|
||||||
|
const val SERVER_OBFS = "serverObfs"
|
||||||
|
|
||||||
|
const val SERVER_SECURITY = "serverSecurity"
|
||||||
|
const val SERVER_NETWORK = "serverNetwork"
|
||||||
|
const val SERVER_HOST = "serverHost"
|
||||||
|
const val SERVER_PATH = "serverPath"
|
||||||
|
const val SERVER_SNI = "serverSNI"
|
||||||
|
const val SERVER_ENCRYPTION = "serverEncryption"
|
||||||
|
const val SERVER_ALPN = "serverALPN"
|
||||||
|
const val SERVER_CERTIFICATES = "serverCertificates"
|
||||||
|
const val SERVER_CONFIG = "serverConfig"
|
||||||
|
|
||||||
|
const val SERVER_SECURITY_CATEGORY = "serverSecurityCategory"
|
||||||
|
const val SERVER_WS_CATEGORY = "serverWsCategory"
|
||||||
|
const val SERVER_SS_CATEGORY = "serverSsCategory"
|
||||||
|
const val SERVER_HEADERS = "serverHeaders"
|
||||||
|
const val SERVER_ALLOW_INSECURE = "serverAllowInsecure"
|
||||||
|
|
||||||
|
const val SERVER_AUTH_TYPE = "serverAuthType"
|
||||||
|
const val SERVER_UPLOAD_SPEED = "serverUploadSpeed"
|
||||||
|
const val SERVER_DOWNLOAD_SPEED = "serverDownloadSpeed"
|
||||||
|
const val SERVER_STREAM_RECEIVE_WINDOW = "serverStreamReceiveWindow"
|
||||||
|
const val SERVER_CONNECTION_RECEIVE_WINDOW = "serverConnectionReceiveWindow"
|
||||||
|
const val SERVER_MTU = "serverMTU"
|
||||||
|
const val SERVER_DISABLE_MTU_DISCOVERY = "serverDisableMtuDiscovery"
|
||||||
|
const val SERVER_HOP_INTERVAL = "hopInterval"
|
||||||
|
|
||||||
|
const val SERVER_PRIVATE_KEY = "serverPrivateKey"
|
||||||
|
const val SERVER_LOCAL_ADDRESS = "serverLocalAddress"
|
||||||
|
const val SERVER_INSECURE_CONCURRENCY = "serverInsecureConcurrency"
|
||||||
|
|
||||||
|
const val SERVER_UDP_RELAY_MODE = "serverUDPRelayMode"
|
||||||
|
const val SERVER_CONGESTION_CONTROLLER = "serverCongestionController"
|
||||||
|
const val SERVER_DISABLE_SNI = "serverDisableSNI"
|
||||||
|
const val SERVER_REDUCE_RTT = "serverReduceRTT"
|
||||||
|
const val SERVER_FAST_CONNECT = "serverFastConnect"
|
||||||
|
|
||||||
|
const val ROUTE_NAME = "routeName"
|
||||||
|
const val ROUTE_DOMAIN = "routeDomain"
|
||||||
|
const val ROUTE_IP = "routeIP"
|
||||||
|
const val ROUTE_PORT = "routePort"
|
||||||
|
const val ROUTE_SOURCE_PORT = "routeSourcePort"
|
||||||
|
const val ROUTE_NETWORK = "routeNetwork"
|
||||||
|
const val ROUTE_SOURCE = "routeSource"
|
||||||
|
const val ROUTE_PROTOCOL = "routeProtocol"
|
||||||
|
const val ROUTE_OUTBOUND = "routeOutbound"
|
||||||
|
const val ROUTE_OUTBOUND_RULE = "routeOutboundRule"
|
||||||
|
const val ROUTE_PACKAGES = "routePackages"
|
||||||
|
|
||||||
|
const val GROUP_NAME = "groupName"
|
||||||
|
const val GROUP_TYPE = "groupType"
|
||||||
|
const val GROUP_ORDER = "groupOrder"
|
||||||
|
|
||||||
|
const val GROUP_SUBSCRIPTION = "groupSubscription"
|
||||||
|
const val SUBSCRIPTION_LINK = "subscriptionLink"
|
||||||
|
const val SUBSCRIPTION_FORCE_RESOLVE = "subscriptionForceResolve"
|
||||||
|
const val SUBSCRIPTION_DEDUPLICATION = "subscriptionDeduplication"
|
||||||
|
const val SUBSCRIPTION_UPDATE = "subscriptionUpdate"
|
||||||
|
const val SUBSCRIPTION_UPDATE_WHEN_CONNECTED_ONLY = "subscriptionUpdateWhenConnectedOnly"
|
||||||
|
const val SUBSCRIPTION_USER_AGENT = "subscriptionUserAgent"
|
||||||
|
const val SUBSCRIPTION_AUTO_UPDATE = "subscriptionAutoUpdate"
|
||||||
|
const val SUBSCRIPTION_AUTO_UPDATE_DELAY = "subscriptionAutoUpdateDelay"
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
const val NEKO_PLUGIN_MANAGED = "nekoPlugins"
|
||||||
|
const val APP_TLS_VERSION = "appTLSVersion"
|
||||||
|
const val ENABLE_CLASH_API = "enableClashAPI"
|
||||||
|
}
|
||||||
|
|
||||||
|
object TunImplementation {
|
||||||
|
const val GVISOR = 0
|
||||||
|
const val SYSTEM = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
object IPv6Mode {
|
||||||
|
const val DISABLE = 0
|
||||||
|
const val ENABLE = 1
|
||||||
|
const val PREFER = 2
|
||||||
|
const val ONLY = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
object GroupType {
|
||||||
|
const val BASIC = 0
|
||||||
|
const val SUBSCRIPTION = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
object GroupOrder {
|
||||||
|
const val ORIGIN = 0
|
||||||
|
const val BY_NAME = 1
|
||||||
|
const val BY_DELAY = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
object Action {
|
||||||
|
const val SERVICE = "io.nekohasekai.sagernet.SERVICE"
|
||||||
|
const val CLOSE = "io.nekohasekai.sagernet.CLOSE"
|
||||||
|
const val RELOAD = "io.nekohasekai.sagernet.RELOAD"
|
||||||
|
const val SWITCH_WAKE_LOCK = "io.nekohasekai.sagernet.SWITCH_WAKELOCK"
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 io.nekohasekai.sagernet
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ShortcutManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.core.content.pm.ShortcutInfoCompat
|
||||||
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import io.nekohasekai.sagernet.aidl.ISagerNetService
|
||||||
|
import io.nekohasekai.sagernet.bg.BaseService
|
||||||
|
import io.nekohasekai.sagernet.bg.SagerConnection
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
class QuickToggleShortcut : Activity(), SagerConnection.Callback {
|
||||||
|
private val connection = SagerConnection()
|
||||||
|
private var profileId = -1L
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
if (intent.action == Intent.ACTION_CREATE_SHORTCUT) {
|
||||||
|
setResult(RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this,
|
||||||
|
ShortcutInfoCompat.Builder(this, "toggle")
|
||||||
|
.setIntent(Intent(this,
|
||||||
|
QuickToggleShortcut::class.java).setAction(Intent.ACTION_MAIN))
|
||||||
|
.setIcon(IconCompat.createWithResource(this,
|
||||||
|
R.drawable.ic_qu_shadowsocks_launcher))
|
||||||
|
.setShortLabel(getString(R.string.quick_toggle))
|
||||||
|
.build()))
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
profileId = intent.getLongExtra("profile", -1L)
|
||||||
|
connection.connect(this, this)
|
||||||
|
if (Build.VERSION.SDK_INT >= 25) {
|
||||||
|
getSystemService<ShortcutManager>()!!.reportShortcutUsed(if (profileId >= 0) "shortcut-profile-$profileId" else "toggle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceConnected(service: ISagerNetService) {
|
||||||
|
val state = BaseService.State.values()[service.state]
|
||||||
|
when {
|
||||||
|
state.canStop -> {
|
||||||
|
if (profileId == DataStore.selectedProxy || profileId == -1L) {
|
||||||
|
SagerNet.stopService()
|
||||||
|
} else {
|
||||||
|
DataStore.selectedProxy = profileId
|
||||||
|
SagerNet.reloadService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state == BaseService.State.Stopped -> {
|
||||||
|
if (profileId >= 0L) DataStore.selectedProxy = profileId
|
||||||
|
SagerNet.startService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
connection.disconnect(this)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
283
app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt
Normal file
283
app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
package io.nekohasekai.sagernet
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.*
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
|
import android.os.StrictMode
|
||||||
|
import android.os.UserManager
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import go.Seq
|
||||||
|
import io.nekohasekai.sagernet.bg.SagerConnection
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.ktx.Logs
|
||||||
|
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
|
||||||
|
import io.nekohasekai.sagernet.ui.MainActivity
|
||||||
|
import io.nekohasekai.sagernet.utils.*
|
||||||
|
import kotlinx.coroutines.DEBUG_PROPERTY_NAME
|
||||||
|
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
|
||||||
|
import libcore.BoxPlatformInterface
|
||||||
|
import libcore.Libcore
|
||||||
|
import libcore.NB4AInterface
|
||||||
|
import moe.matsuri.nb4a.utils.JavaUtil
|
||||||
|
import moe.matsuri.nb4a.utils.cleanWebview
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import androidx.work.Configuration as WorkConfiguration
|
||||||
|
|
||||||
|
class SagerNet : Application(),
|
||||||
|
BoxPlatformInterface,
|
||||||
|
WorkConfiguration.Provider, NB4AInterface {
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
|
||||||
|
application = this
|
||||||
|
}
|
||||||
|
|
||||||
|
val externalAssets by lazy { getExternalFilesDir(null) ?: filesDir }
|
||||||
|
val process = JavaUtil.getProcessName()
|
||||||
|
val isMainProcess = process == BuildConfig.APPLICATION_ID
|
||||||
|
val isBgProcess = process.endsWith(":bg")
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(CrashHandler)
|
||||||
|
|
||||||
|
if (isMainProcess || isBgProcess) {
|
||||||
|
// fix multi process issue in Android 9+
|
||||||
|
JavaUtil.handleWebviewDir(this)
|
||||||
|
|
||||||
|
runOnDefaultDispatcher {
|
||||||
|
PackageCache.register()
|
||||||
|
cleanWebview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Seq.setContext(this)
|
||||||
|
updateNotificationChannels()
|
||||||
|
|
||||||
|
// nb4a: init core
|
||||||
|
externalAssets.mkdirs()
|
||||||
|
Libcore.initCore(
|
||||||
|
process,
|
||||||
|
cacheDir.absolutePath + "/",
|
||||||
|
filesDir.absolutePath + "/",
|
||||||
|
externalAssets.absolutePath + "/",
|
||||||
|
DataStore.logBufSize,
|
||||||
|
DataStore.enableLog,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
|
||||||
|
// libbox: platform interface
|
||||||
|
Libcore.setBoxPlatformInterface(this)
|
||||||
|
|
||||||
|
if (isMainProcess) {
|
||||||
|
Theme.apply(this)
|
||||||
|
Theme.applyNightTheme()
|
||||||
|
runOnDefaultDispatcher {
|
||||||
|
DefaultNetworkListener.start(this) {
|
||||||
|
underlyingNetwork = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) StrictMode.setVmPolicy(
|
||||||
|
StrictMode.VmPolicy.Builder()
|
||||||
|
.detectLeakedSqlLiteObjects()
|
||||||
|
.detectLeakedClosableObjects()
|
||||||
|
.detectLeakedRegistrationObjects()
|
||||||
|
.penaltyLog()
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(
|
||||||
|
packageName, if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
|
else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES
|
||||||
|
)!!
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
updateNotificationChannels()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getWorkManagerConfiguration(): WorkConfiguration {
|
||||||
|
return WorkConfiguration.Builder()
|
||||||
|
.setDefaultProcessName("${BuildConfig.APPLICATION_ID}:bg")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTrimMemory(level: Int) {
|
||||||
|
super.onTrimMemory(level)
|
||||||
|
|
||||||
|
Libcore.forceGc()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
lateinit var application: SagerNet
|
||||||
|
|
||||||
|
val isTv by lazy {
|
||||||
|
uiMode.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
|
||||||
|
}
|
||||||
|
|
||||||
|
// /data/user_de available when not unlocked
|
||||||
|
val deviceStorage by lazy {
|
||||||
|
if (Build.VERSION.SDK_INT < 24) application else DeviceStorageApp(application)
|
||||||
|
}
|
||||||
|
|
||||||
|
val configureIntent: (Context) -> PendingIntent by lazy {
|
||||||
|
{
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
it,
|
||||||
|
0,
|
||||||
|
Intent(
|
||||||
|
application, MainActivity::class.java
|
||||||
|
).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val activity by lazy { application.getSystemService<ActivityManager>()!! }
|
||||||
|
val clipboard by lazy { application.getSystemService<ClipboardManager>()!! }
|
||||||
|
val connectivity by lazy { application.getSystemService<ConnectivityManager>()!! }
|
||||||
|
val notification by lazy { application.getSystemService<NotificationManager>()!! }
|
||||||
|
val user by lazy { application.getSystemService<UserManager>()!! }
|
||||||
|
val uiMode by lazy { application.getSystemService<UiModeManager>()!! }
|
||||||
|
val power by lazy { application.getSystemService<PowerManager>()!! }
|
||||||
|
|
||||||
|
val packageInfo: PackageInfo by lazy { application.getPackageInfo(application.packageName) }
|
||||||
|
|
||||||
|
fun getClipboardText(): String {
|
||||||
|
return clipboard.primaryClip?.takeIf { it.itemCount > 0 }
|
||||||
|
?.getItemAt(0)?.text?.toString() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trySetPrimaryClip(clip: String) = try {
|
||||||
|
clipboard.setPrimaryClip(ClipData.newPlainText(null, clip))
|
||||||
|
true
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
Logs.w(e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateNotificationChannels() {
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
|
||||||
|
notification.createNotificationChannels(
|
||||||
|
listOf(
|
||||||
|
NotificationChannel(
|
||||||
|
"service-vpn",
|
||||||
|
application.getText(R.string.service_vpn),
|
||||||
|
if (Build.VERSION.SDK_INT >= 28) NotificationManager.IMPORTANCE_MIN
|
||||||
|
else NotificationManager.IMPORTANCE_LOW
|
||||||
|
), // #1355
|
||||||
|
NotificationChannel(
|
||||||
|
"service-proxy",
|
||||||
|
application.getText(R.string.service_proxy),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
), NotificationChannel(
|
||||||
|
"service-subscription",
|
||||||
|
application.getText(R.string.service_subscription),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startService() = ContextCompat.startForegroundService(
|
||||||
|
application, Intent(application, SagerConnection.serviceClass)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun reloadService() =
|
||||||
|
application.sendBroadcast(Intent(Action.RELOAD).setPackage(application.packageName))
|
||||||
|
|
||||||
|
fun stopService() =
|
||||||
|
application.sendBroadcast(Intent(Action.CLOSE).setPackage(application.packageName))
|
||||||
|
|
||||||
|
var underlyingNetwork: Network? = null
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// libbox interface
|
||||||
|
|
||||||
|
override fun autoDetectInterfaceControl(fd: Int) {
|
||||||
|
DataStore.vpnService?.protect(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openTun(singTunOptionsJson: String, tunPlatformOptionsJson: String): Long {
|
||||||
|
if (DataStore.vpnService == null) {
|
||||||
|
throw Exception("no VpnService")
|
||||||
|
}
|
||||||
|
return DataStore.vpnService!!.startVpn(singTunOptionsJson, tunPlatformOptionsJson).toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun useProcFS(): Boolean {
|
||||||
|
return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
override fun findConnectionOwner(
|
||||||
|
ipProto: Int, srcIp: String, srcPort: Int, destIp: String, destPort: Int
|
||||||
|
): Int {
|
||||||
|
return connectivity.getConnectionOwnerUid(
|
||||||
|
ipProto, InetSocketAddress(srcIp, srcPort), InetSocketAddress(destIp, destPort)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun packageNameByUid(uid: Int): String {
|
||||||
|
PackageCache.awaitLoadSync()
|
||||||
|
|
||||||
|
if (uid <= 1000L) {
|
||||||
|
return "android"
|
||||||
|
}
|
||||||
|
|
||||||
|
val packageNames = PackageCache.uidMap[uid]
|
||||||
|
if (!packageNames.isNullOrEmpty()) for (packageName in packageNames) {
|
||||||
|
return packageName
|
||||||
|
}
|
||||||
|
|
||||||
|
error("unknown uid $uid")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun uidByPackageName(packageName: String): Int {
|
||||||
|
PackageCache.awaitLoadSync()
|
||||||
|
return PackageCache[packageName] ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// nb4a interface
|
||||||
|
|
||||||
|
override fun write(p: ByteArray): Long {
|
||||||
|
// NB4AGuiLogWriter
|
||||||
|
if (isBgProcess) {
|
||||||
|
runOnDefaultDispatcher {
|
||||||
|
DataStore.baseService?.data?.binder?.broadcast {
|
||||||
|
it.cbLogUpdate(String(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DataStore.postLogListener?.let { it(String(p)) }
|
||||||
|
}
|
||||||
|
return p.size.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun useOfficialAssets(): Boolean {
|
||||||
|
return DataStore.rulesProvider == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package io.nekohasekai.sagernet.aidl
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class SpeedDisplayData(
|
||||||
|
// Bytes per second
|
||||||
|
var txRateProxy: Long = 0L,
|
||||||
|
var rxRateProxy: Long = 0L,
|
||||||
|
var txRateDirect: Long = 0L,
|
||||||
|
var rxRateDirect: Long = 0L,
|
||||||
|
|
||||||
|
// Bytes for the current session
|
||||||
|
// Outbound "bypass" usage is not counted
|
||||||
|
var txTotal: Long = 0L,
|
||||||
|
var rxTotal: Long = 0L,
|
||||||
|
) : Parcelable
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package io.nekohasekai.sagernet.aidl
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class TrafficData(
|
||||||
|
var id: Long = 0L,
|
||||||
|
var tx: Long = 0L,
|
||||||
|
var rx: Long = 0L,
|
||||||
|
) : Parcelable
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg
|
||||||
|
|
||||||
|
import java.io.Closeable
|
||||||
|
|
||||||
|
interface AbstractInstance : Closeable {
|
||||||
|
|
||||||
|
fun launch()
|
||||||
|
|
||||||
|
}
|
||||||
345
app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
Normal file
345
app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.*
|
||||||
|
import android.widget.Toast
|
||||||
|
import io.nekohasekai.sagernet.Action
|
||||||
|
import io.nekohasekai.sagernet.R
|
||||||
|
import io.nekohasekai.sagernet.SagerNet
|
||||||
|
import io.nekohasekai.sagernet.aidl.ISagerNetService
|
||||||
|
import io.nekohasekai.sagernet.aidl.ISagerNetServiceCallback
|
||||||
|
import io.nekohasekai.sagernet.bg.proto.ProxyInstance
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.database.SagerDatabase
|
||||||
|
import io.nekohasekai.sagernet.ktx.*
|
||||||
|
import io.nekohasekai.sagernet.plugin.PluginManager
|
||||||
|
import io.nekohasekai.sagernet.utils.DefaultNetworkListener
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import libcore.Libcore
|
||||||
|
import moe.matsuri.nb4a.Protocols
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
|
||||||
|
class BaseService {
|
||||||
|
|
||||||
|
enum class State(
|
||||||
|
val canStop: Boolean = false,
|
||||||
|
val started: Boolean = false,
|
||||||
|
val connected: Boolean = false,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Idle state is only used by UI and will never be returned by BaseService.
|
||||||
|
*/
|
||||||
|
Idle, Connecting(true, true, false), Connected(true, true, true), Stopping, Stopped,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExpectedException
|
||||||
|
|
||||||
|
class Data internal constructor(private val service: Interface) {
|
||||||
|
var state = State.Stopped
|
||||||
|
var proxy: ProxyInstance? = null
|
||||||
|
var notification: ServiceNotification? = null
|
||||||
|
|
||||||
|
val receiver = broadcastReceiver { _, intent ->
|
||||||
|
when (intent.action) {
|
||||||
|
Intent.ACTION_SHUTDOWN -> service.persistStats()
|
||||||
|
Action.RELOAD -> service.forceLoad()
|
||||||
|
Action.SWITCH_WAKE_LOCK -> runOnDefaultDispatcher { service.switchWakeLock() }
|
||||||
|
else -> service.stopRunner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var closeReceiverRegistered = false
|
||||||
|
|
||||||
|
val binder = Binder(this)
|
||||||
|
var connectingJob: Job? = null
|
||||||
|
|
||||||
|
fun changeState(s: State, msg: String? = null) {
|
||||||
|
if (state == s && msg == null) return
|
||||||
|
binder.stateChanged(s, msg)
|
||||||
|
state = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Binder(private var data: Data? = null) : ISagerNetService.Stub(), CoroutineScope,
|
||||||
|
AutoCloseable {
|
||||||
|
private val callbacks = object : RemoteCallbackList<ISagerNetServiceCallback>() {
|
||||||
|
override fun onCallbackDied(callback: ISagerNetServiceCallback?, cookie: Any?) {
|
||||||
|
super.onCallbackDied(callback, cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val coroutineContext = Dispatchers.Main.immediate + Job()
|
||||||
|
|
||||||
|
override fun getState(): Int = (data?.state ?: State.Idle).ordinal
|
||||||
|
override fun getProfileName(): String = data?.proxy?.profile?.displayName() ?: "Idle"
|
||||||
|
|
||||||
|
override fun registerCallback(cb: ISagerNetServiceCallback) {
|
||||||
|
callbacks.register(cb)
|
||||||
|
cb.updateWakeLockStatus(data?.proxy?.service?.wakeLock != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val boardcastMutex = Mutex()
|
||||||
|
|
||||||
|
suspend fun broadcast(work: (ISagerNetServiceCallback) -> Unit) {
|
||||||
|
boardcastMutex.withLock {
|
||||||
|
val count = callbacks.beginBroadcast()
|
||||||
|
try {
|
||||||
|
repeat(count) {
|
||||||
|
try {
|
||||||
|
work(callbacks.getBroadcastItem(it))
|
||||||
|
} catch (_: RemoteException) {
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
callbacks.finishBroadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregisterCallback(cb: ISagerNetServiceCallback) {
|
||||||
|
callbacks.unregister(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun urlTest(): Int {
|
||||||
|
if (data?.proxy?.box == null) {
|
||||||
|
error("core not started")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Libcore.urlTest(
|
||||||
|
data!!.proxy!!.box, DataStore.connectionTestURL, 3000
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
error(Protocols.genFriendlyMsg(e.readableMessage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stateChanged(s: State, msg: String?) = launch {
|
||||||
|
val profileName = profileName
|
||||||
|
broadcast { it.stateChanged(s.ordinal, profileName, msg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun missingPlugin(pluginName: String) = launch {
|
||||||
|
val profileName = profileName
|
||||||
|
broadcast { it.missingPlugin(profileName, pluginName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
callbacks.kill()
|
||||||
|
cancel()
|
||||||
|
data = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Interface {
|
||||||
|
val data: Data
|
||||||
|
val tag: String
|
||||||
|
fun createNotification(profileName: String): ServiceNotification
|
||||||
|
|
||||||
|
fun onBind(intent: Intent): IBinder? =
|
||||||
|
if (intent.action == Action.SERVICE) data.binder else null
|
||||||
|
|
||||||
|
fun forceLoad() {
|
||||||
|
if (DataStore.selectedProxy == 0L) {
|
||||||
|
stopRunner(false, (this as Context).getString(R.string.profile_empty))
|
||||||
|
}
|
||||||
|
val s = data.state
|
||||||
|
when {
|
||||||
|
s == State.Stopped -> startRunner()
|
||||||
|
s.canStop -> stopRunner(true)
|
||||||
|
else -> Logs.w("Illegal state $s when invoking use")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun startProcesses() {
|
||||||
|
data.proxy!!.launch()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startRunner() {
|
||||||
|
this as Context
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) startForegroundService(Intent(this, javaClass))
|
||||||
|
else startService(Intent(this, javaClass))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun killProcesses() {
|
||||||
|
data.proxy?.close()
|
||||||
|
wakeLock?.apply {
|
||||||
|
release()
|
||||||
|
wakeLock = null
|
||||||
|
}
|
||||||
|
runOnDefaultDispatcher {
|
||||||
|
DefaultNetworkListener.stop(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopRunner(restart: Boolean = false, msg: String? = null) {
|
||||||
|
DataStore.baseService = null
|
||||||
|
DataStore.vpnService = null
|
||||||
|
|
||||||
|
if (data.state == State.Stopping) return
|
||||||
|
data.notification?.destroy()
|
||||||
|
data.notification = null
|
||||||
|
this as Service
|
||||||
|
|
||||||
|
data.changeState(State.Stopping)
|
||||||
|
|
||||||
|
runOnMainDispatcher {
|
||||||
|
data.connectingJob?.cancelAndJoin() // ensure stop connecting first
|
||||||
|
// we use a coroutineScope here to allow clean-up in parallel
|
||||||
|
coroutineScope {
|
||||||
|
killProcesses()
|
||||||
|
val data = data
|
||||||
|
if (data.closeReceiverRegistered) {
|
||||||
|
unregisterReceiver(data.receiver)
|
||||||
|
data.closeReceiverRegistered = false
|
||||||
|
}
|
||||||
|
data.proxy = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// change the state
|
||||||
|
data.changeState(State.Stopped, msg)
|
||||||
|
// stop the service if nothing has bound to it
|
||||||
|
if (restart) startRunner() else {
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun persistStats() {
|
||||||
|
// TODO NEW save app stats?
|
||||||
|
}
|
||||||
|
|
||||||
|
// networks
|
||||||
|
var upstreamInterfaceName: String?
|
||||||
|
|
||||||
|
suspend fun preInit() {
|
||||||
|
DefaultNetworkListener.start(this) {
|
||||||
|
SagerNet.connectivity.getLinkProperties(it)?.also { link ->
|
||||||
|
val oldName = upstreamInterfaceName
|
||||||
|
if (oldName != link.interfaceName) {
|
||||||
|
upstreamInterfaceName = link.interfaceName
|
||||||
|
}
|
||||||
|
if (oldName != null && upstreamInterfaceName != null && oldName != upstreamInterfaceName) {
|
||||||
|
Logs.d("Network changed: $oldName -> $upstreamInterfaceName")
|
||||||
|
Libcore.resetAllConnections(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wakeLock: PowerManager.WakeLock?
|
||||||
|
fun acquireWakeLock()
|
||||||
|
suspend fun switchWakeLock() {
|
||||||
|
wakeLock?.apply {
|
||||||
|
release()
|
||||||
|
wakeLock = null
|
||||||
|
data.binder.broadcast {
|
||||||
|
it.updateWakeLockStatus(false)
|
||||||
|
}
|
||||||
|
} ?: apply {
|
||||||
|
acquireWakeLock()
|
||||||
|
data.binder.broadcast {
|
||||||
|
it.updateWakeLockStatus(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun lateInit() {
|
||||||
|
wakeLock?.apply {
|
||||||
|
release()
|
||||||
|
wakeLock = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DataStore.acquireWakeLock) {
|
||||||
|
acquireWakeLock()
|
||||||
|
data.binder.broadcast {
|
||||||
|
it.updateWakeLockStatus(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.binder.broadcast {
|
||||||
|
it.updateWakeLockStatus(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
DataStore.baseService = this
|
||||||
|
|
||||||
|
val data = data
|
||||||
|
if (data.state != State.Stopped) return Service.START_NOT_STICKY
|
||||||
|
val profile = SagerDatabase.proxyDao.getById(DataStore.selectedProxy)
|
||||||
|
this as Context
|
||||||
|
if (profile == null) { // gracefully shutdown: https://stackoverflow.com/q/47337857/2245107
|
||||||
|
data.notification = createNotification("")
|
||||||
|
stopRunner(false, getString(R.string.profile_empty))
|
||||||
|
return Service.START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
val proxy = ProxyInstance(profile, this)
|
||||||
|
data.proxy = proxy
|
||||||
|
if (!data.closeReceiverRegistered) {
|
||||||
|
registerReceiver(data.receiver, IntentFilter().apply {
|
||||||
|
addAction(Action.RELOAD)
|
||||||
|
addAction(Intent.ACTION_SHUTDOWN)
|
||||||
|
addAction(Action.CLOSE)
|
||||||
|
addAction(Action.SWITCH_WAKE_LOCK)
|
||||||
|
}, "$packageName.SERVICE", null)
|
||||||
|
data.closeReceiverRegistered = true
|
||||||
|
}
|
||||||
|
|
||||||
|
data.changeState(State.Connecting)
|
||||||
|
runOnMainDispatcher {
|
||||||
|
try {
|
||||||
|
data.notification = createNotification(profile.displayName())
|
||||||
|
|
||||||
|
Executable.killAll() // clean up old processes
|
||||||
|
preInit()
|
||||||
|
proxy.init()
|
||||||
|
DataStore.currentProfile = profile.id
|
||||||
|
|
||||||
|
proxy.processes = GuardedProcessPool {
|
||||||
|
Logs.w(it)
|
||||||
|
stopRunner(false, it.readableMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
startProcesses()
|
||||||
|
data.changeState(State.Connected)
|
||||||
|
|
||||||
|
for ((type, routeName) in proxy.config.alerts) {
|
||||||
|
data.binder.broadcast {
|
||||||
|
it.routeAlert(type, routeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lateInit()
|
||||||
|
} catch (_: CancellationException) { // if the job was cancelled, it is canceller's responsibility to call stopRunner
|
||||||
|
} catch (_: UnknownHostException) {
|
||||||
|
stopRunner(false, getString(R.string.invalid_server))
|
||||||
|
} catch (e: PluginManager.PluginNotFoundException) {
|
||||||
|
Toast.makeText(this@Interface, e.readableMessage, Toast.LENGTH_SHORT).show()
|
||||||
|
Logs.d(e.readableMessage)
|
||||||
|
data.binder.missingPlugin(e.plugin)
|
||||||
|
stopRunner(false, null)
|
||||||
|
} catch (exc: Throwable) {
|
||||||
|
if (exc.javaClass.name.endsWith("proxyerror")) {
|
||||||
|
// error from golang
|
||||||
|
Logs.w(exc.readableMessage)
|
||||||
|
} else {
|
||||||
|
Logs.w(exc)
|
||||||
|
}
|
||||||
|
stopRunner(
|
||||||
|
false, "${getString(R.string.service_failed)}: ${exc.readableMessage}"
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
data.connectingJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Service.START_NOT_STICKY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
41
app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt
Normal file
41
app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg
|
||||||
|
|
||||||
|
import android.system.ErrnoException
|
||||||
|
import android.system.Os
|
||||||
|
import android.system.OsConstants
|
||||||
|
import android.text.TextUtils
|
||||||
|
import io.nekohasekai.sagernet.ktx.Logs
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
object Executable {
|
||||||
|
private val EXECUTABLES = setOf(
|
||||||
|
"libtrojan.so",
|
||||||
|
"libtrojan-go.so",
|
||||||
|
"libnaive.so",
|
||||||
|
"libhysteria.so",
|
||||||
|
"libwg.so"
|
||||||
|
)
|
||||||
|
|
||||||
|
fun killAll(alsoKillBg: Boolean = false) {
|
||||||
|
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }
|
||||||
|
?: return) {
|
||||||
|
val exe = File(try {
|
||||||
|
File(process, "cmdline").inputStream().bufferedReader().use {
|
||||||
|
it.readText()
|
||||||
|
}
|
||||||
|
} catch (_: IOException) {
|
||||||
|
continue
|
||||||
|
}.split(Character.MIN_VALUE, limit = 2).first())
|
||||||
|
if (EXECUTABLES.contains(exe.name) || (alsoKillBg && exe.name.endsWith(":bg"))) try {
|
||||||
|
Os.kill(process.name.toInt(), OsConstants.SIGKILL)
|
||||||
|
Logs.w("SIGKILL ${exe.name} (${process.name}) succeed")
|
||||||
|
} catch (e: ErrnoException) {
|
||||||
|
if (e.errno != OsConstants.ESRCH) {
|
||||||
|
Logs.w("SIGKILL ${exe.absolutePath} (${process.name}) failed")
|
||||||
|
Logs.w(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.system.ErrnoException
|
||||||
|
import android.system.Os
|
||||||
|
import android.system.OsConstants
|
||||||
|
import androidx.annotation.MainThread
|
||||||
|
import io.nekohasekai.sagernet.SagerNet
|
||||||
|
import io.nekohasekai.sagernet.ktx.Logs
|
||||||
|
import io.nekohasekai.sagernet.utils.Commandline
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import libcore.Libcore
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : CoroutineScope {
|
||||||
|
companion object {
|
||||||
|
private val pid by lazy {
|
||||||
|
Class.forName("java.lang.ProcessManager\$ProcessImpl").getDeclaredField("pid")
|
||||||
|
.apply { isAccessible = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class Guard(
|
||||||
|
private val cmd: List<String>,
|
||||||
|
private val env: Map<String, String> = mapOf()
|
||||||
|
) {
|
||||||
|
private lateinit var process: Process
|
||||||
|
|
||||||
|
private fun streamLogger(input: InputStream, logger: (String) -> Unit) = try {
|
||||||
|
input.bufferedReader().forEachLine(logger)
|
||||||
|
} catch (_: IOException) {
|
||||||
|
} // ignore
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
process = ProcessBuilder(cmd).directory(SagerNet.application.noBackupFilesDir).apply {
|
||||||
|
environment().putAll(env)
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
@DelicateCoroutinesApi
|
||||||
|
suspend fun looper(onRestartCallback: (suspend () -> Unit)?) {
|
||||||
|
var running = true
|
||||||
|
val cmdName = File(cmd.first()).nameWithoutExtension
|
||||||
|
val exitChannel = Channel<Int>()
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
thread(name = "stderr-$cmdName") {
|
||||||
|
streamLogger(process.errorStream) { Libcore.nekoLogPrintln("[$cmdName] $it") }
|
||||||
|
}
|
||||||
|
thread(name = "stdout-$cmdName") {
|
||||||
|
streamLogger(process.inputStream) { Libcore.nekoLogPrintln("[$cmdName] $it") }
|
||||||
|
// this thread also acts as a daemon thread for waitFor
|
||||||
|
runBlocking { exitChannel.send(process.waitFor()) }
|
||||||
|
}
|
||||||
|
val startTime = SystemClock.elapsedRealtime()
|
||||||
|
val exitCode = exitChannel.receive()
|
||||||
|
running = false
|
||||||
|
when {
|
||||||
|
SystemClock.elapsedRealtime() - startTime < 1000 -> throw IOException(
|
||||||
|
"$cmdName exits too fast (exit code: $exitCode)"
|
||||||
|
)
|
||||||
|
exitCode == 128 + OsConstants.SIGKILL -> Logs.w("$cmdName was killed")
|
||||||
|
else -> Logs.w(IOException("$cmdName unexpectedly exits with code $exitCode"))
|
||||||
|
}
|
||||||
|
Logs.i("restart process: ${Commandline.toString(cmd)} (last exit code: $exitCode)")
|
||||||
|
start()
|
||||||
|
running = true
|
||||||
|
onRestartCallback?.invoke()
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Logs.w("error occurred. stop guard: ${Commandline.toString(cmd)}")
|
||||||
|
GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
|
||||||
|
} finally {
|
||||||
|
if (running) withContext(NonCancellable) { // clean-up cannot be cancelled
|
||||||
|
if (Build.VERSION.SDK_INT < 24) {
|
||||||
|
try {
|
||||||
|
Os.kill(pid.get(process) as Int, OsConstants.SIGTERM)
|
||||||
|
} catch (e: ErrnoException) {
|
||||||
|
if (e.errno != OsConstants.ESRCH) Logs.w(e)
|
||||||
|
} catch (e: ReflectiveOperationException) {
|
||||||
|
Logs.w(e)
|
||||||
|
}
|
||||||
|
if (withTimeoutOrNull(500) { exitChannel.receive() } != null) return@withContext
|
||||||
|
}
|
||||||
|
process.destroy() // kill the process
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) {
|
||||||
|
if (withTimeoutOrNull(1000) { exitChannel.receive() } != null) return@withContext
|
||||||
|
process.destroyForcibly() // Force to kill the process if it's still alive
|
||||||
|
}
|
||||||
|
exitChannel.receive()
|
||||||
|
} // otherwise process already exited, nothing to be done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val coroutineContext = Dispatchers.Main.immediate + Job()
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun start(
|
||||||
|
cmd: List<String>,
|
||||||
|
env: MutableMap<String, String> = mutableMapOf(),
|
||||||
|
onRestartCallback: (suspend () -> Unit)? = null
|
||||||
|
) {
|
||||||
|
Logs.i("start process: ${Commandline.toString(cmd)}")
|
||||||
|
Guard(cmd, env).apply {
|
||||||
|
start() // if start fails, IOException will be thrown directly
|
||||||
|
launch { looper(onRestartCallback) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun close(scope: CoroutineScope) {
|
||||||
|
cancel()
|
||||||
|
coroutineContext[Job]!!.also { job -> scope.launch { job.cancelAndJoin() } }
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/src/main/java/io/nekohasekai/sagernet/bg/ProxyService.kt
Normal file
27
app/src/main/java/io/nekohasekai/sagernet/bg/ProxyService.kt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.PowerManager
|
||||||
|
import io.nekohasekai.sagernet.SagerNet
|
||||||
|
|
||||||
|
class ProxyService : Service(), BaseService.Interface {
|
||||||
|
override val data = BaseService.Data(this)
|
||||||
|
override val tag: String get() = "SagerNetProxyService"
|
||||||
|
override fun createNotification(profileName: String): ServiceNotification =
|
||||||
|
ServiceNotification(this, profileName, "service-proxy", true)
|
||||||
|
|
||||||
|
override var wakeLock: PowerManager.WakeLock? = null
|
||||||
|
override var upstreamInterfaceName: String? = null
|
||||||
|
|
||||||
|
@SuppressLint("WakelockTimeout")
|
||||||
|
override fun acquireWakeLock() {
|
||||||
|
wakeLock = SagerNet.power.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "sagernet:proxy")
|
||||||
|
.apply { acquire() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent) = super.onBind(intent)
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
|
||||||
|
super<BaseService.Interface>.onStartCommand(intent, flags, startId)
|
||||||
|
}
|
||||||
167
app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt
Normal file
167
app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.RemoteException
|
||||||
|
import io.nekohasekai.sagernet.Action
|
||||||
|
import io.nekohasekai.sagernet.Key
|
||||||
|
import io.nekohasekai.sagernet.aidl.*
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.ktx.runOnMainDispatcher
|
||||||
|
|
||||||
|
class SagerConnection(private var listenForDeath: Boolean = false) : ServiceConnection,
|
||||||
|
IBinder.DeathRecipient {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val serviceClass
|
||||||
|
get() = when (DataStore.serviceMode) {
|
||||||
|
Key.MODE_PROXY -> ProxyService::class
|
||||||
|
Key.MODE_VPN -> VpnService::class // Key.MODE_TRANS -> TransproxyService::class
|
||||||
|
else -> throw UnknownError()
|
||||||
|
}.java
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
// smaller ISagerNetServiceCallback
|
||||||
|
|
||||||
|
fun cbSpeedUpdate(stats: SpeedDisplayData) {}
|
||||||
|
fun cbTrafficUpdate(data: TrafficData) {}
|
||||||
|
|
||||||
|
fun stateChanged(state: BaseService.State, profileName: String?, msg: String?)
|
||||||
|
|
||||||
|
fun missingPlugin(profileName: String, pluginName: String) {}
|
||||||
|
fun routeAlert(type: Int, routeName: String) {}
|
||||||
|
|
||||||
|
fun onServiceConnected(service: ISagerNetService)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Different from Android framework, this method will be called even when you call `detachService`.
|
||||||
|
*/
|
||||||
|
fun onServiceDisconnected() {}
|
||||||
|
fun onBinderDied() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var connectionActive = false
|
||||||
|
private var callbackRegistered = false
|
||||||
|
private var callback: Callback? = null
|
||||||
|
private val serviceCallback = object : ISagerNetServiceCallback.Stub() {
|
||||||
|
|
||||||
|
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
||||||
|
val s = BaseService.State.values()[state]
|
||||||
|
DataStore.serviceState = s
|
||||||
|
val callback = callback ?: return
|
||||||
|
runOnMainDispatcher {
|
||||||
|
callback.stateChanged(s, profileName, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cbSpeedUpdate(stats: SpeedDisplayData) {
|
||||||
|
val callback = callback ?: return
|
||||||
|
runOnMainDispatcher {
|
||||||
|
callback.cbSpeedUpdate(stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cbTrafficUpdate(stats: TrafficData) {
|
||||||
|
val callback = callback ?: return
|
||||||
|
runOnMainDispatcher {
|
||||||
|
callback.cbTrafficUpdate(stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun missingPlugin(profileName: String, pluginName: String) {
|
||||||
|
val callback = callback ?: return
|
||||||
|
runOnMainDispatcher {
|
||||||
|
callback.missingPlugin(profileName, pluginName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun routeAlert(type: Int, routeName: String) {
|
||||||
|
val callback = callback ?: return
|
||||||
|
runOnMainDispatcher {
|
||||||
|
callback.routeAlert(type, routeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateWakeLockStatus(acquired: Boolean) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cbLogUpdate(str: String?) {
|
||||||
|
DataStore.postLogListener?.let {
|
||||||
|
if (str != null) {
|
||||||
|
it(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private var binder: IBinder? = null
|
||||||
|
|
||||||
|
var service: ISagerNetService? = null
|
||||||
|
|
||||||
|
override fun onServiceConnected(name: ComponentName?, binder: IBinder) {
|
||||||
|
this.binder = binder
|
||||||
|
val service = ISagerNetService.Stub.asInterface(binder)!!
|
||||||
|
this.service = service
|
||||||
|
try {
|
||||||
|
if (listenForDeath) binder.linkToDeath(this, 0)
|
||||||
|
check(!callbackRegistered)
|
||||||
|
service.registerCallback(serviceCallback)
|
||||||
|
callbackRegistered = true
|
||||||
|
} catch (e: RemoteException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
callback!!.onServiceConnected(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
unregisterCallback()
|
||||||
|
callback?.onServiceDisconnected()
|
||||||
|
service = null
|
||||||
|
binder = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun binderDied() {
|
||||||
|
service = null
|
||||||
|
callbackRegistered = false
|
||||||
|
callback?.also { runOnMainDispatcher { it.onBinderDied() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterCallback() {
|
||||||
|
val service = service
|
||||||
|
if (service != null && callbackRegistered) try {
|
||||||
|
service.unregisterCallback(serviceCallback)
|
||||||
|
} catch (_: RemoteException) {
|
||||||
|
}
|
||||||
|
callbackRegistered = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connect(context: Context, callback: Callback) {
|
||||||
|
if (connectionActive) return
|
||||||
|
connectionActive = true
|
||||||
|
check(this.callback == null)
|
||||||
|
this.callback = callback
|
||||||
|
val intent = Intent(context, serviceClass).setAction(Action.SERVICE)
|
||||||
|
context.bindService(intent, this, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disconnect(context: Context) {
|
||||||
|
unregisterCallback()
|
||||||
|
if (connectionActive) try {
|
||||||
|
context.unbindService(this)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
} // ignore
|
||||||
|
connectionActive = false
|
||||||
|
if (listenForDeath) try {
|
||||||
|
binder?.unlinkToDeath(this, 0)
|
||||||
|
} catch (_: NoSuchElementException) {
|
||||||
|
}
|
||||||
|
binder = null
|
||||||
|
service = null
|
||||||
|
callback = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,202 @@
|
|||||||
|
package io.nekohasekai.sagernet.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.text.format.Formatter
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import io.nekohasekai.sagernet.Action
|
||||||
|
import io.nekohasekai.sagernet.R
|
||||||
|
import io.nekohasekai.sagernet.SagerNet
|
||||||
|
import io.nekohasekai.sagernet.aidl.ISagerNetServiceCallback
|
||||||
|
import io.nekohasekai.sagernet.aidl.SpeedDisplayData
|
||||||
|
import io.nekohasekai.sagernet.aidl.TrafficData
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.ktx.app
|
||||||
|
import io.nekohasekai.sagernet.ktx.getColorAttr
|
||||||
|
import io.nekohasekai.sagernet.ui.SwitchActivity
|
||||||
|
import io.nekohasekai.sagernet.utils.Theme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
companion object {
|
||||||
|
const val notificationId = 1
|
||||||
|
val flags =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val trafficStatistics = DataStore.profileTrafficStatistics
|
||||||
|
val showDirectSpeed = DataStore.showDirectSpeed
|
||||||
|
|
||||||
|
private val callback: ISagerNetServiceCallback by lazy {
|
||||||
|
object : ISagerNetServiceCallback.Stub() {
|
||||||
|
override fun cbSpeedUpdate(stats: SpeedDisplayData) {
|
||||||
|
if (!trafficStatistics) return
|
||||||
|
builder.apply {
|
||||||
|
if (showDirectSpeed) {
|
||||||
|
val speedDetail = (service as Context).getString(
|
||||||
|
R.string.speed_detail, service.getString(
|
||||||
|
R.string.speed, Formatter.formatFileSize(service, stats.txRateProxy)
|
||||||
|
), service.getString(
|
||||||
|
R.string.speed, Formatter.formatFileSize(service, stats.rxRateProxy)
|
||||||
|
), service.getString(
|
||||||
|
R.string.speed,
|
||||||
|
Formatter.formatFileSize(service, stats.txRateDirect)
|
||||||
|
), service.getString(
|
||||||
|
R.string.speed,
|
||||||
|
Formatter.formatFileSize(service, stats.rxRateDirect)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setStyle(NotificationCompat.BigTextStyle().bigText(speedDetail))
|
||||||
|
setContentText(speedDetail)
|
||||||
|
} else {
|
||||||
|
val speedSimple = (service as Context).getString(
|
||||||
|
R.string.traffic, service.getString(
|
||||||
|
R.string.speed, Formatter.formatFileSize(service, stats.txRateProxy)
|
||||||
|
), service.getString(
|
||||||
|
R.string.speed, Formatter.formatFileSize(service, stats.rxRateProxy)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setContentText(speedSimple)
|
||||||
|
}
|
||||||
|
setSubText(
|
||||||
|
service.getString(
|
||||||
|
R.string.traffic,
|
||||||
|
Formatter.formatFileSize(service, stats.txTotal),
|
||||||
|
Formatter.formatFileSize(service, stats.rxTotal)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cbTrafficUpdate(stats: TrafficData?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stateChanged(state: Int, profileName: String?, msg: String?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun missingPlugin(profileName: String?, pluginName: String?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun routeAlert(type: Int, routeName: String?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateWakeLockStatus(acquired: Boolean) {
|
||||||
|
updateActions(acquired)
|
||||||
|
builder.priority =
|
||||||
|
if (acquired) NotificationCompat.PRIORITY_HIGH else NotificationCompat.PRIORITY_LOW
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cbLogUpdate(str: String?) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var callbackRegistered = false
|
||||||
|
|
||||||
|
private val builder = NotificationCompat.Builder(service as Context, channel)
|
||||||
|
.setWhen(0)
|
||||||
|
.setTicker(service.getString(R.string.forward_success))
|
||||||
|
.setContentTitle(profileName)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setContentIntent(SagerNet.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
|
||||||
|
updateActions(false)
|
||||||
|
|
||||||
|
Theme.apply(app)
|
||||||
|
Theme.apply(service)
|
||||||
|
builder.color = service.getColorAttr(R.attr.colorPrimary)
|
||||||
|
|
||||||
|
updateCallback(SagerNet.power.isInteractive)
|
||||||
|
service.registerReceiver(this, IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_SCREEN_ON)
|
||||||
|
addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
|
})
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateActions(wakeLockAcquired: Boolean) {
|
||||||
|
service as Context
|
||||||
|
|
||||||
|
builder.clearActions()
|
||||||
|
val closeAction = NotificationCompat.Action.Builder(
|
||||||
|
0, service.getText(R.string.stop), PendingIntent.getBroadcast(
|
||||||
|
service, 0, Intent(Action.CLOSE).setPackage(service.packageName), flags
|
||||||
|
)
|
||||||
|
).apply {
|
||||||
|
setShowsUserInterface(false)
|
||||||
|
}.build()
|
||||||
|
builder.addAction(closeAction)
|
||||||
|
|
||||||
|
val switchAction = NotificationCompat.Action.Builder(
|
||||||
|
0, service.getString(R.string.action_switch), PendingIntent.getActivity(
|
||||||
|
service, 0, Intent(service, SwitchActivity::class.java), flags
|
||||||
|
)
|
||||||
|
).apply {
|
||||||
|
setShowsUserInterface(false)
|
||||||
|
}.build()
|
||||||
|
builder.addAction(switchAction)
|
||||||
|
|
||||||
|
val wakeLockAction = NotificationCompat.Action.Builder(
|
||||||
|
0,
|
||||||
|
service.getText(if (!wakeLockAcquired) R.string.acquire_wake_lock else R.string.release_wake_lock),
|
||||||
|
PendingIntent.getBroadcast(
|
||||||
|
service,
|
||||||
|
0,
|
||||||
|
Intent(Action.SWITCH_WAKE_LOCK).setPackage(service.packageName),
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
||||||
|
)
|
||||||
|
).apply {
|
||||||
|
setShowsUserInterface(false)
|
||||||
|
}.build()
|
||||||
|
builder.addAction(wakeLockAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (!trafficStatistics) return
|
||||||
|
if (screenOn) {
|
||||||
|
service.data.binder.registerCallback(callback)
|
||||||
|
callbackRegistered = true
|
||||||
|
} else if (callbackRegistered) { // unregister callback to save battery
|
||||||
|
service.data.binder.unregisterCallback(callback)
|
||||||
|
callbackRegistered = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun show() = (service as Service).startForeground(notificationId, builder.build())
|
||||||
|
private fun update() =
|
||||||
|
NotificationManagerCompat.from(service as Service).notify(notificationId, builder.build())
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
(service as Service).stopForeground(true)
|
||||||
|
service.unregisterReceiver(this)
|
||||||
|
updateCallback(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.PeriodicWorkRequest
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import androidx.work.multiprocess.RemoteWorkManager
|
||||||
|
import io.nekohasekai.sagernet.R
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.database.SagerDatabase
|
||||||
|
import io.nekohasekai.sagernet.group.GroupUpdater
|
||||||
|
import io.nekohasekai.sagernet.ktx.Logs
|
||||||
|
import io.nekohasekai.sagernet.ktx.app
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
object SubscriptionUpdater {
|
||||||
|
|
||||||
|
private const val WORK_NAME = "SubscriptionUpdater"
|
||||||
|
|
||||||
|
suspend fun reconfigureUpdater() {
|
||||||
|
RemoteWorkManager.getInstance(app).cancelUniqueWork(WORK_NAME)
|
||||||
|
|
||||||
|
val subscriptions = SagerDatabase.groupDao.subscriptions()
|
||||||
|
.filter { it.subscription!!.autoUpdate }
|
||||||
|
if (subscriptions.isEmpty()) return
|
||||||
|
|
||||||
|
// PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS
|
||||||
|
var minDelay =
|
||||||
|
subscriptions.minByOrNull { it.subscription!!.autoUpdateDelay }!!.subscription!!.autoUpdateDelay.toLong()
|
||||||
|
val now = System.currentTimeMillis() / 1000L
|
||||||
|
var minInitDelay =
|
||||||
|
subscriptions.minOf { now - it.subscription!!.lastUpdated - (minDelay * 60) }
|
||||||
|
if (minDelay < 15) minDelay = 15
|
||||||
|
if (minInitDelay > 60) minInitDelay = 60
|
||||||
|
|
||||||
|
// main process
|
||||||
|
RemoteWorkManager.getInstance(app).enqueueUniquePeriodicWork(
|
||||||
|
WORK_NAME,
|
||||||
|
ExistingPeriodicWorkPolicy.REPLACE,
|
||||||
|
PeriodicWorkRequest.Builder(UpdateTask::class.java, minDelay, TimeUnit.MINUTES)
|
||||||
|
.apply {
|
||||||
|
if (minInitDelay > 0) setInitialDelay(minInitDelay, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdateTask(
|
||||||
|
appContext: Context, params: WorkerParameters
|
||||||
|
) : CoroutineWorker(appContext, params) {
|
||||||
|
|
||||||
|
val nm = NotificationManagerCompat.from(applicationContext)
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(applicationContext, "service-subscription")
|
||||||
|
.setWhen(0)
|
||||||
|
.setTicker(applicationContext.getString(R.string.forward_success))
|
||||||
|
.setContentTitle(applicationContext.getString(R.string.subscription_update))
|
||||||
|
.setSmallIcon(R.drawable.ic_service_active)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
var subscriptions =
|
||||||
|
SagerDatabase.groupDao.subscriptions().filter { it.subscription!!.autoUpdate }
|
||||||
|
if (!DataStore.serviceState.connected) {
|
||||||
|
Logs.d("work: not connected")
|
||||||
|
subscriptions = subscriptions.filter { !it.subscription!!.updateWhenConnectedOnly }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscriptions.isNotEmpty()) for (profile in subscriptions) {
|
||||||
|
val subscription = profile.subscription!!
|
||||||
|
|
||||||
|
if (((System.currentTimeMillis() / 1000).toInt() - subscription.lastUpdated) < subscription.autoUpdateDelay * 60) {
|
||||||
|
Logs.d("work: not updating " + profile.displayName())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Logs.d("work: updating " + profile.displayName())
|
||||||
|
|
||||||
|
notification.setContentText(
|
||||||
|
applicationContext.getString(
|
||||||
|
R.string.subscription_update_message, profile.displayName()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
nm.notify(2, notification.build())
|
||||||
|
|
||||||
|
GroupUpdater.executeUpdate(profile, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
nm.cancel(2)
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
104
app/src/main/java/io/nekohasekai/sagernet/bg/TileService.kt
Normal file
104
app/src/main/java/io/nekohasekai/sagernet/bg/TileService.kt
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* *
|
||||||
|
* 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 io.nekohasekai.sagernet.bg
|
||||||
|
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.service.quicksettings.Tile
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import io.nekohasekai.sagernet.R
|
||||||
|
import io.nekohasekai.sagernet.SagerNet
|
||||||
|
import io.nekohasekai.sagernet.aidl.ISagerNetService
|
||||||
|
import android.service.quicksettings.TileService as BaseTileService
|
||||||
|
|
||||||
|
@RequiresApi(24)
|
||||||
|
class TileService : BaseTileService(), SagerConnection.Callback {
|
||||||
|
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_service_idle) }
|
||||||
|
private val iconBusy by lazy { Icon.createWithResource(this, R.drawable.ic_service_busy) }
|
||||||
|
private val iconConnected by lazy {
|
||||||
|
Icon.createWithResource(this, R.drawable.ic_service_active)
|
||||||
|
}
|
||||||
|
private var tapPending = false
|
||||||
|
|
||||||
|
private val connection = SagerConnection()
|
||||||
|
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) =
|
||||||
|
updateTile(state) { profileName }
|
||||||
|
|
||||||
|
override fun onServiceConnected(service: ISagerNetService) {
|
||||||
|
updateTile(BaseService.State.values()[service.state]) { service.profileName }
|
||||||
|
if (tapPending) {
|
||||||
|
tapPending = false
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartListening() {
|
||||||
|
super.onStartListening()
|
||||||
|
connection.connect(this, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopListening() {
|
||||||
|
connection.disconnect(this)
|
||||||
|
super.onStopListening()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick() {
|
||||||
|
toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTile(serviceState: BaseService.State, profileName: () -> String?) {
|
||||||
|
qsTile?.apply {
|
||||||
|
label = null
|
||||||
|
when (serviceState) {
|
||||||
|
BaseService.State.Idle -> error("serviceState")
|
||||||
|
BaseService.State.Connecting -> {
|
||||||
|
icon = iconBusy
|
||||||
|
state = Tile.STATE_ACTIVE
|
||||||
|
}
|
||||||
|
BaseService.State.Connected -> {
|
||||||
|
icon = iconConnected
|
||||||
|
label = profileName()
|
||||||
|
state = Tile.STATE_ACTIVE
|
||||||
|
}
|
||||||
|
BaseService.State.Stopping -> {
|
||||||
|
icon = iconBusy
|
||||||
|
state = Tile.STATE_UNAVAILABLE
|
||||||
|
}
|
||||||
|
BaseService.State.Stopped -> {
|
||||||
|
icon = iconIdle
|
||||||
|
state = Tile.STATE_INACTIVE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label = label ?: getString(R.string.app_name)
|
||||||
|
updateTile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggle() {
|
||||||
|
val service = connection.service
|
||||||
|
if (service == null) tapPending =
|
||||||
|
true else BaseService.State.values()[service.state].let { state ->
|
||||||
|
when {
|
||||||
|
state.canStop -> SagerNet.stopService()
|
||||||
|
state == BaseService.State.Stopped -> SagerNet.startService()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
269
app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt
Normal file
269
app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.ProxyInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.os.PowerManager
|
||||||
|
import io.nekohasekai.sagernet.*
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.fmt.LOCALHOST
|
||||||
|
import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean
|
||||||
|
import io.nekohasekai.sagernet.ktx.*
|
||||||
|
import io.nekohasekai.sagernet.ui.VpnRequestActivity
|
||||||
|
import io.nekohasekai.sagernet.utils.Subnet
|
||||||
|
import libcore.*
|
||||||
|
import moe.matsuri.nb4a.net.LocalResolverImpl
|
||||||
|
import moe.matsuri.nb4a.proxy.neko.needBypassRootUid
|
||||||
|
import android.net.VpnService as BaseVpnService
|
||||||
|
|
||||||
|
class VpnService : BaseVpnService(),
|
||||||
|
BaseService.Interface {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val PRIVATE_VLAN4_CLIENT = "172.19.0.1"
|
||||||
|
const val PRIVATE_VLAN4_ROUTER = "172.19.0.2"
|
||||||
|
const val FAKEDNS_VLAN4_CLIENT = "198.18.0.0"
|
||||||
|
const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1"
|
||||||
|
const val PRIVATE_VLAN6_ROUTER = "fdfe:dcba:9876::2"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var conn: ParcelFileDescriptor? = null
|
||||||
|
|
||||||
|
private var metered = false
|
||||||
|
|
||||||
|
override var upstreamInterfaceName: String? = null
|
||||||
|
|
||||||
|
override suspend fun startProcesses() {
|
||||||
|
DataStore.vpnService = this
|
||||||
|
super.startProcesses() // launch proxy instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override var wakeLock: PowerManager.WakeLock? = null
|
||||||
|
|
||||||
|
@SuppressLint("WakelockTimeout")
|
||||||
|
override fun acquireWakeLock() {
|
||||||
|
wakeLock = SagerNet.power.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "sagernet:vpn")
|
||||||
|
.apply { acquire() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||||
|
override fun killProcesses() {
|
||||||
|
conn?.close()
|
||||||
|
conn = null
|
||||||
|
super.killProcesses()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent) = when (intent.action) {
|
||||||
|
SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent)
|
||||||
|
else -> super<BaseService.Interface>.onBind(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val data = BaseService.Data(this)
|
||||||
|
override val tag = "SagerNetVpnService"
|
||||||
|
override fun createNotification(profileName: String) =
|
||||||
|
ServiceNotification(this, profileName, "service-vpn")
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (DataStore.serviceMode == Key.MODE_VPN) {
|
||||||
|
if (prepare(this) != null) {
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
this, VpnRequestActivity::class.java
|
||||||
|
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
} else return super<BaseService.Interface>.onStartCommand(intent, flags, startId)
|
||||||
|
}
|
||||||
|
stopRunner()
|
||||||
|
return Service.START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class NullConnectionException : NullPointerException(),
|
||||||
|
BaseService.ExpectedException {
|
||||||
|
override fun getLocalizedMessage() = getString(R.string.reboot_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startVpn(tunOptionsJson: String, tunPlatformOptionsJson: String): Int {
|
||||||
|
// Logs.d(tunOptionsJson)
|
||||||
|
// Logs.d(tunPlatformOptionsJson)
|
||||||
|
// val tunOptions = JSONObject(tunOptionsJson)
|
||||||
|
|
||||||
|
// address & route & MTU ...... use NB4A GUI config
|
||||||
|
val profile = data.proxy!!.profile
|
||||||
|
val builder = Builder().setConfigureIntent(SagerNet.configureIntent(this))
|
||||||
|
.setSession(profile.displayName())
|
||||||
|
.setMtu(DataStore.mtu)
|
||||||
|
val ipv6Mode = DataStore.ipv6Mode
|
||||||
|
|
||||||
|
// address
|
||||||
|
builder.addAddress(PRIVATE_VLAN4_CLIENT, 30)
|
||||||
|
if (ipv6Mode != IPv6Mode.DISABLE) {
|
||||||
|
builder.addAddress(PRIVATE_VLAN6_CLIENT, 126)
|
||||||
|
}
|
||||||
|
builder.addDnsServer(PRIVATE_VLAN4_ROUTER)
|
||||||
|
|
||||||
|
// route
|
||||||
|
if (DataStore.bypassLan && !DataStore.bypassLanInCoreOnly) {
|
||||||
|
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)
|
||||||
|
builder.addRoute(FAKEDNS_VLAN4_CLIENT, 15)
|
||||||
|
// https://issuetracker.google.com/issues/149636790
|
||||||
|
if (ipv6Mode != IPv6Mode.DISABLE) {
|
||||||
|
builder.addRoute("2000::", 3)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.addRoute("0.0.0.0", 0)
|
||||||
|
if (ipv6Mode != IPv6Mode.DISABLE) {
|
||||||
|
builder.addRoute("::", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ?
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||||
|
// TODO listen for change?
|
||||||
|
if (SagerNet.underlyingNetwork != null) {
|
||||||
|
builder.setUnderlyingNetworks(arrayOf(SagerNet.underlyingNetwork))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) builder.setMetered(metered)
|
||||||
|
|
||||||
|
// app route
|
||||||
|
val packageName = packageName
|
||||||
|
var proxyApps = DataStore.proxyApps
|
||||||
|
var bypass = DataStore.bypass
|
||||||
|
var workaroundSYSTEM = false /* DataStore.tunImplementation == TunImplementation.SYSTEM */
|
||||||
|
var needBypassRootUid = workaroundSYSTEM || data.proxy!!.config.trafficMap.values.any {
|
||||||
|
it.nekoBean?.needBypassRootUid() == true || it.hysteriaBean?.protocol == HysteriaBean.PROTOCOL_FAKETCP
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxyApps || needBypassRootUid) {
|
||||||
|
val individual = mutableSetOf<String>()
|
||||||
|
val allApps by lazy {
|
||||||
|
packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS).filter {
|
||||||
|
when (it.packageName) {
|
||||||
|
packageName -> false
|
||||||
|
"android" -> true
|
||||||
|
else -> it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||||
|
}
|
||||||
|
}.map {
|
||||||
|
it.packageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proxyApps) {
|
||||||
|
individual.addAll(DataStore.individual.split('\n').filter { it.isNotBlank() })
|
||||||
|
if (bypass && needBypassRootUid) {
|
||||||
|
val individualNew = allApps.toMutableList()
|
||||||
|
individualNew.removeAll(individual)
|
||||||
|
individual.clear()
|
||||||
|
individual.addAll(individualNew)
|
||||||
|
bypass = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
individual.addAll(allApps)
|
||||||
|
bypass = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val added = mutableListOf<String>()
|
||||||
|
|
||||||
|
individual.apply {
|
||||||
|
// Allow Matsuri itself using VPN.
|
||||||
|
remove(packageName)
|
||||||
|
if (!bypass) add(packageName)
|
||||||
|
}.forEach {
|
||||||
|
try {
|
||||||
|
if (bypass) {
|
||||||
|
builder.addDisallowedApplication(it)
|
||||||
|
} else {
|
||||||
|
builder.addAllowedApplication(it)
|
||||||
|
}
|
||||||
|
added.add(it)
|
||||||
|
} catch (ex: PackageManager.NameNotFoundException) {
|
||||||
|
Logs.w(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bypass) {
|
||||||
|
Logs.d("Add bypass: ${added.joinToString(", ")}")
|
||||||
|
} else {
|
||||||
|
Logs.d("Add allow: ${added.joinToString(", ")}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && DataStore.appendHttpProxy) {
|
||||||
|
builder.setHttpProxy(ProxyInfo.buildDirectProxy(LOCALHOST, DataStore.mixedPort))
|
||||||
|
}
|
||||||
|
|
||||||
|
metered = DataStore.meteredNetwork
|
||||||
|
if (Build.VERSION.SDK_INT >= 29) builder.setMetered(metered)
|
||||||
|
conn = builder.establish() ?: throw NullConnectionException()
|
||||||
|
|
||||||
|
// post setup
|
||||||
|
Libcore.setLocalResolver(LocalResolverImpl)
|
||||||
|
|
||||||
|
return conn!!.fd
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// val appStats = mutableListOf<AppStats>()
|
||||||
|
//
|
||||||
|
// override fun updateStats(stats: AppStats) {
|
||||||
|
// appStats.add(stats)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fun persistAppStats() {
|
||||||
|
// if (!DataStore.appTrafficStatistics) return
|
||||||
|
// val tun = getTun() ?: return
|
||||||
|
// appStats.clear()
|
||||||
|
// tun.readAppTraffics(this)
|
||||||
|
// val toUpdate = mutableListOf<StatsEntity>()
|
||||||
|
// val all = SagerDatabase.statsDao.all().associateBy { it.packageName }
|
||||||
|
// for (stats in appStats) {
|
||||||
|
// if (stats.nekoConnectionsJSON.isNotBlank()) continue
|
||||||
|
// val packageName = if (stats.uid >= 10000) {
|
||||||
|
// PackageCache.uidMap[stats.uid]?.iterator()?.next() ?: "android"
|
||||||
|
// } else {
|
||||||
|
// "android"
|
||||||
|
// }
|
||||||
|
// if (!all.containsKey(packageName)) {
|
||||||
|
// SagerDatabase.statsDao.create(
|
||||||
|
// StatsEntity(
|
||||||
|
// packageName = packageName,
|
||||||
|
// tcpConnections = stats.tcpConnTotal,
|
||||||
|
// udpConnections = stats.udpConnTotal,
|
||||||
|
// uplink = stats.uplinkTotal,
|
||||||
|
// downlink = stats.downlinkTotal
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// } else {
|
||||||
|
// val entity = all[packageName]!!
|
||||||
|
// entity.tcpConnections += stats.tcpConnTotal
|
||||||
|
// entity.udpConnections += stats.udpConnTotal
|
||||||
|
// entity.uplink += stats.uplinkTotal
|
||||||
|
// entity.downlink += stats.downlinkTotal
|
||||||
|
// toUpdate.add(entity)
|
||||||
|
// }
|
||||||
|
// if (toUpdate.isNotEmpty()) {
|
||||||
|
// SagerDatabase.statsDao.update(toUpdate)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
override fun onRevoke() = stopRunner()
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
Libcore.setLocalResolver(null)
|
||||||
|
DataStore.vpnService = null
|
||||||
|
super.onDestroy()
|
||||||
|
data.binder.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,277 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg.proto
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.SystemClock
|
||||||
|
import io.nekohasekai.sagernet.SagerNet
|
||||||
|
import io.nekohasekai.sagernet.bg.AbstractInstance
|
||||||
|
import io.nekohasekai.sagernet.bg.GuardedProcessPool
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.database.ProxyEntity
|
||||||
|
import io.nekohasekai.sagernet.fmt.ConfigBuildResult
|
||||||
|
import io.nekohasekai.sagernet.fmt.buildConfig
|
||||||
|
import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.hysteria.buildHysteriaConfig
|
||||||
|
import io.nekohasekai.sagernet.fmt.naive.NaiveBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.naive.buildNaiveConfig
|
||||||
|
import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig
|
||||||
|
import io.nekohasekai.sagernet.fmt.tuic.TuicBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.tuic.buildTuicConfig
|
||||||
|
import io.nekohasekai.sagernet.ktx.*
|
||||||
|
import io.nekohasekai.sagernet.plugin.PluginManager
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import libcore.BoxInstance
|
||||||
|
import libcore.Libcore
|
||||||
|
import moe.matsuri.nb4a.proxy.neko.NekoBean
|
||||||
|
import moe.matsuri.nb4a.proxy.neko.NekoJSInterface
|
||||||
|
import moe.matsuri.nb4a.plugin.NekoPluginManager
|
||||||
|
import moe.matsuri.nb4a.proxy.neko.updateAllConfig
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
abstract class BoxInstance(
|
||||||
|
val profile: ProxyEntity
|
||||||
|
) : AbstractInstance {
|
||||||
|
|
||||||
|
lateinit var config: ConfigBuildResult
|
||||||
|
lateinit var box: BoxInstance
|
||||||
|
|
||||||
|
val pluginPath = hashMapOf<String, PluginManager.InitResult>()
|
||||||
|
val pluginConfigs = hashMapOf<Int, Pair<Int, String>>()
|
||||||
|
val externalInstances = hashMapOf<Int, AbstractInstance>()
|
||||||
|
open lateinit var processes: GuardedProcessPool
|
||||||
|
private var cacheFiles = ArrayList<File>()
|
||||||
|
fun isInitialized(): Boolean {
|
||||||
|
return ::config.isInitialized && ::box.isInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun initPlugin(name: String): PluginManager.InitResult {
|
||||||
|
return pluginPath.getOrPut(name) { PluginManager.init(name)!! }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun buildConfig() {
|
||||||
|
config = buildConfig(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open suspend fun loadConfig() {
|
||||||
|
NekoJSInterface.Default.destroyAllJsi()
|
||||||
|
box = Libcore.newSingBoxInstance(config.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
open suspend fun init() {
|
||||||
|
buildConfig()
|
||||||
|
for ((chain) in config.externalIndex) {
|
||||||
|
chain.entries.forEachIndexed { index, (port, profile) ->
|
||||||
|
when (val bean = profile.requireBean()) {
|
||||||
|
is TrojanGoBean -> {
|
||||||
|
initPlugin("trojan-go-plugin")
|
||||||
|
pluginConfigs[port] = profile.type to bean.buildTrojanGoConfig(port)
|
||||||
|
}
|
||||||
|
is NaiveBean -> {
|
||||||
|
initPlugin("naive-plugin")
|
||||||
|
pluginConfigs[port] = profile.type to bean.buildNaiveConfig(port)
|
||||||
|
}
|
||||||
|
is HysteriaBean -> {
|
||||||
|
initPlugin("hysteria-plugin")
|
||||||
|
pluginConfigs[port] = profile.type to bean.buildHysteriaConfig(port) {
|
||||||
|
File(
|
||||||
|
app.cacheDir, "hysteria_" + SystemClock.elapsedRealtime() + ".ca"
|
||||||
|
).apply {
|
||||||
|
parentFile?.mkdirs()
|
||||||
|
cacheFiles.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TuicBean -> {
|
||||||
|
initPlugin("tuic-plugin")
|
||||||
|
pluginConfigs[port] = profile.type to bean.buildTuicConfig(port) {
|
||||||
|
File(
|
||||||
|
app.noBackupFilesDir,
|
||||||
|
"tuic_" + SystemClock.elapsedRealtime() + ".ca"
|
||||||
|
).apply {
|
||||||
|
parentFile?.mkdirs()
|
||||||
|
cacheFiles.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is NekoBean -> {
|
||||||
|
// check if plugin binary can be loaded
|
||||||
|
initPlugin(bean.plgId)
|
||||||
|
|
||||||
|
// build config and check if succeed
|
||||||
|
bean.updateAllConfig(port)
|
||||||
|
if (bean.allConfig == null) {
|
||||||
|
throw NekoPluginManager.PluginInternalException(bean.protocolId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun launch() {
|
||||||
|
// TODO move, this is not box
|
||||||
|
val context =
|
||||||
|
if (Build.VERSION.SDK_INT < 24 || SagerNet.user.isUserUnlocked) SagerNet.application else SagerNet.deviceStorage
|
||||||
|
val cache = File(context.cacheDir, "tmpcfg")
|
||||||
|
cache.mkdirs()
|
||||||
|
|
||||||
|
for ((chain) in config.externalIndex) {
|
||||||
|
chain.entries.forEachIndexed { index, (port, profile) ->
|
||||||
|
val bean = profile.requireBean()
|
||||||
|
val needChain = index != chain.size - 1
|
||||||
|
val (profileType, config) = pluginConfigs[port] ?: (0 to "")
|
||||||
|
|
||||||
|
when {
|
||||||
|
externalInstances.containsKey(port) -> {
|
||||||
|
externalInstances[port]!!.launch()
|
||||||
|
}
|
||||||
|
bean is TrojanGoBean -> {
|
||||||
|
val configFile = File(
|
||||||
|
cache, "trojan_go_" + SystemClock.elapsedRealtime() + ".json"
|
||||||
|
)
|
||||||
|
configFile.parentFile?.mkdirs()
|
||||||
|
configFile.writeText(config)
|
||||||
|
cacheFiles.add(configFile)
|
||||||
|
|
||||||
|
val commands = mutableListOf(
|
||||||
|
initPlugin("trojan-go-plugin").path, "-config", configFile.absolutePath
|
||||||
|
)
|
||||||
|
|
||||||
|
processes.start(commands)
|
||||||
|
}
|
||||||
|
bean is NaiveBean -> {
|
||||||
|
val configFile = File(
|
||||||
|
cache, "naive_" + SystemClock.elapsedRealtime() + ".json"
|
||||||
|
)
|
||||||
|
|
||||||
|
configFile.parentFile?.mkdirs()
|
||||||
|
configFile.writeText(config)
|
||||||
|
cacheFiles.add(configFile)
|
||||||
|
|
||||||
|
val envMap = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
if (bean.certificates.isNotBlank()) {
|
||||||
|
val certFile = File(
|
||||||
|
cache, "naive_" + SystemClock.elapsedRealtime() + ".crt"
|
||||||
|
)
|
||||||
|
|
||||||
|
certFile.parentFile?.mkdirs()
|
||||||
|
certFile.writeText(bean.certificates)
|
||||||
|
cacheFiles.add(certFile)
|
||||||
|
|
||||||
|
envMap["SSL_CERT_FILE"] = certFile.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
val commands = mutableListOf(
|
||||||
|
initPlugin("naive-plugin").path, configFile.absolutePath
|
||||||
|
)
|
||||||
|
|
||||||
|
processes.start(commands, envMap)
|
||||||
|
}
|
||||||
|
bean is HysteriaBean -> {
|
||||||
|
val configFile = File(
|
||||||
|
cache, "hysteria_" + SystemClock.elapsedRealtime() + ".json"
|
||||||
|
)
|
||||||
|
|
||||||
|
configFile.parentFile?.mkdirs()
|
||||||
|
configFile.writeText(config)
|
||||||
|
cacheFiles.add(configFile)
|
||||||
|
|
||||||
|
val commands = mutableListOf(
|
||||||
|
initPlugin("hysteria-plugin").path,
|
||||||
|
"--no-check",
|
||||||
|
"--config",
|
||||||
|
configFile.absolutePath,
|
||||||
|
"--log-level",
|
||||||
|
if (DataStore.enableLog) "trace" else "warn",
|
||||||
|
"client"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (bean.protocol == HysteriaBean.PROTOCOL_FAKETCP) {
|
||||||
|
commands.addAll(0, listOf("su", "-c"))
|
||||||
|
}
|
||||||
|
|
||||||
|
processes.start(commands)
|
||||||
|
}
|
||||||
|
bean is NekoBean -> {
|
||||||
|
// config built from JS
|
||||||
|
val nekoRunConfigs = bean.allConfig.optJSONArray("nekoRunConfigs")
|
||||||
|
val configs = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
nekoRunConfigs?.forEach { _, any ->
|
||||||
|
any as JSONObject
|
||||||
|
|
||||||
|
val name = any.getString("name")
|
||||||
|
val configFile = File(cache, name)
|
||||||
|
configFile.parentFile?.mkdirs()
|
||||||
|
val content = any.getString("content")
|
||||||
|
configFile.writeText(content)
|
||||||
|
|
||||||
|
cacheFiles.add(configFile)
|
||||||
|
configs[name] = configFile.absolutePath
|
||||||
|
|
||||||
|
Logs.d(name + "\n\n" + content)
|
||||||
|
}
|
||||||
|
|
||||||
|
val nekoCommands = bean.allConfig.getJSONArray("nekoCommands")
|
||||||
|
val commands = mutableListOf<String>()
|
||||||
|
|
||||||
|
nekoCommands.forEach { _, any ->
|
||||||
|
if (any is String) {
|
||||||
|
if (configs.containsKey(any)) {
|
||||||
|
commands.add(configs[any]!!)
|
||||||
|
} else if (any == "%exe%") {
|
||||||
|
commands.add(initPlugin(bean.plgId).path)
|
||||||
|
} else {
|
||||||
|
commands.add(any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processes.start(commands)
|
||||||
|
}
|
||||||
|
bean is TuicBean -> {
|
||||||
|
val configFile = File(
|
||||||
|
context.noBackupFilesDir,
|
||||||
|
"tuic_" + SystemClock.elapsedRealtime() + ".json"
|
||||||
|
)
|
||||||
|
|
||||||
|
configFile.parentFile?.mkdirs()
|
||||||
|
configFile.writeText(config)
|
||||||
|
cacheFiles.add(configFile)
|
||||||
|
|
||||||
|
val commands = mutableListOf(
|
||||||
|
initPlugin("tuic-plugin").path,
|
||||||
|
"-c",
|
||||||
|
configFile.absolutePath,
|
||||||
|
)
|
||||||
|
|
||||||
|
processes.start(commands)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
box.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||||
|
override fun close() {
|
||||||
|
for (instance in externalInstances.values) {
|
||||||
|
runCatching {
|
||||||
|
instance.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheFiles.removeAll { it.delete(); true }
|
||||||
|
|
||||||
|
if (::processes.isInitialized) processes.close(GlobalScope + Dispatchers.IO)
|
||||||
|
|
||||||
|
if (::box.isInitialized) {
|
||||||
|
box.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg.proto
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.bg.BaseService
|
||||||
|
import io.nekohasekai.sagernet.database.ProxyEntity
|
||||||
|
import io.nekohasekai.sagernet.ktx.Logs
|
||||||
|
import io.nekohasekai.sagernet.ktx.runOnIoDispatcher
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
class ProxyInstance(profile: ProxyEntity, val service: BaseService.Interface) :
|
||||||
|
BoxInstance(profile) {
|
||||||
|
|
||||||
|
// for TrafficLooper
|
||||||
|
private var looper: TrafficLooper? = null
|
||||||
|
|
||||||
|
override fun buildConfig() {
|
||||||
|
super.buildConfig()
|
||||||
|
Logs.d(config.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun init() {
|
||||||
|
super.init()
|
||||||
|
pluginConfigs.forEach { (_, plugin) ->
|
||||||
|
val (_, content) = plugin
|
||||||
|
Logs.d(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun launch() {
|
||||||
|
box.setAsMain()
|
||||||
|
super.launch()
|
||||||
|
runOnIoDispatcher {
|
||||||
|
looper = TrafficLooper(service.data, this)
|
||||||
|
looper?.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
super.close()
|
||||||
|
runBlocking {
|
||||||
|
looper?.stop()
|
||||||
|
looper = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg.proto
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.BuildConfig
|
||||||
|
import io.nekohasekai.sagernet.bg.GuardedProcessPool
|
||||||
|
import io.nekohasekai.sagernet.database.ProxyEntity
|
||||||
|
import io.nekohasekai.sagernet.fmt.buildConfig
|
||||||
|
import io.nekohasekai.sagernet.ktx.Logs
|
||||||
|
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
|
||||||
|
import io.nekohasekai.sagernet.ktx.tryResume
|
||||||
|
import io.nekohasekai.sagernet.ktx.tryResumeWithException
|
||||||
|
import libcore.Libcore
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
class TestInstance(profile: ProxyEntity, val link: String, val timeout: Int) :
|
||||||
|
BoxInstance(profile) {
|
||||||
|
|
||||||
|
suspend fun doTest(): Int {
|
||||||
|
return suspendCoroutine { c ->
|
||||||
|
processes = GuardedProcessPool {
|
||||||
|
Logs.w(it)
|
||||||
|
c.tryResumeWithException(it)
|
||||||
|
}
|
||||||
|
runOnDefaultDispatcher {
|
||||||
|
use {
|
||||||
|
try {
|
||||||
|
init()
|
||||||
|
launch()
|
||||||
|
c.tryResume(Libcore.urlTest(box, link, timeout))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
c.tryResumeWithException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildConfig() {
|
||||||
|
config = buildConfig(profile, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun loadConfig() {
|
||||||
|
// don't call destroyAllJsi here
|
||||||
|
if (BuildConfig.DEBUG) Logs.d(config.config)
|
||||||
|
box = Libcore.newSingBoxInstance(config.config)
|
||||||
|
box.forTest = true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,130 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg.proto
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.aidl.SpeedDisplayData
|
||||||
|
import io.nekohasekai.sagernet.aidl.TrafficData
|
||||||
|
import io.nekohasekai.sagernet.bg.BaseService
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.database.ProfileManager
|
||||||
|
import io.nekohasekai.sagernet.ktx.Logs
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.time.DurationUnit
|
||||||
|
import kotlin.time.toDuration
|
||||||
|
|
||||||
|
class TrafficLooper
|
||||||
|
(
|
||||||
|
val data: BaseService.Data, private val sc: CoroutineScope
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var job: Job? = null
|
||||||
|
private val items = mutableMapOf<String, TrafficUpdater.TrafficLooperData>()
|
||||||
|
|
||||||
|
suspend fun stop() {
|
||||||
|
job?.cancel()
|
||||||
|
// finally
|
||||||
|
val traffic = mutableMapOf<Long, TrafficData>()
|
||||||
|
data.proxy?.config?.trafficMap?.forEach { (tag, ent) ->
|
||||||
|
val item = items[tag] ?: return@forEach
|
||||||
|
ent.rx = item.rx
|
||||||
|
ent.tx = item.tx
|
||||||
|
ProfileManager.updateProfile(ent) // update DB
|
||||||
|
traffic[ent.id] = TrafficData(
|
||||||
|
id = ent.id,
|
||||||
|
rx = ent.rx,
|
||||||
|
tx = ent.tx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
data.binder.broadcast { b ->
|
||||||
|
for (t in traffic) {
|
||||||
|
b.cbTrafficUpdate(t.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logs.d("finally traffic post done")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
job = sc.launch { loop() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loop() {
|
||||||
|
val delayMs = DataStore.speedInterval
|
||||||
|
val showDirectSpeed = DataStore.showDirectSpeed
|
||||||
|
if (delayMs == 0) return
|
||||||
|
|
||||||
|
var trafficUpdater: TrafficUpdater? = null
|
||||||
|
var proxy: ProxyInstance?
|
||||||
|
var itemMain: TrafficUpdater.TrafficLooperData? = null
|
||||||
|
var itemMainBase: TrafficUpdater.TrafficLooperData? = null
|
||||||
|
var itemBypass: TrafficUpdater.TrafficLooperData? = null
|
||||||
|
|
||||||
|
while (sc.isActive) {
|
||||||
|
delay(delayMs.toDuration(DurationUnit.MILLISECONDS))
|
||||||
|
proxy = data.proxy ?: continue
|
||||||
|
|
||||||
|
if (trafficUpdater == null) {
|
||||||
|
if (!proxy.isInitialized()) continue
|
||||||
|
items.clear()
|
||||||
|
itemBypass = TrafficUpdater.TrafficLooperData(tag = "bypass")
|
||||||
|
items["bypass"] = itemBypass
|
||||||
|
// proxy.config.trafficMap.forEach { (tag, ent) ->
|
||||||
|
proxy.config.outboundTags.forEach { tag ->
|
||||||
|
// TODO g-xx query traffic return 0?
|
||||||
|
val ent = proxy.config.trafficMap[tag] ?: return@forEach
|
||||||
|
val item = TrafficUpdater.TrafficLooperData(
|
||||||
|
tag = tag,
|
||||||
|
rx = ent.rx,
|
||||||
|
tx = ent.tx,
|
||||||
|
)
|
||||||
|
if (tag == proxy.config.outboundTagMain) {
|
||||||
|
itemMain = item
|
||||||
|
itemMainBase = TrafficUpdater.TrafficLooperData(
|
||||||
|
tag = tag,
|
||||||
|
rx = ent.rx,
|
||||||
|
tx = ent.tx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items[tag] = item
|
||||||
|
Logs.d("traffic count $tag to ${ent.id}")
|
||||||
|
}
|
||||||
|
trafficUpdater = TrafficUpdater(
|
||||||
|
box = proxy.box, items = items
|
||||||
|
)
|
||||||
|
proxy.box.setV2rayStats(items.keys.joinToString("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
trafficUpdater.updateAll()
|
||||||
|
if (!sc.isActive) return
|
||||||
|
|
||||||
|
// speed
|
||||||
|
val speed = SpeedDisplayData(
|
||||||
|
itemMain!!.txRate,
|
||||||
|
itemMain!!.rxRate,
|
||||||
|
if (showDirectSpeed) itemBypass!!.txRate else 0L,
|
||||||
|
if (showDirectSpeed) itemBypass!!.rxRate else 0L,
|
||||||
|
itemMain!!.tx - itemMainBase!!.tx,
|
||||||
|
itemMain!!.rx - itemMainBase!!.rx
|
||||||
|
)
|
||||||
|
|
||||||
|
// traffic
|
||||||
|
val traffic = mutableMapOf<Long, TrafficData>()
|
||||||
|
proxy.config.trafficMap.forEach { (tag, ent) ->
|
||||||
|
val item = items[tag] ?: return@forEach
|
||||||
|
ent.rx = item.rx
|
||||||
|
ent.tx = item.tx
|
||||||
|
// ProfileManager.updateProfile(ent) // update DB
|
||||||
|
traffic[ent.id] = TrafficData(
|
||||||
|
id = ent.id,
|
||||||
|
rx = ent.rx,
|
||||||
|
tx = ent.tx,
|
||||||
|
) // display
|
||||||
|
}
|
||||||
|
|
||||||
|
// broadcast
|
||||||
|
data.binder.broadcast { b ->
|
||||||
|
b.cbSpeedUpdate(speed)
|
||||||
|
for (t in traffic) {
|
||||||
|
b.cbTrafficUpdate(t.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg.proto
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.ktx.onIoDispatcher
|
||||||
|
|
||||||
|
class TrafficUpdater(
|
||||||
|
private val box: libcore.BoxInstance,
|
||||||
|
val items: Map<String, TrafficLooperData>, // contain "bypass"
|
||||||
|
) {
|
||||||
|
|
||||||
|
class TrafficLooperData(
|
||||||
|
var tag: String,
|
||||||
|
var tx: Long = 0,
|
||||||
|
var rx: Long = 0,
|
||||||
|
var txRate: Long = 0,
|
||||||
|
var rxRate: Long = 0,
|
||||||
|
var lastUpdate: Long = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
private suspend fun updateOne(item: TrafficLooperData): TrafficLooperData {
|
||||||
|
// last update
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val interval = now - item.lastUpdate
|
||||||
|
item.lastUpdate = now
|
||||||
|
if (interval <= 0) return item.apply {
|
||||||
|
rxRate = 0
|
||||||
|
txRate = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// query
|
||||||
|
var tx = 0L
|
||||||
|
var rx = 0L
|
||||||
|
onIoDispatcher {
|
||||||
|
tx = box.queryStats(item.tag, "uplink")
|
||||||
|
rx = box.queryStats(item.tag, "downlink")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add diff
|
||||||
|
item.rx += rx
|
||||||
|
item.tx += tx
|
||||||
|
item.rxRate = rx * 1000 / interval
|
||||||
|
item.txRate = tx * 1000 / interval
|
||||||
|
|
||||||
|
// return diff
|
||||||
|
return TrafficLooperData(
|
||||||
|
tag = item.tag,
|
||||||
|
rx = rx,
|
||||||
|
tx = tx,
|
||||||
|
rxRate = item.rxRate,
|
||||||
|
txRate = item.txRate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateAll() {
|
||||||
|
val updated = mutableMapOf<String, TrafficLooperData>() // diffs
|
||||||
|
items.forEach { (tag, item) ->
|
||||||
|
var diff = updated[tag]
|
||||||
|
// query a tag only once
|
||||||
|
if (diff == null) {
|
||||||
|
diff = updateOne(item)
|
||||||
|
updated[tag] = diff
|
||||||
|
} else {
|
||||||
|
item.rx += diff.rx
|
||||||
|
item.tx += diff.tx
|
||||||
|
item.rxRate += diff.rxRate
|
||||||
|
item.txRate += diff.txRate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package io.nekohasekai.sagernet.bg.proto
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.database.ProxyEntity
|
||||||
|
|
||||||
|
class UrlTest {
|
||||||
|
|
||||||
|
val link = DataStore.connectionTestURL
|
||||||
|
val timeout = 3000
|
||||||
|
|
||||||
|
suspend fun doTest(profile: ProxyEntity): Int {
|
||||||
|
return TestInstance(profile, link, timeout).doTest()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
250
app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
Normal file
250
app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
package io.nekohasekai.sagernet.database
|
||||||
|
|
||||||
|
import android.os.Binder
|
||||||
|
import androidx.preference.PreferenceDataStore
|
||||||
|
import io.nekohasekai.sagernet.*
|
||||||
|
import io.nekohasekai.sagernet.bg.BaseService
|
||||||
|
import io.nekohasekai.sagernet.bg.VpnService
|
||||||
|
import io.nekohasekai.sagernet.database.preference.OnPreferenceDataStoreChangeListener
|
||||||
|
import io.nekohasekai.sagernet.database.preference.PublicDatabase
|
||||||
|
import io.nekohasekai.sagernet.database.preference.RoomPreferenceDataStore
|
||||||
|
import io.nekohasekai.sagernet.ktx.*
|
||||||
|
import moe.matsuri.nb4a.TempDatabase
|
||||||
|
|
||||||
|
object DataStore : OnPreferenceDataStoreChangeListener {
|
||||||
|
|
||||||
|
// share service state in main process
|
||||||
|
@Volatile
|
||||||
|
var serviceState = BaseService.State.Idle
|
||||||
|
|
||||||
|
val configurationStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
|
||||||
|
val profileCacheStore = RoomPreferenceDataStore(TempDatabase.profileCacheDao)
|
||||||
|
|
||||||
|
// last used, but may not be running
|
||||||
|
var currentProfile by configurationStore.long(Key.PROFILE_CURRENT)
|
||||||
|
|
||||||
|
var selectedProxy by configurationStore.long(Key.PROFILE_ID)
|
||||||
|
var selectedGroup by configurationStore.long(Key.PROFILE_GROUP) { currentGroupId() } // "ungrouped" group id = 1
|
||||||
|
|
||||||
|
// only in bg process
|
||||||
|
var vpnService: VpnService? = null
|
||||||
|
var baseService: BaseService.Interface? = null
|
||||||
|
|
||||||
|
// only in GUI process
|
||||||
|
var postLogListener: ((String) -> Unit)? = null
|
||||||
|
|
||||||
|
fun currentGroupId(): Long {
|
||||||
|
val currentSelected = configurationStore.getLong(Key.PROFILE_GROUP, -1)
|
||||||
|
if (currentSelected > 0L) return currentSelected
|
||||||
|
val groups = SagerDatabase.groupDao.allGroups()
|
||||||
|
if (groups.isNotEmpty()) {
|
||||||
|
val groupId = groups[0].id
|
||||||
|
selectedGroup = groupId
|
||||||
|
return groupId
|
||||||
|
}
|
||||||
|
val groupId = SagerDatabase.groupDao.createGroup(ProxyGroup(ungrouped = true))
|
||||||
|
selectedGroup = groupId
|
||||||
|
return groupId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentGroup(): ProxyGroup {
|
||||||
|
var group: ProxyGroup? = null
|
||||||
|
val currentSelected = configurationStore.getLong(Key.PROFILE_GROUP, -1)
|
||||||
|
if (currentSelected > 0L) {
|
||||||
|
group = SagerDatabase.groupDao.getById(currentSelected)
|
||||||
|
}
|
||||||
|
if (group != null) return group
|
||||||
|
val groups = SagerDatabase.groupDao.allGroups()
|
||||||
|
if (groups.isEmpty()) {
|
||||||
|
group = ProxyGroup(ungrouped = true).apply {
|
||||||
|
id = SagerDatabase.groupDao.createGroup(this)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
group = groups[0]
|
||||||
|
}
|
||||||
|
selectedGroup = group.id
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectedGroupForImport(): Long {
|
||||||
|
val current = currentGroup()
|
||||||
|
if (current.type == GroupType.BASIC) return current.id
|
||||||
|
val groups = SagerDatabase.groupDao.allGroups()
|
||||||
|
return groups.find { it.type == GroupType.BASIC }!!.id
|
||||||
|
}
|
||||||
|
|
||||||
|
var nekoPlugins by configurationStore.string(Key.NEKO_PLUGIN_MANAGED)
|
||||||
|
var appTLSVersion by configurationStore.string(Key.APP_TLS_VERSION)
|
||||||
|
var enableClashAPI by configurationStore.boolean(Key.ENABLE_CLASH_API)
|
||||||
|
var showBottomBar by configurationStore.boolean(Key.SHOW_BOTTOM_BAR)
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
var isExpert by configurationStore.boolean(Key.APP_EXPERT)
|
||||||
|
var appTheme by configurationStore.int(Key.APP_THEME)
|
||||||
|
var nightTheme by configurationStore.stringToInt(Key.NIGHT_THEME)
|
||||||
|
var serviceMode by configurationStore.string(Key.SERVICE_MODE) { Key.MODE_VPN }
|
||||||
|
|
||||||
|
// var domainStrategy by configurationStore.string(Key.DOMAIN_STRATEGY) { "AsIs" }
|
||||||
|
var trafficSniffing by configurationStore.boolean(Key.TRAFFIC_SNIFFING) { true }
|
||||||
|
var resolveDestination by configurationStore.boolean(Key.RESOLVE_DESTINATION)
|
||||||
|
|
||||||
|
// var tcpKeepAliveInterval by configurationStore.stringToInt(Key.TCP_KEEP_ALIVE_INTERVAL) { 15 }
|
||||||
|
var mtu by configurationStore.stringToInt(Key.MTU) { 9000 }
|
||||||
|
|
||||||
|
var bypassLan by configurationStore.boolean(Key.BYPASS_LAN)
|
||||||
|
var bypassLanInCoreOnly by configurationStore.boolean(Key.BYPASS_LAN_IN_CORE_ONLY)
|
||||||
|
|
||||||
|
var allowAccess by configurationStore.boolean(Key.ALLOW_ACCESS)
|
||||||
|
var speedInterval by configurationStore.stringToInt(Key.SPEED_INTERVAL)
|
||||||
|
|
||||||
|
var remoteDns by configurationStore.string(Key.REMOTE_DNS) { "https://8.8.8.8/dns-query" }
|
||||||
|
var directDns by configurationStore.string(Key.DIRECT_DNS) { "https://223.5.5.5/dns-query" }
|
||||||
|
var directDnsUseSystem by configurationStore.boolean(Key.DIRECT_DNS_USE_SYSTEM)
|
||||||
|
var enableDnsRouting by configurationStore.boolean(Key.ENABLE_DNS_ROUTING) { true }
|
||||||
|
var enableFakeDns by configurationStore.boolean(Key.ENABLE_FAKEDNS)
|
||||||
|
var dnsNetwork by configurationStore.stringSet(Key.DNS_NETWORK)
|
||||||
|
|
||||||
|
var rulesProvider by configurationStore.stringToInt(Key.RULES_PROVIDER)
|
||||||
|
var enableLog by configurationStore.boolean(Key.ENABLE_LOG)
|
||||||
|
var logBufSize by configurationStore.int(Key.LOG_BUF_SIZE) { 0 }
|
||||||
|
var acquireWakeLock by configurationStore.boolean(Key.ACQUIRE_WAKE_LOCK)
|
||||||
|
|
||||||
|
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
|
||||||
|
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
|
||||||
|
var mixedPort: Int
|
||||||
|
get() = getLocalPort(Key.MIXED_PORT, 2080)
|
||||||
|
set(value) = saveLocalPort(Key.MIXED_PORT, value)
|
||||||
|
var localDNSPort: Int
|
||||||
|
get() = getLocalPort(Key.LOCAL_DNS_PORT, 6450)
|
||||||
|
set(value) {
|
||||||
|
saveLocalPort(Key.LOCAL_DNS_PORT, value)
|
||||||
|
}
|
||||||
|
var transproxyPort: Int
|
||||||
|
get() = getLocalPort(Key.TRANSPROXY_PORT, 9200)
|
||||||
|
set(value) = saveLocalPort(Key.TRANSPROXY_PORT, value)
|
||||||
|
|
||||||
|
fun initGlobal() {
|
||||||
|
if (configurationStore.getString(Key.MIXED_PORT) == null) {
|
||||||
|
mixedPort = mixedPort
|
||||||
|
}
|
||||||
|
if (configurationStore.getString(Key.LOCAL_DNS_PORT) == null) {
|
||||||
|
localDNSPort = localDNSPort
|
||||||
|
}
|
||||||
|
if (configurationStore.getString(Key.TRANSPROXY_PORT) == null) {
|
||||||
|
transproxyPort = transproxyPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun getLocalPort(key: String, default: Int): Int {
|
||||||
|
return parsePort(configurationStore.getString(key), default + userIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveLocalPort(key: String, value: Int) {
|
||||||
|
configurationStore.putString(key, "$value")
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipv6Mode by configurationStore.stringToInt(Key.IPV6_MODE) { IPv6Mode.DISABLE }
|
||||||
|
|
||||||
|
var meteredNetwork by configurationStore.boolean(Key.METERED_NETWORK)
|
||||||
|
var proxyApps by configurationStore.boolean(Key.PROXY_APPS)
|
||||||
|
var bypass by configurationStore.boolean(Key.BYPASS_MODE) { true }
|
||||||
|
var individual by configurationStore.string(Key.INDIVIDUAL)
|
||||||
|
var showDirectSpeed by configurationStore.boolean(Key.SHOW_DIRECT_SPEED) { true }
|
||||||
|
|
||||||
|
var appendHttpProxy by configurationStore.boolean(Key.APPEND_HTTP_PROXY)
|
||||||
|
var requireTransproxy by configurationStore.boolean(Key.REQUIRE_TRANSPROXY)
|
||||||
|
var transproxyMode by configurationStore.stringToInt(Key.TRANSPROXY_MODE)
|
||||||
|
var connectionTestURL by configurationStore.string(Key.CONNECTION_TEST_URL) { CONNECTION_TEST_URL }
|
||||||
|
var alwaysShowAddress by configurationStore.boolean(Key.ALWAYS_SHOW_ADDRESS)
|
||||||
|
|
||||||
|
var tunImplementation by configurationStore.stringToInt(Key.TUN_IMPLEMENTATION) { TunImplementation.SYSTEM }
|
||||||
|
var profileTrafficStatistics by configurationStore.boolean(Key.PROFILE_TRAFFIC_STATISTICS) { true }
|
||||||
|
|
||||||
|
var yacdURL by configurationStore.string("yacdURL") { "http://127.0.0.1:9090/ui" }
|
||||||
|
|
||||||
|
// protocol
|
||||||
|
|
||||||
|
var muxProtocols by configurationStore.stringSet(Key.MUX_PROTOCOLS)
|
||||||
|
var muxConcurrency by configurationStore.stringToInt(Key.MUX_CONCURRENCY) { 8 }
|
||||||
|
|
||||||
|
// old cache, DO NOT ADD
|
||||||
|
|
||||||
|
var dirty by profileCacheStore.boolean(Key.PROFILE_DIRTY)
|
||||||
|
var editingId by profileCacheStore.long(Key.PROFILE_ID)
|
||||||
|
var editingGroup by profileCacheStore.long(Key.PROFILE_GROUP)
|
||||||
|
var profileName by profileCacheStore.string(Key.PROFILE_NAME)
|
||||||
|
var serverAddress by profileCacheStore.string(Key.SERVER_ADDRESS)
|
||||||
|
var serverPort by profileCacheStore.stringToInt(Key.SERVER_PORT)
|
||||||
|
var serverUsername by profileCacheStore.string(Key.SERVER_USERNAME)
|
||||||
|
var serverPassword by profileCacheStore.string(Key.SERVER_PASSWORD)
|
||||||
|
var serverPassword1 by profileCacheStore.string(Key.SERVER_PASSWORD1)
|
||||||
|
var serverMethod by profileCacheStore.string(Key.SERVER_METHOD)
|
||||||
|
|
||||||
|
var sharedStorage by profileCacheStore.string("sharedStorage")
|
||||||
|
|
||||||
|
var serverProtocol by profileCacheStore.string(Key.SERVER_PROTOCOL)
|
||||||
|
var serverObfs by profileCacheStore.string(Key.SERVER_OBFS)
|
||||||
|
|
||||||
|
var serverNetwork by profileCacheStore.string(Key.SERVER_NETWORK)
|
||||||
|
var serverHost by profileCacheStore.string(Key.SERVER_HOST)
|
||||||
|
var serverPath by profileCacheStore.string(Key.SERVER_PATH)
|
||||||
|
var serverSNI by profileCacheStore.string(Key.SERVER_SNI)
|
||||||
|
var serverEncryption by profileCacheStore.string(Key.SERVER_ENCRYPTION)
|
||||||
|
var serverALPN by profileCacheStore.string(Key.SERVER_ALPN)
|
||||||
|
var serverCertificates by profileCacheStore.string(Key.SERVER_CERTIFICATES)
|
||||||
|
var serverHeaders by profileCacheStore.string(Key.SERVER_HEADERS)
|
||||||
|
var serverAllowInsecure by profileCacheStore.boolean(Key.SERVER_ALLOW_INSECURE)
|
||||||
|
|
||||||
|
var serverAuthType by profileCacheStore.stringToInt(Key.SERVER_AUTH_TYPE)
|
||||||
|
var serverUploadSpeed by profileCacheStore.stringToInt(Key.SERVER_UPLOAD_SPEED)
|
||||||
|
var serverDownloadSpeed by profileCacheStore.stringToInt(Key.SERVER_DOWNLOAD_SPEED)
|
||||||
|
var serverStreamReceiveWindow by profileCacheStore.stringToIntIfExists(Key.SERVER_STREAM_RECEIVE_WINDOW)
|
||||||
|
var serverConnectionReceiveWindow by profileCacheStore.stringToIntIfExists(Key.SERVER_CONNECTION_RECEIVE_WINDOW)
|
||||||
|
var serverMTU by profileCacheStore.stringToInt(Key.SERVER_MTU) { 1420 }
|
||||||
|
var serverDisableMtuDiscovery by profileCacheStore.boolean(Key.SERVER_DISABLE_MTU_DISCOVERY)
|
||||||
|
var serverHopInterval by profileCacheStore.stringToInt(Key.SERVER_HOP_INTERVAL) { 10 }
|
||||||
|
|
||||||
|
var serverProtocolVersion by profileCacheStore.stringToInt(Key.SERVER_PROTOCOL)
|
||||||
|
var serverPrivateKey by profileCacheStore.string(Key.SERVER_PRIVATE_KEY)
|
||||||
|
var serverInsecureConcurrency by profileCacheStore.stringToInt(Key.SERVER_INSECURE_CONCURRENCY)
|
||||||
|
|
||||||
|
var serverUDPRelayMode by profileCacheStore.string(Key.SERVER_UDP_RELAY_MODE)
|
||||||
|
var serverCongestionController by profileCacheStore.string(Key.SERVER_CONGESTION_CONTROLLER)
|
||||||
|
var serverDisableSNI by profileCacheStore.boolean(Key.SERVER_DISABLE_SNI)
|
||||||
|
var serverReduceRTT by profileCacheStore.boolean(Key.SERVER_REDUCE_RTT)
|
||||||
|
var serverFastConnect by profileCacheStore.boolean(Key.SERVER_FAST_CONNECT)
|
||||||
|
|
||||||
|
var routeName by profileCacheStore.string(Key.ROUTE_NAME)
|
||||||
|
var routeDomain by profileCacheStore.string(Key.ROUTE_DOMAIN)
|
||||||
|
var routeIP by profileCacheStore.string(Key.ROUTE_IP)
|
||||||
|
var routePort by profileCacheStore.string(Key.ROUTE_PORT)
|
||||||
|
var routeSourcePort by profileCacheStore.string(Key.ROUTE_SOURCE_PORT)
|
||||||
|
var routeNetwork by profileCacheStore.string(Key.ROUTE_NETWORK)
|
||||||
|
var routeSource by profileCacheStore.string(Key.ROUTE_SOURCE)
|
||||||
|
var routeProtocol by profileCacheStore.string(Key.ROUTE_PROTOCOL)
|
||||||
|
var routeOutbound by profileCacheStore.stringToInt(Key.ROUTE_OUTBOUND)
|
||||||
|
var routeOutboundRule by profileCacheStore.long(Key.ROUTE_OUTBOUND_RULE)
|
||||||
|
var routePackages by profileCacheStore.string(Key.ROUTE_PACKAGES)
|
||||||
|
|
||||||
|
|
||||||
|
var serverConfig by profileCacheStore.string(Key.SERVER_CONFIG)
|
||||||
|
|
||||||
|
var groupName by profileCacheStore.string(Key.GROUP_NAME)
|
||||||
|
var groupType by profileCacheStore.stringToInt(Key.GROUP_TYPE)
|
||||||
|
var groupOrder by profileCacheStore.stringToInt(Key.GROUP_ORDER)
|
||||||
|
|
||||||
|
var subscriptionLink by profileCacheStore.string(Key.SUBSCRIPTION_LINK)
|
||||||
|
var subscriptionForceResolve by profileCacheStore.boolean(Key.SUBSCRIPTION_FORCE_RESOLVE)
|
||||||
|
var subscriptionDeduplication by profileCacheStore.boolean(Key.SUBSCRIPTION_DEDUPLICATION)
|
||||||
|
var subscriptionUpdateWhenConnectedOnly by profileCacheStore.boolean(Key.SUBSCRIPTION_UPDATE_WHEN_CONNECTED_ONLY)
|
||||||
|
var subscriptionUserAgent by profileCacheStore.string(Key.SUBSCRIPTION_USER_AGENT)
|
||||||
|
var subscriptionAutoUpdate by profileCacheStore.boolean(Key.SUBSCRIPTION_AUTO_UPDATE)
|
||||||
|
var subscriptionAutoUpdateDelay by profileCacheStore.stringToInt(Key.SUBSCRIPTION_AUTO_UPDATE_DELAY) { 360 }
|
||||||
|
|
||||||
|
var rulesFirstCreate by profileCacheStore.boolean("rulesFirstCreate")
|
||||||
|
|
||||||
|
override fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
package io.nekohasekai.sagernet.database
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.GroupType
|
||||||
|
import io.nekohasekai.sagernet.bg.SubscriptionUpdater
|
||||||
|
import io.nekohasekai.sagernet.ktx.applyDefaultValues
|
||||||
|
|
||||||
|
object GroupManager {
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
suspend fun groupAdd(group: ProxyGroup)
|
||||||
|
suspend fun groupUpdated(group: ProxyGroup)
|
||||||
|
|
||||||
|
suspend fun groupRemoved(groupId: Long)
|
||||||
|
suspend fun groupUpdated(groupId: Long)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Interface {
|
||||||
|
suspend fun confirm(message: String): Boolean
|
||||||
|
suspend fun alert(message: String)
|
||||||
|
suspend fun onUpdateSuccess(
|
||||||
|
group: ProxyGroup,
|
||||||
|
changed: Int,
|
||||||
|
added: List<String>,
|
||||||
|
updated: Map<String, String>,
|
||||||
|
deleted: List<String>,
|
||||||
|
duplicate: List<String>,
|
||||||
|
byUser: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun onUpdateFailure(group: ProxyGroup, message: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val listeners = ArrayList<Listener>()
|
||||||
|
var userInterface: Interface? = null
|
||||||
|
|
||||||
|
suspend fun iterator(what: suspend Listener.() -> Unit) {
|
||||||
|
synchronized(listeners) {
|
||||||
|
listeners.toList()
|
||||||
|
}.forEach { listener ->
|
||||||
|
what(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addListener(listener: Listener) {
|
||||||
|
synchronized(listeners) {
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeListener(listener: Listener) {
|
||||||
|
synchronized(listeners) {
|
||||||
|
listeners.remove(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clearGroup(groupId: Long) {
|
||||||
|
DataStore.selectedProxy = 0L
|
||||||
|
SagerDatabase.proxyDao.deleteAll(groupId)
|
||||||
|
iterator { groupUpdated(groupId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rearrange(groupId: Long) {
|
||||||
|
val entities = SagerDatabase.proxyDao.getByGroup(groupId)
|
||||||
|
for (index in entities.indices) {
|
||||||
|
entities[index].userOrder = (index + 1).toLong()
|
||||||
|
}
|
||||||
|
SagerDatabase.proxyDao.updateProxy(entities)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun postUpdate(group: ProxyGroup) {
|
||||||
|
iterator { groupUpdated(group) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun postUpdate(groupId: Long) {
|
||||||
|
postUpdate(SagerDatabase.groupDao.getById(groupId) ?: return)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun postReload(groupId: Long) {
|
||||||
|
iterator { groupUpdated(groupId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createGroup(group: ProxyGroup): ProxyGroup {
|
||||||
|
group.userOrder = SagerDatabase.groupDao.nextOrder() ?: 1
|
||||||
|
group.id = SagerDatabase.groupDao.createGroup(group.applyDefaultValues())
|
||||||
|
iterator { groupAdd(group) }
|
||||||
|
if (group.type == GroupType.SUBSCRIPTION) {
|
||||||
|
SubscriptionUpdater.reconfigureUpdater()
|
||||||
|
}
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateGroup(group: ProxyGroup) {
|
||||||
|
SagerDatabase.groupDao.updateGroup(group)
|
||||||
|
iterator { groupUpdated(group) }
|
||||||
|
if (group.type == GroupType.SUBSCRIPTION) {
|
||||||
|
SubscriptionUpdater.reconfigureUpdater()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteGroup(groupId: Long) {
|
||||||
|
SagerDatabase.groupDao.deleteById(groupId)
|
||||||
|
SagerDatabase.proxyDao.deleteByGroup(groupId)
|
||||||
|
iterator { groupRemoved(groupId) }
|
||||||
|
SubscriptionUpdater.reconfigureUpdater()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteGroup(group: List<ProxyGroup>) {
|
||||||
|
SagerDatabase.groupDao.deleteGroup(group)
|
||||||
|
SagerDatabase.proxyDao.deleteByGroup(group.map { it.id }.toLongArray())
|
||||||
|
for (proxyGroup in group) iterator { groupRemoved(proxyGroup.id) }
|
||||||
|
SubscriptionUpdater.reconfigureUpdater()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package io.nekohasekai.sagernet.database;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* see: https://youtrack.jetbrains.com/issue/KT-19853
|
||||||
|
*/
|
||||||
|
public class ParcelizeBridge {
|
||||||
|
|
||||||
|
public static RuleEntity createRule(Parcel parcel) {
|
||||||
|
return (RuleEntity) RuleEntity.CREATOR.createFromParcel(parcel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,258 @@
|
|||||||
|
package io.nekohasekai.sagernet.database
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteCantOpenDatabaseException
|
||||||
|
import io.nekohasekai.sagernet.R
|
||||||
|
import io.nekohasekai.sagernet.aidl.SpeedDisplayData
|
||||||
|
import io.nekohasekai.sagernet.aidl.TrafficData
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean
|
||||||
|
import io.nekohasekai.sagernet.ktx.Logs
|
||||||
|
import io.nekohasekai.sagernet.ktx.app
|
||||||
|
import io.nekohasekai.sagernet.ktx.applyDefaultValues
|
||||||
|
import java.io.IOException
|
||||||
|
import java.sql.SQLException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
object ProfileManager {
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
suspend fun onAdd(profile: ProxyEntity)
|
||||||
|
suspend fun onUpdated(data: TrafficData)
|
||||||
|
suspend fun onUpdated(profile: ProxyEntity)
|
||||||
|
suspend fun onRemoved(groupId: Long, profileId: Long)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuleListener {
|
||||||
|
suspend fun onAdd(rule: RuleEntity)
|
||||||
|
suspend fun onUpdated(rule: RuleEntity)
|
||||||
|
suspend fun onRemoved(ruleId: Long)
|
||||||
|
suspend fun onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val listeners = ArrayList<Listener>()
|
||||||
|
private val ruleListeners = ArrayList<RuleListener>()
|
||||||
|
|
||||||
|
suspend fun iterator(what: suspend Listener.() -> Unit) {
|
||||||
|
synchronized(listeners) {
|
||||||
|
listeners.toList()
|
||||||
|
}.forEach { listener ->
|
||||||
|
what(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun ruleIterator(what: suspend RuleListener.() -> Unit) {
|
||||||
|
val ruleListeners = synchronized(ruleListeners) {
|
||||||
|
ruleListeners.toList()
|
||||||
|
}
|
||||||
|
for (listener in ruleListeners) {
|
||||||
|
what(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addListener(listener: Listener) {
|
||||||
|
synchronized(listeners) {
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeListener(listener: Listener) {
|
||||||
|
synchronized(listeners) {
|
||||||
|
listeners.remove(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addListener(listener: RuleListener) {
|
||||||
|
synchronized(ruleListeners) {
|
||||||
|
ruleListeners.add(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeListener(listener: RuleListener) {
|
||||||
|
synchronized(ruleListeners) {
|
||||||
|
ruleListeners.remove(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createProfile(groupId: Long, bean: AbstractBean): ProxyEntity {
|
||||||
|
bean.applyDefaultValues()
|
||||||
|
|
||||||
|
val profile = ProxyEntity(groupId = groupId).apply {
|
||||||
|
id = 0
|
||||||
|
putBean(bean)
|
||||||
|
userOrder = SagerDatabase.proxyDao.nextOrder(groupId) ?: 1
|
||||||
|
}
|
||||||
|
profile.id = SagerDatabase.proxyDao.addProxy(profile)
|
||||||
|
iterator { onAdd(profile) }
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateProfile(profile: ProxyEntity) {
|
||||||
|
SagerDatabase.proxyDao.updateProxy(profile)
|
||||||
|
iterator { onUpdated(profile) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateProfile(profiles: List<ProxyEntity>) {
|
||||||
|
SagerDatabase.proxyDao.updateProxy(profiles)
|
||||||
|
profiles.forEach {
|
||||||
|
iterator { onUpdated(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteProfile2(groupId: Long, profileId: Long) {
|
||||||
|
if (SagerDatabase.proxyDao.deleteById(profileId) == 0) return
|
||||||
|
if (DataStore.selectedProxy == profileId) {
|
||||||
|
DataStore.selectedProxy = 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteProfile(groupId: Long, profileId: Long) {
|
||||||
|
if (SagerDatabase.proxyDao.deleteById(profileId) == 0) return
|
||||||
|
if (DataStore.selectedProxy == profileId) {
|
||||||
|
DataStore.selectedProxy = 0L
|
||||||
|
}
|
||||||
|
iterator { onRemoved(groupId, profileId) }
|
||||||
|
if (SagerDatabase.proxyDao.countByGroup(groupId) > 1) {
|
||||||
|
GroupManager.rearrange(groupId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProfile(profileId: Long): ProxyEntity? {
|
||||||
|
if (profileId == 0L) return null
|
||||||
|
return try {
|
||||||
|
SagerDatabase.proxyDao.getById(profileId)
|
||||||
|
} catch (ex: SQLiteCantOpenDatabaseException) {
|
||||||
|
throw IOException(ex)
|
||||||
|
} catch (ex: SQLException) {
|
||||||
|
Logs.w(ex)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProfiles(profileIds: List<Long>): List<ProxyEntity> {
|
||||||
|
if (profileIds.isEmpty()) return listOf()
|
||||||
|
return try {
|
||||||
|
SagerDatabase.proxyDao.getEntities(profileIds)
|
||||||
|
} catch (ex: SQLiteCantOpenDatabaseException) {
|
||||||
|
throw IOException(ex)
|
||||||
|
} catch (ex: SQLException) {
|
||||||
|
Logs.w(ex)
|
||||||
|
listOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// postUpdate: post to listeners, don't change the DB
|
||||||
|
|
||||||
|
suspend fun postUpdate(profileId: Long) {
|
||||||
|
postUpdate(getProfile(profileId) ?: return)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun postUpdate(profile: ProxyEntity) {
|
||||||
|
iterator { onUpdated(profile) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun postUpdate(data: TrafficData) {
|
||||||
|
iterator { onUpdated(data) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createRule(rule: RuleEntity, post: Boolean = true): RuleEntity {
|
||||||
|
rule.userOrder = SagerDatabase.rulesDao.nextOrder() ?: 1
|
||||||
|
rule.id = SagerDatabase.rulesDao.createRule(rule)
|
||||||
|
if (post) {
|
||||||
|
ruleIterator { onAdd(rule) }
|
||||||
|
}
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateRule(rule: RuleEntity) {
|
||||||
|
SagerDatabase.rulesDao.updateRule(rule)
|
||||||
|
ruleIterator { onUpdated(rule) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteRule(ruleId: Long) {
|
||||||
|
SagerDatabase.rulesDao.deleteById(ruleId)
|
||||||
|
ruleIterator { onRemoved(ruleId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteRules(rules: List<RuleEntity>) {
|
||||||
|
SagerDatabase.rulesDao.deleteRules(rules)
|
||||||
|
ruleIterator {
|
||||||
|
rules.forEach {
|
||||||
|
onRemoved(it.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getRules(): List<RuleEntity> {
|
||||||
|
var rules = SagerDatabase.rulesDao.allRules()
|
||||||
|
if (rules.isEmpty() && !DataStore.rulesFirstCreate) {
|
||||||
|
DataStore.rulesFirstCreate = true
|
||||||
|
createRule(
|
||||||
|
RuleEntity(
|
||||||
|
name = app.getString(R.string.route_opt_block_quic),
|
||||||
|
port = "443",
|
||||||
|
network = "udp",
|
||||||
|
outbound = -2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
createRule(
|
||||||
|
RuleEntity(
|
||||||
|
name = app.getString(R.string.route_opt_block_ads),
|
||||||
|
domains = "geosite:category-ads-all",
|
||||||
|
outbound = -2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
createRule(
|
||||||
|
RuleEntity(
|
||||||
|
name = app.getString(R.string.route_opt_block_analysis),
|
||||||
|
domains = app.assets.open("analysis.txt").use {
|
||||||
|
it.bufferedReader()
|
||||||
|
.readLines()
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.joinToString("\n")
|
||||||
|
},
|
||||||
|
outbound = -2,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var country = Locale.getDefault().country.lowercase()
|
||||||
|
var displayCountry = Locale.getDefault().displayCountry
|
||||||
|
if (country in arrayOf(
|
||||||
|
"ir"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
createRule(
|
||||||
|
RuleEntity(
|
||||||
|
name = app.getString(R.string.route_bypass_domain, displayCountry),
|
||||||
|
domains = "domain:$country",
|
||||||
|
outbound = -1
|
||||||
|
), false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
country = Locale.CHINA.country.lowercase()
|
||||||
|
displayCountry = Locale.CHINA.displayCountry
|
||||||
|
createRule(
|
||||||
|
RuleEntity(
|
||||||
|
name = app.getString(R.string.route_play_store, displayCountry),
|
||||||
|
domains = "domain:googleapis.cn",
|
||||||
|
), false
|
||||||
|
)
|
||||||
|
createRule(
|
||||||
|
RuleEntity(
|
||||||
|
name = app.getString(R.string.route_bypass_domain, displayCountry),
|
||||||
|
domains = "geosite:$country",
|
||||||
|
outbound = -1
|
||||||
|
), false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
createRule(
|
||||||
|
RuleEntity(
|
||||||
|
name = app.getString(R.string.route_bypass_ip, displayCountry),
|
||||||
|
ip = "geoip:$country",
|
||||||
|
outbound = -1
|
||||||
|
), false
|
||||||
|
)
|
||||||
|
rules = SagerDatabase.rulesDao.allRules()
|
||||||
|
}
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,489 @@
|
|||||||
|
package io.nekohasekai.sagernet.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.room.*
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput
|
||||||
|
import io.nekohasekai.sagernet.R
|
||||||
|
import io.nekohasekai.sagernet.fmt.*
|
||||||
|
import io.nekohasekai.sagernet.fmt.http.HttpBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.http.toUri
|
||||||
|
import io.nekohasekai.sagernet.fmt.hysteria.*
|
||||||
|
import io.nekohasekai.sagernet.fmt.internal.ChainBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.naive.NaiveBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.naive.buildNaiveConfig
|
||||||
|
import io.nekohasekai.sagernet.fmt.naive.toUri
|
||||||
|
import io.nekohasekai.sagernet.fmt.shadowsocks.*
|
||||||
|
import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.socks.toUri
|
||||||
|
import io.nekohasekai.sagernet.fmt.ssh.SSHBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.trojan.toUri
|
||||||
|
import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig
|
||||||
|
import io.nekohasekai.sagernet.fmt.trojan_go.toUri
|
||||||
|
import io.nekohasekai.sagernet.fmt.tuic.TuicBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.tuic.buildTuicConfig
|
||||||
|
import io.nekohasekai.sagernet.fmt.v2ray.*
|
||||||
|
import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean
|
||||||
|
import io.nekohasekai.sagernet.ktx.app
|
||||||
|
import io.nekohasekai.sagernet.ktx.applyDefaultValues
|
||||||
|
import io.nekohasekai.sagernet.ui.profile.*
|
||||||
|
import moe.matsuri.nb4a.Protocols
|
||||||
|
import moe.matsuri.nb4a.proxy.neko.*
|
||||||
|
import moe.matsuri.nb4a.proxy.config.ConfigBean
|
||||||
|
import moe.matsuri.nb4a.proxy.config.ConfigSettingActivity
|
||||||
|
import moe.matsuri.nb4a.proxy.neko.NekoBean
|
||||||
|
import moe.matsuri.nb4a.proxy.neko.NekoSettingActivity
|
||||||
|
import moe.matsuri.nb4a.proxy.neko.haveStandardLink
|
||||||
|
import moe.matsuri.nb4a.proxy.neko.shareLink
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "proxy_entities", indices = [Index("groupId", name = "groupId")]
|
||||||
|
)
|
||||||
|
data class ProxyEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true) var id: Long = 0L,
|
||||||
|
var groupId: Long = 0L,
|
||||||
|
var type: Int = 0,
|
||||||
|
var userOrder: Long = 0L,
|
||||||
|
var tx: Long = 0L,
|
||||||
|
var rx: Long = 0L,
|
||||||
|
var status: Int = 0,
|
||||||
|
var ping: Int = 0,
|
||||||
|
var uuid: String = "",
|
||||||
|
var error: String? = null,
|
||||||
|
var socksBean: SOCKSBean? = null,
|
||||||
|
var httpBean: HttpBean? = null,
|
||||||
|
var ssBean: ShadowsocksBean? = null,
|
||||||
|
var vmessBean: VMessBean? = null,
|
||||||
|
var trojanBean: TrojanBean? = null,
|
||||||
|
var trojanGoBean: TrojanGoBean? = null,
|
||||||
|
var naiveBean: NaiveBean? = null,
|
||||||
|
var hysteriaBean: HysteriaBean? = null,
|
||||||
|
var tuicBean: TuicBean? = null,
|
||||||
|
var sshBean: SSHBean? = null,
|
||||||
|
var wgBean: WireGuardBean? = null,
|
||||||
|
var chainBean: ChainBean? = null,
|
||||||
|
var nekoBean: NekoBean? = null,
|
||||||
|
var configBean: ConfigBean? = null,
|
||||||
|
) : Serializable() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TYPE_SOCKS = 0
|
||||||
|
const val TYPE_HTTP = 1
|
||||||
|
const val TYPE_SS = 2
|
||||||
|
const val TYPE_VMESS = 4
|
||||||
|
|
||||||
|
const val TYPE_TROJAN = 6
|
||||||
|
const val TYPE_TROJAN_GO = 7
|
||||||
|
const val TYPE_NAIVE = 9
|
||||||
|
const val TYPE_HYSTERIA = 15
|
||||||
|
|
||||||
|
const val TYPE_SSH = 17
|
||||||
|
const val TYPE_WG = 18
|
||||||
|
|
||||||
|
const val TYPE_TUIC = 20
|
||||||
|
|
||||||
|
const val TYPE_CONFIG = 998
|
||||||
|
const val TYPE_NEKO = 999
|
||||||
|
|
||||||
|
const val TYPE_CHAIN = 8
|
||||||
|
|
||||||
|
val chainName by lazy { app.getString(R.string.proxy_chain) }
|
||||||
|
|
||||||
|
private val placeHolderBean = SOCKSBean().applyDefaultValues()
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATOR = object : Serializable.CREATOR<ProxyEntity>() {
|
||||||
|
|
||||||
|
override fun newInstance(): ProxyEntity {
|
||||||
|
return ProxyEntity()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<ProxyEntity?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Transient
|
||||||
|
var info: String = ""
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Transient
|
||||||
|
var dirty: Boolean = false
|
||||||
|
|
||||||
|
override fun initializeDefaultValues() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serializeToBuffer(output: ByteBufferOutput) {
|
||||||
|
output.writeInt(0)
|
||||||
|
|
||||||
|
output.writeLong(id)
|
||||||
|
output.writeLong(groupId)
|
||||||
|
output.writeInt(type)
|
||||||
|
output.writeLong(userOrder)
|
||||||
|
output.writeLong(tx)
|
||||||
|
output.writeLong(rx)
|
||||||
|
output.writeInt(status)
|
||||||
|
output.writeInt(ping)
|
||||||
|
output.writeString(uuid)
|
||||||
|
output.writeString(error)
|
||||||
|
|
||||||
|
val data = KryoConverters.serialize(requireBean())
|
||||||
|
output.writeVarInt(data.size, true)
|
||||||
|
output.writeBytes(data)
|
||||||
|
|
||||||
|
output.writeBoolean(dirty)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserializeFromBuffer(input: ByteBufferInput) {
|
||||||
|
val version = input.readInt()
|
||||||
|
|
||||||
|
id = input.readLong()
|
||||||
|
groupId = input.readLong()
|
||||||
|
type = input.readInt()
|
||||||
|
userOrder = input.readLong()
|
||||||
|
tx = input.readLong()
|
||||||
|
rx = input.readLong()
|
||||||
|
status = input.readInt()
|
||||||
|
ping = input.readInt()
|
||||||
|
uuid = input.readString()
|
||||||
|
error = input.readString()
|
||||||
|
putByteArray(input.readBytes(input.readVarInt(true)))
|
||||||
|
|
||||||
|
dirty = input.readBoolean()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun putByteArray(byteArray: ByteArray) {
|
||||||
|
when (type) {
|
||||||
|
TYPE_SOCKS -> socksBean = KryoConverters.socksDeserialize(byteArray)
|
||||||
|
TYPE_HTTP -> httpBean = KryoConverters.httpDeserialize(byteArray)
|
||||||
|
TYPE_SS -> ssBean = KryoConverters.shadowsocksDeserialize(byteArray)
|
||||||
|
TYPE_VMESS -> vmessBean = KryoConverters.vmessDeserialize(byteArray)
|
||||||
|
TYPE_TROJAN -> trojanBean = KryoConverters.trojanDeserialize(byteArray)
|
||||||
|
TYPE_TROJAN_GO -> trojanGoBean = KryoConverters.trojanGoDeserialize(byteArray)
|
||||||
|
TYPE_NAIVE -> naiveBean = KryoConverters.naiveDeserialize(byteArray)
|
||||||
|
TYPE_HYSTERIA -> hysteriaBean = KryoConverters.hysteriaDeserialize(byteArray)
|
||||||
|
TYPE_SSH -> sshBean = KryoConverters.sshDeserialize(byteArray)
|
||||||
|
TYPE_WG -> wgBean = KryoConverters.wireguardDeserialize(byteArray)
|
||||||
|
TYPE_TUIC -> tuicBean = KryoConverters.tuicDeserialize(byteArray)
|
||||||
|
TYPE_CHAIN -> chainBean = KryoConverters.chainDeserialize(byteArray)
|
||||||
|
TYPE_NEKO -> nekoBean = KryoConverters.nekoDeserialize(byteArray)
|
||||||
|
TYPE_CONFIG -> configBean = KryoConverters.configDeserialize(byteArray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayType() = when (type) {
|
||||||
|
TYPE_SOCKS -> socksBean!!.protocolName()
|
||||||
|
TYPE_HTTP -> if (httpBean!!.isTLS()) "HTTPS" else "HTTP"
|
||||||
|
TYPE_SS -> "Shadowsocks"
|
||||||
|
TYPE_VMESS -> if (vmessBean!!.isVLESS) "VLESS" else "VMess"
|
||||||
|
TYPE_TROJAN -> "Trojan"
|
||||||
|
TYPE_TROJAN_GO -> "Trojan-Go"
|
||||||
|
TYPE_NAIVE -> "Naïve"
|
||||||
|
TYPE_HYSTERIA -> "Hysteria"
|
||||||
|
TYPE_SSH -> "SSH"
|
||||||
|
TYPE_WG -> "WireGuard"
|
||||||
|
TYPE_TUIC -> "TUIC"
|
||||||
|
TYPE_CHAIN -> chainName
|
||||||
|
TYPE_NEKO -> nekoBean!!.displayType()
|
||||||
|
TYPE_CONFIG -> configBean!!.displayType()
|
||||||
|
else -> "Undefined type $type"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayName() = requireBean().displayName()
|
||||||
|
fun displayAddress() = requireBean().displayAddress()
|
||||||
|
|
||||||
|
fun requireBean(): AbstractBean {
|
||||||
|
return when (type) {
|
||||||
|
TYPE_SOCKS -> socksBean
|
||||||
|
TYPE_HTTP -> httpBean
|
||||||
|
TYPE_SS -> ssBean
|
||||||
|
TYPE_VMESS -> vmessBean
|
||||||
|
TYPE_TROJAN -> trojanBean
|
||||||
|
TYPE_TROJAN_GO -> trojanGoBean
|
||||||
|
TYPE_NAIVE -> naiveBean
|
||||||
|
TYPE_HYSTERIA -> hysteriaBean
|
||||||
|
TYPE_SSH -> sshBean
|
||||||
|
TYPE_WG -> wgBean
|
||||||
|
TYPE_TUIC -> tuicBean
|
||||||
|
TYPE_CHAIN -> chainBean
|
||||||
|
TYPE_NEKO -> nekoBean
|
||||||
|
TYPE_CONFIG -> configBean
|
||||||
|
else -> error("Undefined type $type")
|
||||||
|
} ?: error("Null ${displayType()} profile")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun haveLink(): Boolean {
|
||||||
|
return when (type) {
|
||||||
|
TYPE_CHAIN -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun haveStandardLink(): Boolean {
|
||||||
|
return when (requireBean()) {
|
||||||
|
is TuicBean -> false
|
||||||
|
is SSHBean -> false
|
||||||
|
is WireGuardBean -> false
|
||||||
|
is NekoBean -> nekoBean!!.haveStandardLink()
|
||||||
|
is ConfigBean -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toLink(compact: Boolean = false): String? = with(requireBean()) {
|
||||||
|
when (this) {
|
||||||
|
is SOCKSBean -> toUri()
|
||||||
|
is HttpBean -> toUri()
|
||||||
|
is ShadowsocksBean -> toUri()
|
||||||
|
is VMessBean -> if (compact) toV2rayN() else toUri()
|
||||||
|
is TrojanBean -> toUri()
|
||||||
|
is TrojanGoBean -> toUri()
|
||||||
|
is NaiveBean -> toUri()
|
||||||
|
is HysteriaBean -> toUri()
|
||||||
|
is SSHBean -> toUniversalLink()
|
||||||
|
is WireGuardBean -> toUniversalLink()
|
||||||
|
is TuicBean -> toUniversalLink()
|
||||||
|
is ConfigBean -> toUniversalLink()
|
||||||
|
is NekoBean -> shareLink()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportConfig(): Pair<String, String> {
|
||||||
|
var name = "${requireBean().displayName()}.json"
|
||||||
|
|
||||||
|
return with(requireBean()) {
|
||||||
|
StringBuilder().apply {
|
||||||
|
val config = buildConfig(this@ProxyEntity)
|
||||||
|
append(config.config)
|
||||||
|
|
||||||
|
if (!config.externalIndex.all { it.chain.isEmpty() }) {
|
||||||
|
name = "profiles.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((chain) in config.externalIndex) {
|
||||||
|
chain.entries.forEachIndexed { index, (port, profile) ->
|
||||||
|
when (val bean = profile.requireBean()) {
|
||||||
|
is TrojanGoBean -> {
|
||||||
|
append("\n\n")
|
||||||
|
append(bean.buildTrojanGoConfig(port))
|
||||||
|
}
|
||||||
|
is NaiveBean -> {
|
||||||
|
append("\n\n")
|
||||||
|
append(bean.buildNaiveConfig(port))
|
||||||
|
}
|
||||||
|
is HysteriaBean -> {
|
||||||
|
append("\n\n")
|
||||||
|
append(bean.buildHysteriaConfig(port, null))
|
||||||
|
}
|
||||||
|
is TuicBean -> {
|
||||||
|
append("\n\n")
|
||||||
|
append(bean.buildTuicConfig(port, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toString()
|
||||||
|
} to name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun needExternal(): Boolean {
|
||||||
|
return when (type) {
|
||||||
|
TYPE_TROJAN_GO -> true
|
||||||
|
TYPE_NAIVE -> true
|
||||||
|
TYPE_HYSTERIA -> !hysteriaBean!!.canUseSingBox()
|
||||||
|
TYPE_TUIC -> true
|
||||||
|
TYPE_NEKO -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isV2RayNetworkTcp(): Boolean {
|
||||||
|
val bean = requireBean() as StandardV2RayBean
|
||||||
|
return when (bean.type) {
|
||||||
|
"tcp", "ws", "http" -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun needCoreMux(): Boolean {
|
||||||
|
return when (type) {
|
||||||
|
TYPE_VMESS -> isV2RayNetworkTcp() && Protocols.shouldEnableMux("vmess")
|
||||||
|
TYPE_TROJAN -> isV2RayNetworkTcp() && Protocols.shouldEnableMux("trojan")
|
||||||
|
TYPE_SS -> !ssBean!!.sUoT && Protocols.shouldEnableMux("shadowsocks")
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putBean(bean: AbstractBean): ProxyEntity {
|
||||||
|
socksBean = null
|
||||||
|
httpBean = null
|
||||||
|
ssBean = null
|
||||||
|
vmessBean = null
|
||||||
|
trojanBean = null
|
||||||
|
trojanGoBean = null
|
||||||
|
naiveBean = null
|
||||||
|
hysteriaBean = null
|
||||||
|
sshBean = null
|
||||||
|
wgBean = null
|
||||||
|
tuicBean = null
|
||||||
|
chainBean = null
|
||||||
|
configBean = null
|
||||||
|
nekoBean = null
|
||||||
|
|
||||||
|
when (bean) {
|
||||||
|
is SOCKSBean -> {
|
||||||
|
type = TYPE_SOCKS
|
||||||
|
socksBean = bean
|
||||||
|
}
|
||||||
|
is HttpBean -> {
|
||||||
|
type = TYPE_HTTP
|
||||||
|
httpBean = bean
|
||||||
|
}
|
||||||
|
is ShadowsocksBean -> {
|
||||||
|
type = TYPE_SS
|
||||||
|
ssBean = bean
|
||||||
|
}
|
||||||
|
is VMessBean -> {
|
||||||
|
type = TYPE_VMESS
|
||||||
|
vmessBean = bean
|
||||||
|
}
|
||||||
|
is TrojanBean -> {
|
||||||
|
type = TYPE_TROJAN
|
||||||
|
trojanBean = bean
|
||||||
|
}
|
||||||
|
is TrojanGoBean -> {
|
||||||
|
type = TYPE_TROJAN_GO
|
||||||
|
trojanGoBean = bean
|
||||||
|
}
|
||||||
|
is NaiveBean -> {
|
||||||
|
type = TYPE_NAIVE
|
||||||
|
naiveBean = bean
|
||||||
|
}
|
||||||
|
is HysteriaBean -> {
|
||||||
|
type = TYPE_HYSTERIA
|
||||||
|
hysteriaBean = bean
|
||||||
|
}
|
||||||
|
is SSHBean -> {
|
||||||
|
type = TYPE_SSH
|
||||||
|
sshBean = bean
|
||||||
|
}
|
||||||
|
is WireGuardBean -> {
|
||||||
|
type = TYPE_WG
|
||||||
|
wgBean = bean
|
||||||
|
}
|
||||||
|
is TuicBean -> {
|
||||||
|
type = TYPE_TUIC
|
||||||
|
tuicBean = bean
|
||||||
|
}
|
||||||
|
is ChainBean -> {
|
||||||
|
type = TYPE_CHAIN
|
||||||
|
chainBean = bean
|
||||||
|
}
|
||||||
|
is NekoBean -> {
|
||||||
|
type = TYPE_NEKO
|
||||||
|
nekoBean = bean
|
||||||
|
}
|
||||||
|
is ConfigBean -> {
|
||||||
|
type = TYPE_CONFIG
|
||||||
|
configBean = bean
|
||||||
|
}
|
||||||
|
else -> error("Undefined type $type")
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun settingIntent(ctx: Context, isSubscription: Boolean): Intent {
|
||||||
|
return Intent(
|
||||||
|
ctx, when (type) {
|
||||||
|
TYPE_SOCKS -> SocksSettingsActivity::class.java
|
||||||
|
TYPE_HTTP -> HttpSettingsActivity::class.java
|
||||||
|
TYPE_SS -> ShadowsocksSettingsActivity::class.java
|
||||||
|
TYPE_VMESS -> VMessSettingsActivity::class.java
|
||||||
|
TYPE_TROJAN -> TrojanSettingsActivity::class.java
|
||||||
|
TYPE_TROJAN_GO -> TrojanGoSettingsActivity::class.java
|
||||||
|
TYPE_NAIVE -> NaiveSettingsActivity::class.java
|
||||||
|
TYPE_HYSTERIA -> HysteriaSettingsActivity::class.java
|
||||||
|
TYPE_SSH -> SSHSettingsActivity::class.java
|
||||||
|
TYPE_WG -> WireGuardSettingsActivity::class.java
|
||||||
|
TYPE_TUIC -> TuicSettingsActivity::class.java
|
||||||
|
TYPE_CHAIN -> ChainSettingsActivity::class.java
|
||||||
|
TYPE_NEKO -> NekoSettingActivity::class.java
|
||||||
|
TYPE_CONFIG -> ConfigSettingActivity::class.java
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
).apply {
|
||||||
|
putExtra(ProfileSettingsActivity.EXTRA_PROFILE_ID, id)
|
||||||
|
putExtra(ProfileSettingsActivity.EXTRA_IS_SUBSCRIPTION, isSubscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.room.Dao
|
||||||
|
interface Dao {
|
||||||
|
|
||||||
|
@Query("select * from proxy_entities")
|
||||||
|
fun getAll(): List<ProxyEntity>
|
||||||
|
|
||||||
|
@Query("SELECT id FROM proxy_entities WHERE groupId = :groupId ORDER BY userOrder")
|
||||||
|
fun getIdsByGroup(groupId: Long): List<Long>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM proxy_entities WHERE groupId = :groupId ORDER BY userOrder")
|
||||||
|
fun getByGroup(groupId: Long): List<ProxyEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM proxy_entities WHERE id in (:proxyIds)")
|
||||||
|
fun getEntities(proxyIds: List<Long>): List<ProxyEntity>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM proxy_entities WHERE groupId = :groupId")
|
||||||
|
fun countByGroup(groupId: Long): Long
|
||||||
|
|
||||||
|
@Query("SELECT MAX(userOrder) + 1 FROM proxy_entities WHERE groupId = :groupId")
|
||||||
|
fun nextOrder(groupId: Long): Long?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM proxy_entities WHERE id = :proxyId")
|
||||||
|
fun getById(proxyId: Long): ProxyEntity?
|
||||||
|
|
||||||
|
@Query("DELETE FROM proxy_entities WHERE id IN (:proxyId)")
|
||||||
|
fun deleteById(proxyId: Long): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM proxy_entities WHERE groupId = :groupId")
|
||||||
|
fun deleteByGroup(groupId: Long)
|
||||||
|
|
||||||
|
@Query("DELETE FROM proxy_entities WHERE groupId in (:groupId)")
|
||||||
|
fun deleteByGroup(groupId: LongArray)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun deleteProxy(proxy: ProxyEntity): Int
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun deleteProxy(proxies: List<ProxyEntity>): Int
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun updateProxy(proxy: ProxyEntity): Int
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun updateProxy(proxies: List<ProxyEntity>): Int
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun addProxy(proxy: ProxyEntity): Long
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insert(proxies: List<ProxyEntity>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM proxy_entities WHERE groupId = :groupId")
|
||||||
|
fun deleteAll(groupId: Long): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM proxy_entities")
|
||||||
|
fun reset()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
140
app/src/main/java/io/nekohasekai/sagernet/database/ProxyGroup.kt
Normal file
140
app/src/main/java/io/nekohasekai/sagernet/database/ProxyGroup.kt
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package io.nekohasekai.sagernet.database
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput
|
||||||
|
import io.nekohasekai.sagernet.GroupOrder
|
||||||
|
import io.nekohasekai.sagernet.GroupType
|
||||||
|
import io.nekohasekai.sagernet.R
|
||||||
|
import io.nekohasekai.sagernet.fmt.Serializable
|
||||||
|
import io.nekohasekai.sagernet.ktx.app
|
||||||
|
import io.nekohasekai.sagernet.ktx.applyDefaultValues
|
||||||
|
|
||||||
|
@Entity(tableName = "proxy_groups")
|
||||||
|
data class ProxyGroup(
|
||||||
|
@PrimaryKey(autoGenerate = true) var id: Long = 0L,
|
||||||
|
var userOrder: Long = 0L,
|
||||||
|
var ungrouped: Boolean = false,
|
||||||
|
var name: String? = null,
|
||||||
|
var type: Int = GroupType.BASIC,
|
||||||
|
var subscription: SubscriptionBean? = null,
|
||||||
|
var order: Int = GroupOrder.ORIGIN,
|
||||||
|
) : Serializable() {
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var export = false
|
||||||
|
|
||||||
|
override fun initializeDefaultValues() {
|
||||||
|
subscription?.applyDefaultValues()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serializeToBuffer(output: ByteBufferOutput) {
|
||||||
|
if (export) {
|
||||||
|
|
||||||
|
output.writeInt(0)
|
||||||
|
output.writeString(name)
|
||||||
|
output.writeInt(type)
|
||||||
|
val subscription = subscription!!
|
||||||
|
subscription.serializeForShare(output)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
output.writeInt(0)
|
||||||
|
output.writeLong(id)
|
||||||
|
output.writeLong(userOrder)
|
||||||
|
output.writeBoolean(ungrouped)
|
||||||
|
output.writeString(name)
|
||||||
|
output.writeInt(type)
|
||||||
|
|
||||||
|
if (type == GroupType.SUBSCRIPTION) {
|
||||||
|
subscription?.serializeToBuffer(output)
|
||||||
|
}
|
||||||
|
output.writeInt(order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserializeFromBuffer(input: ByteBufferInput) {
|
||||||
|
if (export) {
|
||||||
|
val version = input.readInt()
|
||||||
|
|
||||||
|
name = input.readString()
|
||||||
|
type = input.readInt()
|
||||||
|
val subscription = SubscriptionBean()
|
||||||
|
this.subscription = subscription
|
||||||
|
|
||||||
|
subscription.deserializeFromShare(input)
|
||||||
|
} else {
|
||||||
|
val version = input.readInt()
|
||||||
|
|
||||||
|
id = input.readLong()
|
||||||
|
userOrder = input.readLong()
|
||||||
|
ungrouped = input.readBoolean()
|
||||||
|
name = input.readString()
|
||||||
|
type = input.readInt()
|
||||||
|
|
||||||
|
if (type == GroupType.SUBSCRIPTION) {
|
||||||
|
val subscription = SubscriptionBean()
|
||||||
|
this.subscription = subscription
|
||||||
|
|
||||||
|
subscription.deserializeFromBuffer(input)
|
||||||
|
}
|
||||||
|
order = input.readInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayName(): String {
|
||||||
|
return name.takeIf { !it.isNullOrBlank() } ?: app.getString(R.string.group_default)
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.room.Dao
|
||||||
|
interface Dao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM proxy_groups ORDER BY userOrder")
|
||||||
|
fun allGroups(): List<ProxyGroup>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM proxy_groups WHERE type = ${GroupType.SUBSCRIPTION}")
|
||||||
|
suspend fun subscriptions(): List<ProxyGroup>
|
||||||
|
|
||||||
|
@Query("SELECT MAX(userOrder) + 1 FROM proxy_groups")
|
||||||
|
fun nextOrder(): Long?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM proxy_groups WHERE id = :groupId")
|
||||||
|
fun getById(groupId: Long): ProxyGroup?
|
||||||
|
|
||||||
|
@Query("DELETE FROM proxy_groups WHERE id = :groupId")
|
||||||
|
fun deleteById(groupId: Long): Int
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun deleteGroup(group: ProxyGroup)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun deleteGroup(groupList: List<ProxyGroup>)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun createGroup(group: ProxyGroup): Long
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun updateGroup(group: ProxyGroup)
|
||||||
|
|
||||||
|
@Query("DELETE FROM proxy_groups")
|
||||||
|
fun reset()
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insert(groupList: List<ProxyGroup>)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val CREATOR = object : Serializable.CREATOR<ProxyGroup>() {
|
||||||
|
|
||||||
|
override fun newInstance(): ProxyGroup {
|
||||||
|
return ProxyGroup()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<ProxyGroup?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
106
app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt
Normal file
106
app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package io.nekohasekai.sagernet.database
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.*
|
||||||
|
import io.nekohasekai.sagernet.R
|
||||||
|
import io.nekohasekai.sagernet.ktx.app
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Entity(tableName = "rules")
|
||||||
|
@Parcelize
|
||||||
|
data class RuleEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true) var id: Long = 0L,
|
||||||
|
var name: String = "",
|
||||||
|
var userOrder: Long = 0L,
|
||||||
|
var enabled: Boolean = false,
|
||||||
|
var domains: String = "",
|
||||||
|
var ip: String = "",
|
||||||
|
var port: String = "",
|
||||||
|
var sourcePort: String = "",
|
||||||
|
var network: String = "",
|
||||||
|
var source: String = "",
|
||||||
|
var protocol: String = "",
|
||||||
|
var outbound: Long = 0,
|
||||||
|
var packages: List<String> = listOf(),
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun displayName(): String {
|
||||||
|
return name.takeIf { it.isNotBlank() } ?: "Rule $id"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mkSummary(): String {
|
||||||
|
var summary = ""
|
||||||
|
if (domains.isNotBlank()) summary += "$domains\n"
|
||||||
|
if (ip.isNotBlank()) summary += "$ip\n"
|
||||||
|
if (source.isNotBlank()) summary += "source: $source\n"
|
||||||
|
if (sourcePort.isNotBlank()) summary += "sourcePort: $sourcePort\n"
|
||||||
|
if (port.isNotBlank()) summary += "port: $port\n"
|
||||||
|
if (network.isNotBlank()) summary += "network: $network\n"
|
||||||
|
if (protocol.isNotBlank()) summary += "protocol: $protocol\n"
|
||||||
|
if (packages.isNotEmpty()) summary += app.getString(
|
||||||
|
R.string.apps_message, packages.size
|
||||||
|
) + "\n"
|
||||||
|
val lines = summary.trim().split("\n")
|
||||||
|
return if (lines.size > 3) {
|
||||||
|
lines.subList(0, 3).joinToString("\n", postfix = "\n...")
|
||||||
|
} else {
|
||||||
|
summary.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayOutbound(): String {
|
||||||
|
return when (outbound) {
|
||||||
|
0L -> app.getString(R.string.route_proxy)
|
||||||
|
-1L -> app.getString(R.string.route_bypass)
|
||||||
|
-2L -> app.getString(R.string.route_block)
|
||||||
|
else -> ProfileManager.getProfile(outbound)?.displayName()
|
||||||
|
?: app.getString(R.string.route_proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.room.Dao
|
||||||
|
interface Dao {
|
||||||
|
|
||||||
|
@Query("SELECT * from rules WHERE (packages != '') AND enabled = 1")
|
||||||
|
fun checkVpnNeeded(): List<RuleEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM rules ORDER BY userOrder")
|
||||||
|
fun allRules(): List<RuleEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM rules WHERE enabled = :enabled ORDER BY userOrder")
|
||||||
|
fun enabledRules(enabled: Boolean = true): List<RuleEntity>
|
||||||
|
|
||||||
|
@Query("SELECT MAX(userOrder) + 1 FROM rules")
|
||||||
|
fun nextOrder(): Long?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM rules WHERE id = :ruleId")
|
||||||
|
fun getById(ruleId: Long): RuleEntity?
|
||||||
|
|
||||||
|
@Query("DELETE FROM rules WHERE id = :ruleId")
|
||||||
|
fun deleteById(ruleId: Long): Int
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun deleteRule(rule: RuleEntity)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun deleteRules(rules: List<RuleEntity>)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun createRule(rule: RuleEntity): Long
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun updateRule(rule: RuleEntity)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun updateRules(rules: List<RuleEntity>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM rules")
|
||||||
|
fun reset()
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insert(rules: List<RuleEntity>)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package io.nekohasekai.sagernet.database
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import dev.matrix.roomigrant.GenerateRoomMigrations
|
||||||
|
import io.nekohasekai.sagernet.Key
|
||||||
|
import io.nekohasekai.sagernet.SagerNet
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters
|
||||||
|
import io.nekohasekai.sagernet.fmt.gson.GsonConverters
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class],
|
||||||
|
version = 1
|
||||||
|
)
|
||||||
|
@TypeConverters(value = [KryoConverters::class, GsonConverters::class])
|
||||||
|
@GenerateRoomMigrations
|
||||||
|
abstract class SagerDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||||
|
private val instance by lazy {
|
||||||
|
SagerNet.application.getDatabasePath(Key.DB_PROFILE).parentFile?.mkdirs()
|
||||||
|
Room.databaseBuilder(SagerNet.application, SagerDatabase::class.java, Key.DB_PROFILE)
|
||||||
|
.addMigrations(*SagerDatabase_Migrations.build())
|
||||||
|
.allowMainThreadQueries()
|
||||||
|
.enableMultiInstanceInvalidation()
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.setQueryExecutor { GlobalScope.launch { it.run() } }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val groupDao get() = instance.groupDao()
|
||||||
|
val proxyDao get() = instance.proxyDao()
|
||||||
|
val rulesDao get() = instance.rulesDao()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun groupDao(): ProxyGroup.Dao
|
||||||
|
abstract fun proxyDao(): ProxyEntity.Dao
|
||||||
|
abstract fun rulesDao(): RuleEntity.Dao
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
package io.nekohasekai.sagernet.database;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.Serializable;
|
||||||
|
|
||||||
|
public class SubscriptionBean extends Serializable {
|
||||||
|
|
||||||
|
public Integer type;
|
||||||
|
public String link;
|
||||||
|
public String token;
|
||||||
|
public Boolean forceResolve;
|
||||||
|
public Boolean deduplication;
|
||||||
|
public Boolean updateWhenConnectedOnly;
|
||||||
|
public String customUserAgent;
|
||||||
|
public Boolean autoUpdate;
|
||||||
|
public Integer autoUpdateDelay;
|
||||||
|
public Integer lastUpdated;
|
||||||
|
|
||||||
|
// SIP008
|
||||||
|
|
||||||
|
public Long bytesUsed;
|
||||||
|
public Long bytesRemaining;
|
||||||
|
|
||||||
|
// Open Online Config
|
||||||
|
|
||||||
|
public String username;
|
||||||
|
public Integer expiryDate;
|
||||||
|
public List<String> protocols;
|
||||||
|
|
||||||
|
|
||||||
|
// https://github.com/crossutility/Quantumult/blob/master/extra-subscription-feature.md
|
||||||
|
|
||||||
|
public String subscriptionUserinfo;
|
||||||
|
|
||||||
|
public SubscriptionBean() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serializeToBuffer(ByteBufferOutput output) {
|
||||||
|
output.writeInt(1);
|
||||||
|
|
||||||
|
output.writeInt(type);
|
||||||
|
|
||||||
|
output.writeString(link);
|
||||||
|
|
||||||
|
output.writeBoolean(forceResolve);
|
||||||
|
output.writeBoolean(deduplication);
|
||||||
|
output.writeBoolean(updateWhenConnectedOnly);
|
||||||
|
output.writeString(customUserAgent);
|
||||||
|
output.writeBoolean(autoUpdate);
|
||||||
|
output.writeInt(autoUpdateDelay);
|
||||||
|
output.writeInt(lastUpdated);
|
||||||
|
|
||||||
|
output.writeString(subscriptionUserinfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void serializeForShare(ByteBufferOutput output) {
|
||||||
|
output.writeInt(0);
|
||||||
|
|
||||||
|
output.writeInt(type);
|
||||||
|
|
||||||
|
output.writeString(link);
|
||||||
|
|
||||||
|
output.writeBoolean(forceResolve);
|
||||||
|
output.writeBoolean(deduplication);
|
||||||
|
output.writeBoolean(updateWhenConnectedOnly);
|
||||||
|
output.writeString(customUserAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserializeFromBuffer(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
|
||||||
|
type = input.readInt();
|
||||||
|
link = input.readString();
|
||||||
|
forceResolve = input.readBoolean();
|
||||||
|
deduplication = input.readBoolean();
|
||||||
|
updateWhenConnectedOnly = input.readBoolean();
|
||||||
|
customUserAgent = input.readString();
|
||||||
|
autoUpdate = input.readBoolean();
|
||||||
|
autoUpdateDelay = input.readInt();
|
||||||
|
lastUpdated = input.readInt();
|
||||||
|
subscriptionUserinfo = input.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deserializeFromShare(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
|
||||||
|
type = input.readInt();
|
||||||
|
link = input.readString();
|
||||||
|
forceResolve = input.readBoolean();
|
||||||
|
deduplication = input.readBoolean();
|
||||||
|
updateWhenConnectedOnly = input.readBoolean();
|
||||||
|
customUserAgent = input.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
if (type == null) type = 0;
|
||||||
|
if (link == null) link = "";
|
||||||
|
if (token == null) token = "";
|
||||||
|
if (forceResolve == null) forceResolve = false;
|
||||||
|
if (deduplication == null) deduplication = false;
|
||||||
|
if (updateWhenConnectedOnly == null) updateWhenConnectedOnly = false;
|
||||||
|
if (customUserAgent == null) customUserAgent = "";
|
||||||
|
if (autoUpdate == null) autoUpdate = false;
|
||||||
|
if (autoUpdateDelay == null) autoUpdateDelay = 1440;
|
||||||
|
if (lastUpdated == null) lastUpdated = 0;
|
||||||
|
|
||||||
|
if (bytesUsed == null) bytesUsed = 0L;
|
||||||
|
if (bytesRemaining == null) bytesRemaining = 0L;
|
||||||
|
|
||||||
|
if (username == null) username = "";
|
||||||
|
if (expiryDate == null) expiryDate = 0;
|
||||||
|
if (protocols == null) protocols = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<SubscriptionBean> CREATOR = new CREATOR<SubscriptionBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public SubscriptionBean newInstance() {
|
||||||
|
return new SubscriptionBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SubscriptionBean[] newArray(int size) {
|
||||||
|
return new SubscriptionBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
package io.nekohasekai.sagernet.database.preference
|
||||||
|
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.InputFilter
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
|
|
||||||
|
object EditTextPreferenceModifiers {
|
||||||
|
object Monospace : EditTextPreference.OnBindEditTextListener {
|
||||||
|
override fun onBindEditText(editText: EditText) {
|
||||||
|
editText.typeface = Typeface.MONOSPACE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Hosts : EditTextPreference.OnBindEditTextListener {
|
||||||
|
|
||||||
|
override fun onBindEditText(editText: EditText) {
|
||||||
|
editText.setHorizontallyScrolling(true)
|
||||||
|
editText.setSelection(editText.text.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Port : EditTextPreference.OnBindEditTextListener {
|
||||||
|
private val portLengthFilter = arrayOf(InputFilter.LengthFilter(5))
|
||||||
|
|
||||||
|
override fun onBindEditText(editText: EditText) {
|
||||||
|
editText.inputType = EditorInfo.TYPE_CLASS_NUMBER
|
||||||
|
editText.filters = portLengthFilter
|
||||||
|
editText.setSingleLine()
|
||||||
|
editText.setSelection(editText.text.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Number : EditTextPreference.OnBindEditTextListener {
|
||||||
|
|
||||||
|
override fun onBindEditText(editText: EditText) {
|
||||||
|
editText.inputType = EditorInfo.TYPE_CLASS_NUMBER
|
||||||
|
editText.setSingleLine()
|
||||||
|
editText.setSelection(editText.text.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,170 @@
|
|||||||
|
package io.nekohasekai.sagernet.database.preference
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.*
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class KeyValuePair() : Parcelable {
|
||||||
|
companion object {
|
||||||
|
const val TYPE_UNINITIALIZED = 0
|
||||||
|
const val TYPE_BOOLEAN = 1
|
||||||
|
const val TYPE_FLOAT = 2
|
||||||
|
|
||||||
|
@Deprecated("Use TYPE_LONG.")
|
||||||
|
const val TYPE_INT = 3
|
||||||
|
const val TYPE_LONG = 4
|
||||||
|
const val TYPE_STRING = 5
|
||||||
|
const val TYPE_STRING_SET = 6
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATOR = object : Parcelable.Creator<KeyValuePair> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): KeyValuePair {
|
||||||
|
return KeyValuePair(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<KeyValuePair?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.room.Dao
|
||||||
|
interface Dao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM `KeyValuePair`")
|
||||||
|
fun all(): List<KeyValuePair>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM `KeyValuePair` WHERE `key` = :key")
|
||||||
|
operator fun get(key: String): KeyValuePair?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun put(value: KeyValuePair): Long
|
||||||
|
|
||||||
|
@Query("DELETE FROM `KeyValuePair` WHERE `key` = :key")
|
||||||
|
fun delete(key: String): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM `KeyValuePair`")
|
||||||
|
fun reset(): Int
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insert(list: List<KeyValuePair>)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryKey
|
||||||
|
var key: String = ""
|
||||||
|
var valueType: Int = TYPE_UNINITIALIZED
|
||||||
|
var value: ByteArray = ByteArray(0)
|
||||||
|
|
||||||
|
val boolean: Boolean?
|
||||||
|
get() = if (valueType == TYPE_BOOLEAN) ByteBuffer.wrap(value).get() != 0.toByte() else null
|
||||||
|
val float: Float?
|
||||||
|
get() = if (valueType == TYPE_FLOAT) ByteBuffer.wrap(value).float else null
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("Use long.", ReplaceWith("long"))
|
||||||
|
val int: Int?
|
||||||
|
get() = if (valueType == TYPE_INT) ByteBuffer.wrap(value).int else null
|
||||||
|
val long: Long?
|
||||||
|
get() = when (valueType) {
|
||||||
|
@Suppress("DEPRECATION") TYPE_INT,
|
||||||
|
-> ByteBuffer.wrap(value).int.toLong()
|
||||||
|
TYPE_LONG -> ByteBuffer.wrap(value).long
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val string: String?
|
||||||
|
get() = if (valueType == TYPE_STRING) String(value) else null
|
||||||
|
val stringSet: Set<String>?
|
||||||
|
get() = if (valueType == TYPE_STRING_SET) {
|
||||||
|
val buffer = ByteBuffer.wrap(value)
|
||||||
|
val result = HashSet<String>()
|
||||||
|
while (buffer.hasRemaining()) {
|
||||||
|
val chArr = ByteArray(buffer.int)
|
||||||
|
buffer.get(chArr)
|
||||||
|
result.add(String(chArr))
|
||||||
|
}
|
||||||
|
result
|
||||||
|
} else null
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
constructor(key: String) : this() {
|
||||||
|
this.key = key
|
||||||
|
}
|
||||||
|
|
||||||
|
// putting null requires using DataStore
|
||||||
|
fun put(value: Boolean): KeyValuePair {
|
||||||
|
valueType = TYPE_BOOLEAN
|
||||||
|
this.value = ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte()).array()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(value: Float): KeyValuePair {
|
||||||
|
valueType = TYPE_FLOAT
|
||||||
|
this.value = ByteBuffer.allocate(4).putFloat(value).array()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("Use long.")
|
||||||
|
fun put(value: Int): KeyValuePair {
|
||||||
|
valueType = TYPE_INT
|
||||||
|
this.value = ByteBuffer.allocate(4).putInt(value).array()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(value: Long): KeyValuePair {
|
||||||
|
valueType = TYPE_LONG
|
||||||
|
this.value = ByteBuffer.allocate(8).putLong(value).array()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(value: String): KeyValuePair {
|
||||||
|
valueType = TYPE_STRING
|
||||||
|
this.value = value.toByteArray()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(value: Set<String>): KeyValuePair {
|
||||||
|
valueType = TYPE_STRING_SET
|
||||||
|
val stream = ByteArrayOutputStream()
|
||||||
|
val intBuffer = ByteBuffer.allocate(4)
|
||||||
|
for (v in value) {
|
||||||
|
intBuffer.rewind()
|
||||||
|
stream.write(intBuffer.putInt(v.length).array())
|
||||||
|
stream.write(v.toByteArray())
|
||||||
|
}
|
||||||
|
this.value = stream.toByteArray()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("IMPLICIT_CAST_TO_ANY")
|
||||||
|
override fun toString(): String {
|
||||||
|
return when (valueType) {
|
||||||
|
TYPE_BOOLEAN -> boolean
|
||||||
|
TYPE_FLOAT -> float
|
||||||
|
TYPE_LONG -> long
|
||||||
|
TYPE_STRING -> string
|
||||||
|
TYPE_STRING_SET -> stringSet
|
||||||
|
else -> null
|
||||||
|
}?.toString() ?: "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this() {
|
||||||
|
key = parcel.readString()!!
|
||||||
|
valueType = parcel.readInt()
|
||||||
|
value = parcel.createByteArray()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(key)
|
||||||
|
parcel.writeInt(valueType)
|
||||||
|
parcel.writeByteArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package io.nekohasekai.sagernet.database.preference
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceDataStore
|
||||||
|
|
||||||
|
interface OnPreferenceDataStoreChangeListener {
|
||||||
|
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package io.nekohasekai.sagernet.database.preference
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import dev.matrix.roomigrant.GenerateRoomMigrations
|
||||||
|
import io.nekohasekai.sagernet.Key
|
||||||
|
import io.nekohasekai.sagernet.SagerNet
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Database(entities = [KeyValuePair::class], version = 1)
|
||||||
|
@GenerateRoomMigrations
|
||||||
|
abstract class PublicDatabase : RoomDatabase() {
|
||||||
|
companion object {
|
||||||
|
private val instance by lazy {
|
||||||
|
SagerNet.application.getDatabasePath(Key.DB_PROFILE).parentFile?.mkdirs()
|
||||||
|
Room.databaseBuilder(SagerNet.application, PublicDatabase::class.java, Key.DB_PUBLIC)
|
||||||
|
.allowMainThreadQueries()
|
||||||
|
.enableMultiInstanceInvalidation()
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.setQueryExecutor { GlobalScope.launch { it.run() } }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val kvPairDao get() = instance.keyValuePairDao()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun keyValuePairDao(): KeyValuePair.Dao
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
package io.nekohasekai.sagernet.database.preference
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceDataStore
|
||||||
|
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||||
|
open class RoomPreferenceDataStore(private val kvPairDao: KeyValuePair.Dao) :
|
||||||
|
PreferenceDataStore() {
|
||||||
|
|
||||||
|
fun getBoolean(key: String) = kvPairDao[key]?.boolean
|
||||||
|
fun getFloat(key: String) = kvPairDao[key]?.float
|
||||||
|
fun getInt(key: String) = kvPairDao[key]?.long?.toInt()
|
||||||
|
fun getLong(key: String) = kvPairDao[key]?.long
|
||||||
|
fun getString(key: String) = kvPairDao[key]?.string
|
||||||
|
fun getStringSet(key: String) = kvPairDao[key]?.stringSet
|
||||||
|
fun reset() = kvPairDao.reset()
|
||||||
|
|
||||||
|
override fun getBoolean(key: String, defValue: Boolean) = getBoolean(key) ?: defValue
|
||||||
|
override fun getFloat(key: String, defValue: Float) = getFloat(key) ?: defValue
|
||||||
|
override fun getInt(key: String, defValue: Int) = getInt(key) ?: defValue
|
||||||
|
override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue
|
||||||
|
override fun getString(key: String, defValue: String?) = getString(key) ?: defValue
|
||||||
|
override fun getStringSet(key: String, defValue: MutableSet<String>?) =
|
||||||
|
getStringSet(key) ?: defValue
|
||||||
|
|
||||||
|
fun putBoolean(key: String, value: Boolean?) =
|
||||||
|
if (value == null) remove(key) else putBoolean(key, value)
|
||||||
|
|
||||||
|
fun putFloat(key: String, value: Float?) =
|
||||||
|
if (value == null) remove(key) else putFloat(key, value)
|
||||||
|
|
||||||
|
fun putInt(key: String, value: Int?) =
|
||||||
|
if (value == null) remove(key) else putLong(key, value.toLong())
|
||||||
|
|
||||||
|
fun putLong(key: String, value: Long?) = if (value == null) remove(key) else putLong(key, value)
|
||||||
|
override fun putBoolean(key: String, value: Boolean) {
|
||||||
|
kvPairDao.put(KeyValuePair(key).put(value))
|
||||||
|
fireChangeListener(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putFloat(key: String, value: Float) {
|
||||||
|
kvPairDao.put(KeyValuePair(key).put(value))
|
||||||
|
fireChangeListener(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putInt(key: String, value: Int) {
|
||||||
|
kvPairDao.put(KeyValuePair(key).put(value.toLong()))
|
||||||
|
fireChangeListener(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putLong(key: String, value: Long) {
|
||||||
|
kvPairDao.put(KeyValuePair(key).put(value))
|
||||||
|
fireChangeListener(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putString(key: String, value: String?) = if (value == null) remove(key) else {
|
||||||
|
kvPairDao.put(KeyValuePair(key).put(value))
|
||||||
|
fireChangeListener(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putStringSet(key: String, values: MutableSet<String>?) =
|
||||||
|
if (values == null) remove(key) else {
|
||||||
|
kvPairDao.put(KeyValuePair(key).put(values))
|
||||||
|
fireChangeListener(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(key: String) {
|
||||||
|
kvPairDao.delete(key)
|
||||||
|
fireChangeListener(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
|
||||||
|
private fun fireChangeListener(key: String) {
|
||||||
|
val listeners = synchronized(listeners) {
|
||||||
|
listeners.toList()
|
||||||
|
}
|
||||||
|
listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) {
|
||||||
|
synchronized(listeners) {
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) {
|
||||||
|
synchronized(listeners) {
|
||||||
|
listeners.remove(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
151
app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java
Normal file
151
app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.ktx.NetsKt;
|
||||||
|
import moe.matsuri.nb4a.utils.JavaUtil;
|
||||||
|
|
||||||
|
public abstract class AbstractBean extends Serializable {
|
||||||
|
|
||||||
|
public String serverAddress;
|
||||||
|
public Integer serverPort;
|
||||||
|
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
public String customOutboundJson;
|
||||||
|
public String customConfigJson;
|
||||||
|
|
||||||
|
//
|
||||||
|
public transient String finalAddress;
|
||||||
|
public transient int finalPort;
|
||||||
|
|
||||||
|
public String displayName() {
|
||||||
|
if (JavaUtil.isNotBlank(name)) {
|
||||||
|
return name;
|
||||||
|
} else {
|
||||||
|
return displayAddress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String displayAddress() {
|
||||||
|
return NetsKt.wrapIPV6Host(serverAddress) + ":" + serverPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String network() {
|
||||||
|
return "tcp,udp";
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canICMPing() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canTCPing() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canMapping() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
if (JavaUtil.isNullOrBlank(serverAddress)) {
|
||||||
|
serverAddress = "127.0.0.1";
|
||||||
|
} else if (serverAddress.startsWith("[") && serverAddress.endsWith("]")) {
|
||||||
|
serverAddress = NetsKt.unwrapIPV6Host(serverAddress);
|
||||||
|
}
|
||||||
|
if (serverPort == null) {
|
||||||
|
serverPort = 1080;
|
||||||
|
}
|
||||||
|
if (name == null) name = "";
|
||||||
|
|
||||||
|
finalAddress = serverAddress;
|
||||||
|
finalPort = serverPort;
|
||||||
|
|
||||||
|
if (customOutboundJson == null) customOutboundJson = "";
|
||||||
|
if (customConfigJson == null) customConfigJson = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private transient boolean serializeWithoutName;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serializeToBuffer(@NonNull ByteBufferOutput output) {
|
||||||
|
serialize(output);
|
||||||
|
|
||||||
|
output.writeInt(1);
|
||||||
|
if (!serializeWithoutName) {
|
||||||
|
output.writeString(name);
|
||||||
|
}
|
||||||
|
output.writeString(customOutboundJson);
|
||||||
|
output.writeString(customConfigJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserializeFromBuffer(@NonNull ByteBufferInput input) {
|
||||||
|
deserialize(input);
|
||||||
|
|
||||||
|
int extraVersion = input.readInt();
|
||||||
|
|
||||||
|
name = input.readString();
|
||||||
|
customOutboundJson = input.readString();
|
||||||
|
customConfigJson = input.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeString(serverAddress);
|
||||||
|
output.writeInt(serverPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
serverAddress = input.readString();
|
||||||
|
serverPort = input.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public abstract AbstractBean clone();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
try {
|
||||||
|
serializeWithoutName = true;
|
||||||
|
((AbstractBean) o).serializeWithoutName = true;
|
||||||
|
return Arrays.equals(KryoConverters.serialize(this), KryoConverters.serialize((AbstractBean) o));
|
||||||
|
} finally {
|
||||||
|
serializeWithoutName = false;
|
||||||
|
((AbstractBean) o).serializeWithoutName = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
try {
|
||||||
|
serializeWithoutName = true;
|
||||||
|
return Arrays.hashCode(KryoConverters.serialize(this));
|
||||||
|
} finally {
|
||||||
|
serializeWithoutName = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getClass().getSimpleName() + " " + JavaUtil.gson.toJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyFeatureSettings(AbstractBean other) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
710
app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
Normal file
710
app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
Normal file
@ -0,0 +1,710 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.IPv6Mode
|
||||||
|
import io.nekohasekai.sagernet.Key
|
||||||
|
import io.nekohasekai.sagernet.bg.VpnService
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.database.ProxyEntity
|
||||||
|
import io.nekohasekai.sagernet.database.ProxyEntity.Companion.TYPE_CONFIG
|
||||||
|
import io.nekohasekai.sagernet.database.SagerDatabase
|
||||||
|
import io.nekohasekai.sagernet.fmt.ConfigBuildResult.IndexEntity
|
||||||
|
import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.hysteria.buildSingBoxOutboundHysteriaBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.hysteria.isMultiPort
|
||||||
|
import io.nekohasekai.sagernet.fmt.internal.ChainBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.shadowsocks.buildSingBoxOutboundShadowsocksBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.socks.buildSingBoxOutboundSocksBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.ssh.SSHBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.ssh.buildSingBoxOutboundSSHBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.tuic.TuicBean
|
||||||
|
import moe.matsuri.nb4a.SingBoxOptions.*
|
||||||
|
import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.v2ray.buildSingBoxOutboundStandardV2RayBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.wireguard.buildSingBoxOutboundWireguardBean
|
||||||
|
import io.nekohasekai.sagernet.ktx.isIpAddress
|
||||||
|
import io.nekohasekai.sagernet.ktx.mkPort
|
||||||
|
import io.nekohasekai.sagernet.utils.PackageCache
|
||||||
|
import moe.matsuri.nb4a.DNS.applyDNSNetworkSettings
|
||||||
|
import moe.matsuri.nb4a.DNS.makeSingBoxRule
|
||||||
|
import moe.matsuri.nb4a.proxy.config.ConfigBean
|
||||||
|
import moe.matsuri.nb4a.plugin.Plugins
|
||||||
|
import moe.matsuri.nb4a.utils.JavaUtil.gson
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
|
||||||
|
const val TAG_MIXED = "mixed-in"
|
||||||
|
const val TAG_TRANS = "trans-in"
|
||||||
|
|
||||||
|
const val TAG_PROXY = "proxy"
|
||||||
|
const val TAG_DIRECT = "direct"
|
||||||
|
const val TAG_BYPASS = "bypass"
|
||||||
|
const val TAG_BLOCK = "block"
|
||||||
|
|
||||||
|
const val TAG_DNS_IN = "dns-in"
|
||||||
|
const val TAG_DNS_OUT = "dns-out"
|
||||||
|
|
||||||
|
const val LOCALHOST = "127.0.0.1"
|
||||||
|
const val LOCAL_DNS_SERVER = "underlying://0.0.0.0"
|
||||||
|
|
||||||
|
class ConfigBuildResult(
|
||||||
|
var config: String,
|
||||||
|
var externalIndex: List<IndexEntity>,
|
||||||
|
var outboundTags: List<String>,
|
||||||
|
var outboundTagMain: String,
|
||||||
|
var trafficMap: Map<String, ProxyEntity>,
|
||||||
|
val alerts: List<Pair<Int, String>>,
|
||||||
|
) {
|
||||||
|
data class IndexEntity(var chain: LinkedHashMap<Int, ProxyEntity>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mergeJSON(j: String, to: MutableMap<String, Any>) {
|
||||||
|
if (j.isNullOrBlank()) return
|
||||||
|
val m = gson.fromJson(j, to.javaClass)
|
||||||
|
m.forEach { (k, v) ->
|
||||||
|
if (v is Map<*, *> && to[k] is Map<*, *>) {
|
||||||
|
val currentMap = (to[k] as Map<*, *>).toMutableMap()
|
||||||
|
currentMap += v
|
||||||
|
to[k] = currentMap
|
||||||
|
} else {
|
||||||
|
to[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildConfig(
|
||||||
|
proxy: ProxyEntity, forTest: Boolean = false
|
||||||
|
): ConfigBuildResult {
|
||||||
|
|
||||||
|
if (proxy.type == TYPE_CONFIG) {
|
||||||
|
val bean = proxy.requireBean() as ConfigBean
|
||||||
|
if (bean.type == 0) {
|
||||||
|
return ConfigBuildResult(
|
||||||
|
bean.config,
|
||||||
|
listOf(),
|
||||||
|
listOf(TAG_PROXY), //
|
||||||
|
TAG_PROXY, //
|
||||||
|
mapOf(
|
||||||
|
TAG_PROXY to proxy
|
||||||
|
),
|
||||||
|
listOf()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val outboundTags = ArrayList<String>()
|
||||||
|
var outboundTagMain = TAG_BYPASS
|
||||||
|
val trafficMap = HashMap<String, ProxyEntity>()
|
||||||
|
val globalOutbounds = ArrayList<Long>()
|
||||||
|
|
||||||
|
fun ProxyEntity.resolveChain(): MutableList<ProxyEntity> {
|
||||||
|
val bean = requireBean()
|
||||||
|
if (bean is ChainBean) {
|
||||||
|
val beans = SagerDatabase.proxyDao.getEntities(bean.proxies)
|
||||||
|
val beansMap = beans.associateBy { it.id }
|
||||||
|
val beanList = ArrayList<ProxyEntity>()
|
||||||
|
for (proxyId in bean.proxies) {
|
||||||
|
val item = beansMap[proxyId] ?: continue
|
||||||
|
beanList.addAll(item.resolveChain())
|
||||||
|
}
|
||||||
|
return beanList.asReversed()
|
||||||
|
}
|
||||||
|
return mutableListOf(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
val proxies = proxy.resolveChain()
|
||||||
|
val extraRules = if (forTest) listOf() else SagerDatabase.rulesDao.enabledRules()
|
||||||
|
val extraProxies =
|
||||||
|
if (forTest) mapOf() else SagerDatabase.proxyDao.getEntities(extraRules.mapNotNull { rule ->
|
||||||
|
rule.outbound.takeIf { it > 0 && it != proxy.id }
|
||||||
|
}.toHashSet().toList()).associate { it.id to it.resolveChain() }
|
||||||
|
|
||||||
|
val uidListDNSRemote = mutableListOf<Int>()
|
||||||
|
val uidListDNSDirect = mutableListOf<Int>()
|
||||||
|
val domainListDNSRemote = mutableListOf<String>()
|
||||||
|
val domainListDNSDirect = mutableListOf<String>()
|
||||||
|
val domainListDNSBlock = mutableListOf<String>()
|
||||||
|
val bypassDNSBeans = hashSetOf<AbstractBean>()
|
||||||
|
val isVPN = DataStore.serviceMode == Key.MODE_VPN
|
||||||
|
val bind = if (!forTest && DataStore.allowAccess) "0.0.0.0" else LOCALHOST
|
||||||
|
val remoteDns = DataStore.remoteDns.split("\n")
|
||||||
|
.mapNotNull { dns -> dns.trim().takeIf { it.isNotBlank() && !it.startsWith("#") } }
|
||||||
|
var directDNS = DataStore.directDns.split("\n")
|
||||||
|
.mapNotNull { dns -> dns.trim().takeIf { it.isNotBlank() && !it.startsWith("#") } }
|
||||||
|
val enableDnsRouting = DataStore.enableDnsRouting
|
||||||
|
val useFakeDns = DataStore.enableFakeDns && !forTest && DataStore.ipv6Mode != IPv6Mode.ONLY
|
||||||
|
val needSniff = DataStore.trafficSniffing
|
||||||
|
val externalIndexMap = ArrayList<IndexEntity>()
|
||||||
|
val requireTransproxy = if (forTest) false else DataStore.requireTransproxy
|
||||||
|
val ipv6Mode = if (forTest) IPv6Mode.ENABLE else DataStore.ipv6Mode
|
||||||
|
val resolveDestination = DataStore.resolveDestination
|
||||||
|
val alerts = mutableListOf<Pair<Int, String>>()
|
||||||
|
|
||||||
|
var optionsToMerge: String = ""
|
||||||
|
|
||||||
|
return MyOptions().apply {
|
||||||
|
if (!forTest && DataStore.enableClashAPI) experimental = ExperimentalOptions().apply {
|
||||||
|
clash_api = ClashAPIOptions().apply {
|
||||||
|
external_controller = "127.0.0.1:9090"
|
||||||
|
external_ui = "../files/yacd"
|
||||||
|
cache_file = "../cache/clash.db"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns = DNSOptions().apply {
|
||||||
|
// TODO nb4a hosts?
|
||||||
|
// hosts = DataStore.hosts.split("\n")
|
||||||
|
// .filter { it.isNotBlank() }
|
||||||
|
// .associate { it.substringBefore(" ") to it.substringAfter(" ") }
|
||||||
|
// .toMutableMap()
|
||||||
|
|
||||||
|
servers = mutableListOf()
|
||||||
|
rules = mutableListOf()
|
||||||
|
|
||||||
|
when (ipv6Mode) {
|
||||||
|
IPv6Mode.DISABLE -> {
|
||||||
|
strategy = "ipv4_only"
|
||||||
|
}
|
||||||
|
IPv6Mode.ONLY -> {
|
||||||
|
strategy = "ipv6_only"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inbounds = mutableListOf()
|
||||||
|
|
||||||
|
if (!forTest) {
|
||||||
|
if (isVPN) inbounds.add(Inbound_TunOptions().apply {
|
||||||
|
type = "tun"
|
||||||
|
tag = "tun-in"
|
||||||
|
stack = if (DataStore.tunImplementation == 1) "system" else "gvisor"
|
||||||
|
sniff = needSniff
|
||||||
|
endpoint_independent_nat = true
|
||||||
|
when (ipv6Mode) {
|
||||||
|
IPv6Mode.DISABLE -> {
|
||||||
|
inet4_address = listOf(VpnService.PRIVATE_VLAN4_CLIENT + "/28")
|
||||||
|
}
|
||||||
|
IPv6Mode.ONLY -> {
|
||||||
|
inet6_address = listOf(VpnService.PRIVATE_VLAN6_CLIENT + "/126")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
inet4_address = listOf(VpnService.PRIVATE_VLAN4_CLIENT + "/28")
|
||||||
|
inet6_address = listOf(VpnService.PRIVATE_VLAN6_CLIENT + "/126")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
inbounds.add(Inbound_MixedOptions().apply {
|
||||||
|
type = "mixed"
|
||||||
|
tag = TAG_MIXED
|
||||||
|
listen = bind
|
||||||
|
listen_port = DataStore.mixedPort
|
||||||
|
if (needSniff) {
|
||||||
|
sniff = true
|
||||||
|
// destOverride = when {
|
||||||
|
// useFakeDns && !trafficSniffing -> listOf("fakedns")
|
||||||
|
// useFakeDns -> listOf("fakedns", "http", "tls", "quic")
|
||||||
|
// else -> listOf("http", "tls", "quic")
|
||||||
|
// }
|
||||||
|
// metadataOnly = useFakeDns && !trafficSniffing
|
||||||
|
// routeOnly = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireTransproxy) {
|
||||||
|
if (DataStore.transproxyMode == 1) {
|
||||||
|
inbounds.add(Inbound_TProxyOptions().apply {
|
||||||
|
type = "tproxy"
|
||||||
|
tag = TAG_TRANS
|
||||||
|
listen = bind
|
||||||
|
listen_port = DataStore.transproxyPort
|
||||||
|
sniff = needSniff
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
inbounds.add(Inbound_RedirectOptions().apply {
|
||||||
|
type = "redirect"
|
||||||
|
tag = TAG_TRANS
|
||||||
|
listen = bind
|
||||||
|
listen_port = DataStore.transproxyPort
|
||||||
|
sniff = needSniff
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outbounds = mutableListOf()
|
||||||
|
|
||||||
|
// init routing object
|
||||||
|
route = RouteOptions().apply {
|
||||||
|
auto_detect_interface = true
|
||||||
|
rules = mutableListOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns outbound tag
|
||||||
|
fun buildChain(
|
||||||
|
chainId: Long, profileList: List<ProxyEntity>
|
||||||
|
): String {
|
||||||
|
var currentOutbound = mutableMapOf<String, Any>()
|
||||||
|
lateinit var pastOutbound: MutableMap<String, Any>
|
||||||
|
lateinit var pastInboundTag: String
|
||||||
|
var pastEntity: ProxyEntity? = null
|
||||||
|
val externalChainMap = LinkedHashMap<Int, ProxyEntity>()
|
||||||
|
externalIndexMap.add(IndexEntity(externalChainMap))
|
||||||
|
val chainOutbounds = ArrayList<MutableMap<String, Any>>()
|
||||||
|
|
||||||
|
// chainTagOut: v2ray outbound tag for this chain
|
||||||
|
var chainTagOut = ""
|
||||||
|
var chainTag = "c-$chainId"
|
||||||
|
var muxApplied = false
|
||||||
|
|
||||||
|
fun genDomainStrategy(noAsIs: Boolean): String {
|
||||||
|
return when {
|
||||||
|
!resolveDestination && !noAsIs -> ""
|
||||||
|
ipv6Mode == IPv6Mode.DISABLE -> "ipv4_only"
|
||||||
|
ipv6Mode == IPv6Mode.PREFER -> "prefer_ipv6"
|
||||||
|
ipv6Mode == IPv6Mode.ONLY -> "ipv6_only"
|
||||||
|
else -> "prefer_ipv4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentDomainStrategy = genDomainStrategy(false)
|
||||||
|
|
||||||
|
profileList.forEachIndexed { index, proxyEntity ->
|
||||||
|
val bean = proxyEntity.requireBean()
|
||||||
|
|
||||||
|
// tagOut: v2ray outbound tag for a profile
|
||||||
|
// profile2 (in) (global) tag g-(id)
|
||||||
|
// profile1 tag (chainTag)-(id)
|
||||||
|
// profile0 (out) tag (chainTag)-(id) / single: "proxy"
|
||||||
|
var tagOut = "$chainTag-${proxyEntity.id}"
|
||||||
|
|
||||||
|
// needGlobal: can only contain one?
|
||||||
|
var needGlobal = false
|
||||||
|
|
||||||
|
// first profile set as global
|
||||||
|
if (index == profileList.lastIndex) {
|
||||||
|
needGlobal = true
|
||||||
|
tagOut = "g-" + proxyEntity.id
|
||||||
|
bypassDNSBeans += proxyEntity.requireBean()
|
||||||
|
}
|
||||||
|
|
||||||
|
// last profile set as "proxy"
|
||||||
|
if (chainId == 0L && index == 0) {
|
||||||
|
tagOut = TAG_PROXY
|
||||||
|
}
|
||||||
|
|
||||||
|
// chain rules
|
||||||
|
if (index > 0) {
|
||||||
|
// chain route/proxy rules
|
||||||
|
if (pastEntity!!.needExternal()) {
|
||||||
|
route.rules.add(Rule_DefaultOptions().apply {
|
||||||
|
inbound = listOf(pastInboundTag)
|
||||||
|
outbound = tagOut
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
pastOutbound["detour"] = tagOut
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// index == 0 means last profile in chain / not chain
|
||||||
|
chainTagOut = tagOut
|
||||||
|
outboundTags.add(tagOut)
|
||||||
|
if (chainId == 0L) outboundTagMain = tagOut
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needGlobal) {
|
||||||
|
if (globalOutbounds.contains(proxyEntity.id)) {
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
globalOutbounds.add(proxyEntity.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// include g-xx
|
||||||
|
trafficMap[tagOut] = proxyEntity
|
||||||
|
|
||||||
|
// Chain outbound
|
||||||
|
if (proxyEntity.needExternal()) {
|
||||||
|
val localPort = mkPort()
|
||||||
|
externalChainMap[localPort] = proxyEntity
|
||||||
|
currentOutbound = Outbound_SocksOptions().apply {
|
||||||
|
type = "socks"
|
||||||
|
server = LOCALHOST
|
||||||
|
server_port = localPort
|
||||||
|
}.asMap()
|
||||||
|
} else {
|
||||||
|
// internal outbound
|
||||||
|
|
||||||
|
currentOutbound = when (bean) {
|
||||||
|
is ConfigBean ->
|
||||||
|
gson.fromJson(bean.config, currentOutbound.javaClass)
|
||||||
|
is StandardV2RayBean ->
|
||||||
|
buildSingBoxOutboundStandardV2RayBean(bean).asMap()
|
||||||
|
is HysteriaBean ->
|
||||||
|
buildSingBoxOutboundHysteriaBean(bean).asMap()
|
||||||
|
is SOCKSBean ->
|
||||||
|
buildSingBoxOutboundSocksBean(bean).asMap()
|
||||||
|
is ShadowsocksBean ->
|
||||||
|
buildSingBoxOutboundShadowsocksBean(bean).asMap()
|
||||||
|
is WireGuardBean ->
|
||||||
|
buildSingBoxOutboundWireguardBean(bean).asMap()
|
||||||
|
is SSHBean ->
|
||||||
|
buildSingBoxOutboundSSHBean(bean).asMap()
|
||||||
|
else -> throw IllegalStateException("can't reach")
|
||||||
|
}
|
||||||
|
|
||||||
|
currentOutbound.apply {
|
||||||
|
// TODO nb4a keepAliveInterval?
|
||||||
|
// val keepAliveInterval = DataStore.tcpKeepAliveInterval
|
||||||
|
// val needKeepAliveInterval = keepAliveInterval !in intArrayOf(0, 15)
|
||||||
|
|
||||||
|
if (!muxApplied && proxyEntity.needCoreMux()) {
|
||||||
|
muxApplied = true
|
||||||
|
currentOutbound["multiplex"] = MultiplexOptions().apply {
|
||||||
|
enabled = true
|
||||||
|
max_streams = DataStore.muxConcurrency
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// custom JSON merge
|
||||||
|
if (bean.customOutboundJson.isNotBlank()) {
|
||||||
|
mergeJSON(bean.customOutboundJson, currentOutbound)
|
||||||
|
}
|
||||||
|
if (index == 0 && bean.customConfigJson.isNotBlank()) {
|
||||||
|
optionsToMerge = bean.customConfigJson
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pastEntity?.requireBean()?.apply {
|
||||||
|
// don't loopback
|
||||||
|
if (currentDomainStrategy != "" && !serverAddress.isIpAddress()) {
|
||||||
|
domainListDNSDirect.add("full:$serverAddress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (forTest) {
|
||||||
|
currentDomainStrategy = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
currentOutbound["tag"] = tagOut
|
||||||
|
currentOutbound["domain_strategy"] = currentDomainStrategy
|
||||||
|
|
||||||
|
// External proxy need a dokodemo-door inbound to forward the traffic
|
||||||
|
// For external proxy software, their traffic must goes to v2ray-core to use protected fd.
|
||||||
|
if (bean.canMapping() && proxyEntity.needExternal()) {
|
||||||
|
// With ss protect, don't use mapping
|
||||||
|
var needExternal = true
|
||||||
|
if (index == profileList.lastIndex) {
|
||||||
|
val pluginId = when (bean) {
|
||||||
|
is HysteriaBean -> "hysteria-plugin"
|
||||||
|
is TuicBean -> "tuic-plugin"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
if (Plugins.isUsingMatsuriExe(pluginId)) {
|
||||||
|
needExternal = false
|
||||||
|
} else if (bean is HysteriaBean) {
|
||||||
|
throw Exception("not supported hysteria-plugin (SagerNet)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (needExternal) {
|
||||||
|
val mappingPort = mkPort()
|
||||||
|
bean.finalAddress = LOCALHOST
|
||||||
|
bean.finalPort = mappingPort
|
||||||
|
|
||||||
|
inbounds.add(Inbound_DirectOptions().apply {
|
||||||
|
type = "direct"
|
||||||
|
listen = LOCALHOST
|
||||||
|
listen_port = mappingPort
|
||||||
|
tag = "$chainTag-mapping-${proxyEntity.id}"
|
||||||
|
|
||||||
|
override_address = bean.serverAddress
|
||||||
|
override_port = bean.serverPort
|
||||||
|
|
||||||
|
pastInboundTag = tag
|
||||||
|
|
||||||
|
// no chain rule and not outbound, so need to set to direct
|
||||||
|
if (index == profileList.lastIndex) {
|
||||||
|
route.rules.add(Rule_DefaultOptions().apply {
|
||||||
|
inbound = listOf(tag)
|
||||||
|
outbound = TAG_DIRECT
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outbounds.add(currentOutbound)
|
||||||
|
chainOutbounds.add(currentOutbound)
|
||||||
|
pastOutbound = currentOutbound
|
||||||
|
pastEntity = proxyEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
return chainTagOut
|
||||||
|
}
|
||||||
|
|
||||||
|
val tagProxy = buildChain(0, proxies)
|
||||||
|
val tagMap = mutableMapOf<Long, String>()
|
||||||
|
extraProxies.forEach { (key, entities) ->
|
||||||
|
tagMap[key] = buildChain(key, entities)
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply user rules
|
||||||
|
for (rule in extraRules) {
|
||||||
|
val _uidList = rule.packages.map {
|
||||||
|
PackageCache[it]?.takeIf { uid -> uid >= 1000 }
|
||||||
|
}.toHashSet().filterNotNull()
|
||||||
|
|
||||||
|
if (rule.packages.isNotEmpty()) {
|
||||||
|
if (!isVPN) {
|
||||||
|
alerts.add(0 to rule.displayName())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
route.rules.add(Rule_DefaultOptions().apply {
|
||||||
|
if (rule.packages.isNotEmpty()) {
|
||||||
|
PackageCache.awaitLoadSync()
|
||||||
|
user_id = _uidList
|
||||||
|
}
|
||||||
|
|
||||||
|
var _domainList: List<String>? = null
|
||||||
|
if (rule.domains.isNotBlank()) {
|
||||||
|
_domainList = rule.domains.split("\n")
|
||||||
|
makeSingBoxRule(_domainList, false)
|
||||||
|
}
|
||||||
|
if (rule.ip.isNotBlank()) {
|
||||||
|
makeSingBoxRule(rule.ip.split("\n"), true)
|
||||||
|
}
|
||||||
|
if (rule.port.isNotBlank()) {
|
||||||
|
port = rule.port.split("\n").map { it.toIntOrNull() ?: 0 }
|
||||||
|
}
|
||||||
|
if (rule.sourcePort.isNotBlank()) {
|
||||||
|
source_port = rule.sourcePort.split("\n").map { it.toIntOrNull() ?: 0 }
|
||||||
|
}
|
||||||
|
if (rule.network.isNotBlank()) {
|
||||||
|
network = rule.network
|
||||||
|
}
|
||||||
|
if (rule.source.isNotBlank()) {
|
||||||
|
source_ip_cidr = rule.source.split("\n")
|
||||||
|
}
|
||||||
|
if (rule.protocol.isNotBlank()) {
|
||||||
|
protocol = rule.protocol.split("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// also bypass lookup
|
||||||
|
// cannot use other outbound profile to lookup...
|
||||||
|
if (rule.outbound == -1L) {
|
||||||
|
uidListDNSDirect += _uidList
|
||||||
|
if (_domainList != null) domainListDNSDirect += _domainList
|
||||||
|
} else if (rule.outbound == 0L) {
|
||||||
|
uidListDNSRemote += _uidList
|
||||||
|
if (_domainList != null) domainListDNSRemote += _domainList
|
||||||
|
} else if (rule.outbound == -2L) {
|
||||||
|
if (_domainList != null) domainListDNSBlock += _domainList
|
||||||
|
}
|
||||||
|
|
||||||
|
outbound = when (val outId = rule.outbound) {
|
||||||
|
0L -> tagProxy
|
||||||
|
-1L -> TAG_BYPASS
|
||||||
|
-2L -> TAG_BLOCK
|
||||||
|
else -> if (outId == proxy.id) tagProxy else tagMap[outId]
|
||||||
|
?: throw Exception("invalid rule")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (freedom in arrayOf(TAG_DIRECT, TAG_BYPASS)) outbounds.add(Outbound().apply {
|
||||||
|
tag = freedom
|
||||||
|
type = "direct"
|
||||||
|
}.asMap())
|
||||||
|
|
||||||
|
outbounds.add(Outbound().apply {
|
||||||
|
tag = TAG_BLOCK
|
||||||
|
type = "block"
|
||||||
|
}.asMap())
|
||||||
|
|
||||||
|
if (!forTest) {
|
||||||
|
inbounds.add(0, Inbound_DirectOptions().apply {
|
||||||
|
type = "direct"
|
||||||
|
tag = TAG_DNS_IN
|
||||||
|
listen = bind
|
||||||
|
listen_port = DataStore.localDNSPort
|
||||||
|
override_address = if (!remoteDns.first().isIpAddress()) {
|
||||||
|
"8.8.8.8"
|
||||||
|
} else {
|
||||||
|
remoteDns.first()
|
||||||
|
}
|
||||||
|
override_port = 53
|
||||||
|
})
|
||||||
|
|
||||||
|
outbounds.add(Outbound().apply {
|
||||||
|
type = "dns"
|
||||||
|
tag = TAG_DNS_OUT
|
||||||
|
}.asMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DataStore.directDnsUseSystem) {
|
||||||
|
// finally able to use "localDns" now...
|
||||||
|
directDNS = listOf(LOCAL_DNS_SERVER)
|
||||||
|
}
|
||||||
|
|
||||||
|
// routing for DNS server
|
||||||
|
for (dns in remoteDns) {
|
||||||
|
if (!dns.isIpAddress()) continue
|
||||||
|
route.rules.add(Rule_DefaultOptions().apply {
|
||||||
|
outbound = tagProxy
|
||||||
|
ip_cidr = listOf(dns)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (dns in directDNS) {
|
||||||
|
if (!dns.isIpAddress()) continue
|
||||||
|
route.rules.add(Rule_DefaultOptions().apply {
|
||||||
|
outbound = TAG_DIRECT
|
||||||
|
ip_cidr = listOf(dns)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass Lookup for the first profile
|
||||||
|
bypassDNSBeans.forEach {
|
||||||
|
var serverAddr = it.serverAddress
|
||||||
|
if (it is HysteriaBean && it.isMultiPort()) {
|
||||||
|
serverAddr = it.serverAddress.substringBeforeLast(":")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serverAddr.isIpAddress()) {
|
||||||
|
domainListDNSDirect.add("full:${serverAddr}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteDns.forEach {
|
||||||
|
var address = it
|
||||||
|
if (address.contains("://")) {
|
||||||
|
address = address.substringAfter("://")
|
||||||
|
}
|
||||||
|
"https://$address".toHttpUrlOrNull()?.apply {
|
||||||
|
if (!host.isIpAddress()) {
|
||||||
|
domainListDNSDirect.add("full:$host")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remote dns obj
|
||||||
|
remoteDns.firstOrNull()?.apply {
|
||||||
|
val d = this
|
||||||
|
dns.servers.add(DNSServerOptions().apply {
|
||||||
|
address = d
|
||||||
|
tag = "dns-remote"
|
||||||
|
address_resolver = "dns-direct"
|
||||||
|
applyDNSNetworkSettings(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// add directDNS objects here
|
||||||
|
directDNS.firstOrNull()?.apply {
|
||||||
|
val d = this
|
||||||
|
dns.servers.add(DNSServerOptions().apply {
|
||||||
|
address = d
|
||||||
|
tag = "dns-direct"
|
||||||
|
detour = "direct"
|
||||||
|
address_resolver = "dns-local"
|
||||||
|
applyDNSNetworkSettings(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
dns.servers.add(DNSServerOptions().apply {
|
||||||
|
address = LOCAL_DNS_SERVER
|
||||||
|
tag = "dns-local"
|
||||||
|
detour = "direct"
|
||||||
|
})
|
||||||
|
dns.servers.add(DNSServerOptions().apply {
|
||||||
|
address = "rcode://success"
|
||||||
|
tag = "dns-block"
|
||||||
|
})
|
||||||
|
|
||||||
|
// dns object user rules
|
||||||
|
if (enableDnsRouting) {
|
||||||
|
if (domainListDNSRemote.isNotEmpty() || uidListDNSRemote.isNotEmpty()) {
|
||||||
|
dns.rules.add(
|
||||||
|
DNSRule_DefaultOptions().apply {
|
||||||
|
makeSingBoxRule(domainListDNSRemote.toHashSet().toList())
|
||||||
|
user_id = uidListDNSRemote.toHashSet().toList()
|
||||||
|
server = "dns-remote"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (domainListDNSDirect.isNotEmpty() || uidListDNSDirect.isNotEmpty()) {
|
||||||
|
dns.rules.add(
|
||||||
|
DNSRule_DefaultOptions().apply {
|
||||||
|
makeSingBoxRule(domainListDNSDirect.toHashSet().toList())
|
||||||
|
user_id = uidListDNSDirect.toHashSet().toList()
|
||||||
|
server = "dns-direct"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (domainListDNSBlock.isNotEmpty()) {
|
||||||
|
dns.rules.add(
|
||||||
|
DNSRule_DefaultOptions().apply {
|
||||||
|
makeSingBoxRule(domainListDNSBlock.toHashSet().toList())
|
||||||
|
server = "dns-block"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable DNS for test
|
||||||
|
if (forTest) {
|
||||||
|
dns.servers.clear()
|
||||||
|
dns.rules.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forTest) {
|
||||||
|
route.rules.add(Rule_DefaultOptions().apply {
|
||||||
|
inbound = listOf(TAG_DNS_IN)
|
||||||
|
outbound = TAG_DNS_OUT
|
||||||
|
})
|
||||||
|
route.rules.add(Rule_DefaultOptions().apply {
|
||||||
|
port = listOf(53)
|
||||||
|
outbound = TAG_DNS_OUT
|
||||||
|
}) // TODO new mode use system dns?
|
||||||
|
if (DataStore.bypassLan && DataStore.bypassLanInCoreOnly) {
|
||||||
|
route.rules.add(Rule_DefaultOptions().apply {
|
||||||
|
outbound = TAG_BYPASS
|
||||||
|
geoip = listOf("private")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// block mcast
|
||||||
|
route.rules.add(Rule_DefaultOptions().apply {
|
||||||
|
ip_cidr = listOf("224.0.0.0/3", "ff00::/8")
|
||||||
|
source_ip_cidr = listOf("224.0.0.0/3", "ff00::/8")
|
||||||
|
outbound = TAG_BLOCK
|
||||||
|
})
|
||||||
|
dns.rules.add(DNSRule_DefaultOptions().apply {
|
||||||
|
domain_suffix = listOf(".arpa.", ".arpa")
|
||||||
|
server = "dns-block"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// fakedns obj
|
||||||
|
if (useFakeDns) {
|
||||||
|
dns.servers.add(DNSServerOptions().apply {
|
||||||
|
address = "fakedns://" + VpnService.FAKEDNS_VLAN4_CLIENT + "/15"
|
||||||
|
tag = "dns-fake"
|
||||||
|
strategy = "ipv4_only"
|
||||||
|
})
|
||||||
|
dns.rules.add(DNSRule_DefaultOptions().apply {
|
||||||
|
inbound = listOf("tun-in")
|
||||||
|
server = "dns-fake"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}.let {
|
||||||
|
ConfigBuildResult(
|
||||||
|
gson.toJson(it.asMap().apply {
|
||||||
|
mergeJSON(optionsToMerge, this)
|
||||||
|
}),
|
||||||
|
externalIndexMap,
|
||||||
|
outboundTags,
|
||||||
|
outboundTagMain,
|
||||||
|
trafficMap,
|
||||||
|
alerts
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,149 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt;
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.KryoException;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.database.SubscriptionBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.http.HttpBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.internal.ChainBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.naive.NaiveBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.socks.SOCKSBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.ssh.SSHBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.tuic.TuicBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.v2ray.VMessBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean;
|
||||||
|
import io.nekohasekai.sagernet.ktx.KryosKt;
|
||||||
|
import io.nekohasekai.sagernet.ktx.Logs;
|
||||||
|
import moe.matsuri.nb4a.proxy.neko.NekoBean;
|
||||||
|
import moe.matsuri.nb4a.proxy.config.ConfigBean;
|
||||||
|
import moe.matsuri.nb4a.utils.JavaUtil;
|
||||||
|
|
||||||
|
public class KryoConverters {
|
||||||
|
|
||||||
|
private static final byte[] NULL = new byte[0];
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static byte[] serialize(Serializable bean) {
|
||||||
|
if (bean == null) return NULL;
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
ByteBufferOutput buffer = KryosKt.byteBuffer(out);
|
||||||
|
bean.serializeToBuffer(buffer);
|
||||||
|
buffer.flush();
|
||||||
|
buffer.close();
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T extends Serializable> T deserialize(T bean, byte[] bytes) {
|
||||||
|
if (bytes == null) return bean;
|
||||||
|
ByteArrayInputStream input = new ByteArrayInputStream(bytes);
|
||||||
|
ByteBufferInput buffer = KryosKt.byteBuffer(input);
|
||||||
|
try {
|
||||||
|
bean.deserializeFromBuffer(buffer);
|
||||||
|
} catch (KryoException e) {
|
||||||
|
Logs.INSTANCE.w(e);
|
||||||
|
}
|
||||||
|
bean.initializeDefaultValues();
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static SOCKSBean socksDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new SOCKSBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static HttpBean httpDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new HttpBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static ShadowsocksBean shadowsocksDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new ShadowsocksBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static ConfigBean configDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new ConfigBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static VMessBean vmessDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new VMessBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static TrojanBean trojanDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new TrojanBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static TrojanGoBean trojanGoDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new TrojanGoBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static NaiveBean naiveDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new NaiveBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static HysteriaBean hysteriaDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new HysteriaBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static SSHBean sshDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new SSHBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static WireGuardBean wireguardDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new WireGuardBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static TuicBean tuicDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new TuicBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static ChainBean chainDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new ChainBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static NekoBean nekoDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new NekoBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static SubscriptionBean subscriptionDeserialize(byte[] bytes) {
|
||||||
|
if (JavaUtil.isEmpty(bytes)) return null;
|
||||||
|
return deserialize(new SubscriptionBean(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
58
app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt
Normal file
58
app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.R
|
||||||
|
import io.nekohasekai.sagernet.SagerNet
|
||||||
|
|
||||||
|
enum class PluginEntry(
|
||||||
|
val pluginId: String,
|
||||||
|
val displayName: String,
|
||||||
|
val packageName: String, // for play and f-droid page
|
||||||
|
val downloadSource: DownloadSource = DownloadSource()
|
||||||
|
) {
|
||||||
|
TrojanGo(
|
||||||
|
"trojan-go-plugin",
|
||||||
|
SagerNet.application.getString(R.string.action_trojan_go),
|
||||||
|
"io.nekohasekai.sagernet.plugin.trojan_go"
|
||||||
|
),
|
||||||
|
NaiveProxy(
|
||||||
|
"naive-plugin",
|
||||||
|
SagerNet.application.getString(R.string.action_naive),
|
||||||
|
"io.nekohasekai.sagernet.plugin.naive"
|
||||||
|
),
|
||||||
|
Hysteria(
|
||||||
|
"hysteria-plugin",
|
||||||
|
SagerNet.application.getString(R.string.action_hysteria),
|
||||||
|
"moe.matsuri.exe.hysteria", DownloadSource(
|
||||||
|
playStore = false,
|
||||||
|
fdroid = false,
|
||||||
|
downloadLink = "https://github.com/MatsuriDayo/plugins/releases?q=Hysteria"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
TUIC(
|
||||||
|
"tuic-plugin",
|
||||||
|
SagerNet.application.getString(R.string.action_tuic),
|
||||||
|
"io.nekohasekai.sagernet.plugin.tuic",
|
||||||
|
DownloadSource(fdroid = false)
|
||||||
|
),
|
||||||
|
;
|
||||||
|
|
||||||
|
data class DownloadSource(
|
||||||
|
val playStore: Boolean = true,
|
||||||
|
val fdroid: Boolean = true,
|
||||||
|
val downloadLink: String = "https://sagernet.org/download/"
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun find(name: String): PluginEntry? {
|
||||||
|
for (pluginEntry in enumValues<PluginEntry>()) {
|
||||||
|
if (name == pluginEntry.pluginId) {
|
||||||
|
return pluginEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput
|
||||||
|
|
||||||
|
abstract class Serializable : Parcelable {
|
||||||
|
abstract fun initializeDefaultValues()
|
||||||
|
abstract fun serializeToBuffer(output: ByteBufferOutput)
|
||||||
|
abstract fun deserializeFromBuffer(input: ByteBufferInput)
|
||||||
|
|
||||||
|
override fun describeContents() = 0
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
dest.writeByteArray(KryoConverters.serialize(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class CREATOR<T : Serializable> : Parcelable.Creator<T> {
|
||||||
|
abstract fun newInstance(): T
|
||||||
|
|
||||||
|
override fun createFromParcel(source: Parcel): T {
|
||||||
|
return KryoConverters.deserialize(newInstance(), source.createByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
30
app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
Normal file
30
app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.database.ProxyEntity
|
||||||
|
|
||||||
|
object TypeMap : HashMap<String, Int>() {
|
||||||
|
init {
|
||||||
|
this["socks"] = ProxyEntity.TYPE_SOCKS
|
||||||
|
this["http"] = ProxyEntity.TYPE_HTTP
|
||||||
|
this["ss"] = ProxyEntity.TYPE_SS
|
||||||
|
this["vmess"] = ProxyEntity.TYPE_VMESS
|
||||||
|
this["trojan"] = ProxyEntity.TYPE_TROJAN
|
||||||
|
this["trojan-go"] = ProxyEntity.TYPE_TROJAN_GO
|
||||||
|
this["naive"] = ProxyEntity.TYPE_NAIVE
|
||||||
|
this["hysteria"] = ProxyEntity.TYPE_HYSTERIA
|
||||||
|
this["ssh"] = ProxyEntity.TYPE_SSH
|
||||||
|
this["wg"] = ProxyEntity.TYPE_WG
|
||||||
|
this["tuic"] = ProxyEntity.TYPE_TUIC
|
||||||
|
this["neko"] = ProxyEntity.TYPE_NEKO
|
||||||
|
this["config"] = ProxyEntity.TYPE_CONFIG
|
||||||
|
}
|
||||||
|
|
||||||
|
val reversed = HashMap<Int, String>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
TypeMap.forEach { (key, type) ->
|
||||||
|
reversed[type] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.database.ProxyEntity
|
||||||
|
import io.nekohasekai.sagernet.database.ProxyGroup
|
||||||
|
import moe.matsuri.nb4a.utils.Util
|
||||||
|
|
||||||
|
fun parseUniversal(link: String): AbstractBean {
|
||||||
|
return if (link.contains("?")) {
|
||||||
|
val type = link.substringAfter("sn://").substringBefore("?")
|
||||||
|
ProxyEntity(type = TypeMap[type] ?: error("Type $type not found")).apply {
|
||||||
|
putByteArray(Util.zlibDecompress(Util.b64Decode(link.substringAfter("?"))))
|
||||||
|
}.requireBean()
|
||||||
|
} else {
|
||||||
|
val type = link.substringAfter("sn://").substringBefore(":")
|
||||||
|
ProxyEntity(type = TypeMap[type] ?: error("Type $type not found")).apply {
|
||||||
|
putByteArray(Util.b64Decode(link.substringAfter(":").substringAfter(":")))
|
||||||
|
}.requireBean()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AbstractBean.toUniversalLink(): String {
|
||||||
|
var link = "sn://"
|
||||||
|
link += TypeMap.reversed[ProxyEntity().putBean(this).type]
|
||||||
|
link += "?"
|
||||||
|
link += Util.b64EncodeUrlSafe(Util.zlibCompress(KryoConverters.serialize(this), 9))
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun ProxyGroup.toUniversalLink(): String {
|
||||||
|
var link = "sn://subscription?"
|
||||||
|
export = true
|
||||||
|
link += Util.b64EncodeUrlSafe(Util.zlibCompress(KryoConverters.serialize(this), 9))
|
||||||
|
export = false
|
||||||
|
return link
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.gson;
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import kotlin.collections.CollectionsKt;
|
||||||
|
import kotlin.collections.SetsKt;
|
||||||
|
import moe.matsuri.nb4a.utils.JavaUtil;
|
||||||
|
|
||||||
|
public class GsonConverters {
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static String toJson(Object value) {
|
||||||
|
if (value instanceof Collection) {
|
||||||
|
if (((Collection<?>) value).isEmpty()) return "";
|
||||||
|
}
|
||||||
|
return JavaUtil.gson.toJson(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static List toList(String value) {
|
||||||
|
if (JavaUtil.isNullOrBlank(value)) return CollectionsKt.listOf();
|
||||||
|
return JavaUtil.gson.fromJson(value, List.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
public static Set toSet(String value) {
|
||||||
|
if (JavaUtil.isNullOrBlank(value)) return SetsKt.setOf();
|
||||||
|
return JavaUtil.gson.fromJson(value, Set.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.http;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||||
|
import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean;
|
||||||
|
|
||||||
|
public class HttpBean extends StandardV2RayBean {
|
||||||
|
|
||||||
|
public String username;
|
||||||
|
public String password;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
if (username == null) username = "";
|
||||||
|
if (password == null) password = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeInt(0);
|
||||||
|
super.serialize(output);
|
||||||
|
output.writeString(username);
|
||||||
|
output.writeString(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
super.deserialize(input);
|
||||||
|
username = input.readString();
|
||||||
|
password = input.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public HttpBean clone() {
|
||||||
|
return KryoConverters.deserialize(new HttpBean(), KryoConverters.serialize(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<HttpBean> CREATOR = new CREATOR<HttpBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public HttpBean newInstance() {
|
||||||
|
return new HttpBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpBean[] newArray(int size) {
|
||||||
|
return new HttpBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.http
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.v2ray.isTLS
|
||||||
|
import io.nekohasekai.sagernet.fmt.v2ray.setTLS
|
||||||
|
import io.nekohasekai.sagernet.ktx.urlSafe
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
|
||||||
|
fun parseHttp(link: String): HttpBean {
|
||||||
|
val httpUrl = link.toHttpUrlOrNull() ?: error("Invalid http(s) link: $link")
|
||||||
|
|
||||||
|
if (httpUrl.encodedPath != "/") error("Not http proxy")
|
||||||
|
|
||||||
|
return HttpBean().apply {
|
||||||
|
serverAddress = httpUrl.host
|
||||||
|
serverPort = httpUrl.port
|
||||||
|
username = httpUrl.username
|
||||||
|
password = httpUrl.password
|
||||||
|
sni = httpUrl.queryParameter("sni")
|
||||||
|
name = httpUrl.fragment
|
||||||
|
setTLS(httpUrl.scheme == "https")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun HttpBean.toUri(): String {
|
||||||
|
val builder = HttpUrl.Builder().scheme(if (isTLS()) "https" else "http").host(serverAddress)
|
||||||
|
|
||||||
|
if (serverPort in 1..65535) {
|
||||||
|
builder.port(serverPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username.isNotBlank()) {
|
||||||
|
builder.username(username)
|
||||||
|
}
|
||||||
|
if (password.isNotBlank()) {
|
||||||
|
builder.password(password)
|
||||||
|
}
|
||||||
|
if (sni.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("sni", sni)
|
||||||
|
}
|
||||||
|
if (name.isNotBlank()) {
|
||||||
|
builder.encodedFragment(name.urlSafe())
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString()
|
||||||
|
}
|
||||||
@ -0,0 +1,155 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.hysteria;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||||
|
|
||||||
|
public class HysteriaBean extends AbstractBean {
|
||||||
|
|
||||||
|
public static final int TYPE_NONE = 0;
|
||||||
|
public static final int TYPE_STRING = 1;
|
||||||
|
public static final int TYPE_BASE64 = 2;
|
||||||
|
|
||||||
|
public Integer authPayloadType;
|
||||||
|
public String authPayload;
|
||||||
|
|
||||||
|
public static final int PROTOCOL_UDP = 0;
|
||||||
|
public static final int PROTOCOL_FAKETCP = 1;
|
||||||
|
public static final int PROTOCOL_WECHAT_VIDEO = 2;
|
||||||
|
|
||||||
|
public Integer protocol;
|
||||||
|
|
||||||
|
public String obfuscation;
|
||||||
|
public String sni;
|
||||||
|
public String alpn;
|
||||||
|
public String caText;
|
||||||
|
|
||||||
|
public Integer uploadMbps;
|
||||||
|
public Integer downloadMbps;
|
||||||
|
public Boolean allowInsecure;
|
||||||
|
public Integer streamReceiveWindow;
|
||||||
|
public Integer connectionReceiveWindow;
|
||||||
|
public Boolean disableMtuDiscovery;
|
||||||
|
public Integer hopInterval;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canMapping() {
|
||||||
|
return protocol != PROTOCOL_FAKETCP;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
if (authPayloadType == null) authPayloadType = TYPE_NONE;
|
||||||
|
if (authPayload == null) authPayload = "";
|
||||||
|
if (protocol == null) protocol = PROTOCOL_UDP;
|
||||||
|
if (obfuscation == null) obfuscation = "";
|
||||||
|
if (sni == null) sni = "";
|
||||||
|
if (alpn == null) alpn = "";
|
||||||
|
if (caText == null) caText = "";
|
||||||
|
|
||||||
|
if (uploadMbps == null) uploadMbps = 10;
|
||||||
|
if (downloadMbps == null) downloadMbps = 50;
|
||||||
|
if (allowInsecure == null) allowInsecure = false;
|
||||||
|
|
||||||
|
if (streamReceiveWindow == null) streamReceiveWindow = 0;
|
||||||
|
if (connectionReceiveWindow == null) connectionReceiveWindow = 0;
|
||||||
|
if (disableMtuDiscovery == null) disableMtuDiscovery = false;
|
||||||
|
if (hopInterval == null) hopInterval = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeInt(5);
|
||||||
|
super.serialize(output);
|
||||||
|
output.writeInt(authPayloadType);
|
||||||
|
output.writeString(authPayload);
|
||||||
|
output.writeInt(protocol);
|
||||||
|
output.writeString(obfuscation);
|
||||||
|
output.writeString(sni);
|
||||||
|
output.writeString(alpn);
|
||||||
|
|
||||||
|
output.writeInt(uploadMbps);
|
||||||
|
output.writeInt(downloadMbps);
|
||||||
|
output.writeBoolean(allowInsecure);
|
||||||
|
|
||||||
|
output.writeString(caText);
|
||||||
|
output.writeInt(streamReceiveWindow);
|
||||||
|
output.writeInt(connectionReceiveWindow);
|
||||||
|
output.writeBoolean(disableMtuDiscovery);
|
||||||
|
output.writeInt(hopInterval);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
super.deserialize(input);
|
||||||
|
authPayloadType = input.readInt();
|
||||||
|
authPayload = input.readString();
|
||||||
|
if (version >= 3) {
|
||||||
|
protocol = input.readInt();
|
||||||
|
}
|
||||||
|
obfuscation = input.readString();
|
||||||
|
sni = input.readString();
|
||||||
|
if (version >= 2) {
|
||||||
|
alpn = input.readString();
|
||||||
|
}
|
||||||
|
uploadMbps = input.readInt();
|
||||||
|
downloadMbps = input.readInt();
|
||||||
|
allowInsecure = input.readBoolean();
|
||||||
|
if (version >= 1) {
|
||||||
|
caText = input.readString();
|
||||||
|
streamReceiveWindow = input.readInt();
|
||||||
|
connectionReceiveWindow = input.readInt();
|
||||||
|
if (version != 4) disableMtuDiscovery = input.readBoolean(); // note: skip 4
|
||||||
|
}
|
||||||
|
if (version >= 5) {
|
||||||
|
hopInterval = input.readInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyFeatureSettings(AbstractBean other) {
|
||||||
|
if (!(other instanceof HysteriaBean)) return;
|
||||||
|
HysteriaBean bean = ((HysteriaBean) other);
|
||||||
|
bean.uploadMbps = uploadMbps;
|
||||||
|
bean.downloadMbps = downloadMbps;
|
||||||
|
bean.allowInsecure = allowInsecure;
|
||||||
|
bean.disableMtuDiscovery = disableMtuDiscovery;
|
||||||
|
bean.hopInterval = hopInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String displayAddress() {
|
||||||
|
if (HysteriaFmtKt.isMultiPort(this)) {
|
||||||
|
return serverAddress;
|
||||||
|
}
|
||||||
|
return super.displayAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public HysteriaBean clone() {
|
||||||
|
return KryoConverters.deserialize(new HysteriaBean(), KryoConverters.serialize(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<HysteriaBean> CREATOR = new CREATOR<HysteriaBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public HysteriaBean newInstance() {
|
||||||
|
return new HysteriaBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HysteriaBean[] newArray(int size) {
|
||||||
|
return new HysteriaBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,237 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.hysteria
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.fmt.LOCALHOST
|
||||||
|
import io.nekohasekai.sagernet.ktx.*
|
||||||
|
import moe.matsuri.nb4a.SingBoxOptions
|
||||||
|
import moe.matsuri.nb4a.utils.Util
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
// hysteria://host:port?auth=123456&peer=sni.domain&insecure=1|0&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks
|
||||||
|
|
||||||
|
fun parseHysteria(url: String): HysteriaBean {
|
||||||
|
val link = url.replace("hysteria://", "https://").toHttpUrlOrNull() ?: error(
|
||||||
|
"invalid hysteria link $url"
|
||||||
|
)
|
||||||
|
return HysteriaBean().apply {
|
||||||
|
serverAddress = link.host
|
||||||
|
serverPort = link.port
|
||||||
|
name = link.fragment
|
||||||
|
|
||||||
|
link.queryParameter("mport")?.also {
|
||||||
|
serverAddress = serverAddress.wrapIPV6Host() + ":" + it
|
||||||
|
}
|
||||||
|
link.queryParameter("peer")?.also {
|
||||||
|
sni = it
|
||||||
|
}
|
||||||
|
link.queryParameter("auth")?.takeIf { it.isNotBlank() }?.also {
|
||||||
|
authPayloadType = HysteriaBean.TYPE_STRING
|
||||||
|
authPayload = it
|
||||||
|
}
|
||||||
|
link.queryParameter("insecure")?.also {
|
||||||
|
allowInsecure = it == "1"
|
||||||
|
}
|
||||||
|
link.queryParameter("upmbps")?.also {
|
||||||
|
uploadMbps = it.toIntOrNull() ?: uploadMbps
|
||||||
|
}
|
||||||
|
link.queryParameter("downmbps")?.also {
|
||||||
|
downloadMbps = it.toIntOrNull() ?: downloadMbps
|
||||||
|
}
|
||||||
|
link.queryParameter("alpn")?.also {
|
||||||
|
alpn = it
|
||||||
|
}
|
||||||
|
link.queryParameter("obfsParam")?.also {
|
||||||
|
obfuscation = it
|
||||||
|
}
|
||||||
|
link.queryParameter("protocol")?.also {
|
||||||
|
when (it) {
|
||||||
|
"faketcp" -> {
|
||||||
|
protocol = HysteriaBean.PROTOCOL_FAKETCP
|
||||||
|
}
|
||||||
|
"wechat-video" -> {
|
||||||
|
protocol = HysteriaBean.PROTOCOL_WECHAT_VIDEO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun HysteriaBean.toUri(): String {
|
||||||
|
val builder = linkBuilder().host(serverAddress.substringBeforeLast(":")).port(serverPort)
|
||||||
|
if (isMultiPort()) {
|
||||||
|
builder.addQueryParameter("mport", serverAddress.substringAfterLast(":"))
|
||||||
|
}
|
||||||
|
if (allowInsecure) {
|
||||||
|
builder.addQueryParameter("insecure", "1")
|
||||||
|
}
|
||||||
|
if (sni.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("peer", sni)
|
||||||
|
}
|
||||||
|
if (authPayload.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("auth", authPayload)
|
||||||
|
}
|
||||||
|
builder.addQueryParameter("upmbps", "$uploadMbps")
|
||||||
|
builder.addQueryParameter("downmbps", "$downloadMbps")
|
||||||
|
if (alpn.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("alpn", alpn)
|
||||||
|
}
|
||||||
|
if (obfuscation.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("obfs", "xplus")
|
||||||
|
builder.addQueryParameter("obfsParam", obfuscation)
|
||||||
|
}
|
||||||
|
when (protocol) {
|
||||||
|
HysteriaBean.PROTOCOL_FAKETCP -> {
|
||||||
|
builder.addQueryParameter("protocol", "faketcp")
|
||||||
|
}
|
||||||
|
HysteriaBean.PROTOCOL_WECHAT_VIDEO -> {
|
||||||
|
builder.addQueryParameter("protocol", "wechat-video")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (protocol == HysteriaBean.PROTOCOL_FAKETCP) {
|
||||||
|
builder.addQueryParameter("protocol", "faketcp")
|
||||||
|
}
|
||||||
|
if (name.isNotBlank()) {
|
||||||
|
builder.encodedFragment(name.urlSafe())
|
||||||
|
}
|
||||||
|
return builder.toLink("hysteria")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun JSONObject.parseHysteria(): HysteriaBean {
|
||||||
|
return HysteriaBean().apply {
|
||||||
|
serverAddress = optString("server")
|
||||||
|
if (!isMultiPort()) {
|
||||||
|
serverAddress = optString("server").substringBeforeLast(":")
|
||||||
|
serverPort = optString("server").substringAfterLast(":").toIntOrNull() ?: 443
|
||||||
|
}
|
||||||
|
uploadMbps = getIntNya("up_mbps")
|
||||||
|
downloadMbps = getIntNya("down_mbps")
|
||||||
|
obfuscation = getStr("obfs")
|
||||||
|
getStr("auth")?.also {
|
||||||
|
authPayloadType = HysteriaBean.TYPE_BASE64
|
||||||
|
authPayload = it
|
||||||
|
}
|
||||||
|
getStr("auth_str")?.also {
|
||||||
|
authPayloadType = HysteriaBean.TYPE_STRING
|
||||||
|
authPayload = it
|
||||||
|
}
|
||||||
|
getStr("protocol")?.also {
|
||||||
|
when (it) {
|
||||||
|
"faketcp" -> {
|
||||||
|
protocol = HysteriaBean.PROTOCOL_FAKETCP
|
||||||
|
}
|
||||||
|
"wechat-video" -> {
|
||||||
|
protocol = HysteriaBean.PROTOCOL_WECHAT_VIDEO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sni = getStr("server_name")
|
||||||
|
alpn = getStr("alpn")
|
||||||
|
allowInsecure = getBool("insecure")
|
||||||
|
|
||||||
|
streamReceiveWindow = getIntNya("recv_window_conn")
|
||||||
|
connectionReceiveWindow = getIntNya("recv_window")
|
||||||
|
disableMtuDiscovery = getBool("disable_mtu_discovery")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun HysteriaBean.buildHysteriaConfig(port: Int, cacheFile: (() -> File)?): String {
|
||||||
|
return JSONObject().apply {
|
||||||
|
put("server", if (isMultiPort()) serverAddress else wrapUri())
|
||||||
|
when (protocol) {
|
||||||
|
HysteriaBean.PROTOCOL_FAKETCP -> {
|
||||||
|
put("protocol", "faketcp")
|
||||||
|
}
|
||||||
|
HysteriaBean.PROTOCOL_WECHAT_VIDEO -> {
|
||||||
|
put("protocol", "wechat-video")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
put("up_mbps", uploadMbps)
|
||||||
|
put("down_mbps", downloadMbps)
|
||||||
|
put(
|
||||||
|
"socks5", JSONObject(
|
||||||
|
mapOf(
|
||||||
|
"listen" to "$LOCALHOST:$port",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
put("retry", 5)
|
||||||
|
put("obfs", obfuscation)
|
||||||
|
when (authPayloadType) {
|
||||||
|
HysteriaBean.TYPE_BASE64 -> put("auth", authPayload)
|
||||||
|
HysteriaBean.TYPE_STRING -> put("auth_str", authPayload)
|
||||||
|
}
|
||||||
|
if (sni.isBlank() && finalAddress == LOCALHOST && !serverAddress.isIpAddress()) {
|
||||||
|
sni = serverAddress
|
||||||
|
}
|
||||||
|
if (sni.isNotBlank()) {
|
||||||
|
put("server_name", sni)
|
||||||
|
}
|
||||||
|
if (alpn.isNotBlank()) put("alpn", alpn)
|
||||||
|
if (caText.isNotBlank() && cacheFile != null) {
|
||||||
|
val caFile = cacheFile()
|
||||||
|
caFile.writeText(caText)
|
||||||
|
put("ca", caFile.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowInsecure) put("insecure", true)
|
||||||
|
if (streamReceiveWindow > 0) put("recv_window_conn", streamReceiveWindow)
|
||||||
|
if (connectionReceiveWindow > 0) put("recv_window", connectionReceiveWindow)
|
||||||
|
if (disableMtuDiscovery) put("disable_mtu_discovery", true)
|
||||||
|
|
||||||
|
// hy 1.2.0 (不兼容)
|
||||||
|
put("resolver", "udp://127.0.0.1:" + DataStore.localDNSPort)
|
||||||
|
|
||||||
|
put("hop_interval", hopInterval)
|
||||||
|
}.toStringPretty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun HysteriaBean.isMultiPort(): Boolean {
|
||||||
|
if (!serverAddress.contains(":")) return false
|
||||||
|
val p = serverAddress.substringAfterLast(":")
|
||||||
|
if (p.contains("-") || p.contains(",")) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun HysteriaBean.canUseSingBox(): Boolean {
|
||||||
|
if (isMultiPort() || protocol != HysteriaBean.PROTOCOL_UDP) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildSingBoxOutboundHysteriaBean(bean: HysteriaBean): SingBoxOptions.Outbound_HysteriaOptions {
|
||||||
|
// No multi-port
|
||||||
|
return SingBoxOptions.Outbound_HysteriaOptions().apply {
|
||||||
|
type = "hysteria"
|
||||||
|
server = bean.serverAddress
|
||||||
|
server_port = bean.serverPort
|
||||||
|
up_mbps = bean.uploadMbps
|
||||||
|
down_mbps = bean.downloadMbps
|
||||||
|
obfs = bean.obfuscation
|
||||||
|
disable_mtu_discovery = bean.disableMtuDiscovery
|
||||||
|
when (bean.authPayloadType) {
|
||||||
|
HysteriaBean.TYPE_BASE64 -> auth = Util.b64Decode(bean.authPayload).toList()
|
||||||
|
HysteriaBean.TYPE_STRING -> auth_str = bean.authPayload
|
||||||
|
}
|
||||||
|
if (bean.streamReceiveWindow > 0) {
|
||||||
|
recv_window_conn = bean.streamReceiveWindow.toLong()
|
||||||
|
}
|
||||||
|
if (bean.connectionReceiveWindow > 0) {
|
||||||
|
recv_window_conn = bean.connectionReceiveWindow.toLong()
|
||||||
|
}
|
||||||
|
tls = SingBoxOptions.OutboundTLSOptions().apply {
|
||||||
|
if (bean.sni.isNotBlank()) {
|
||||||
|
server_name = bean.sni
|
||||||
|
}
|
||||||
|
if (bean.alpn.isNotBlank()) {
|
||||||
|
alpn = bean.alpn.split("\n")
|
||||||
|
}
|
||||||
|
if (bean.caText.isNotBlank()) {
|
||||||
|
certificate = bean.caText
|
||||||
|
}
|
||||||
|
insecure = bean.allowInsecure
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.internal;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||||
|
import moe.matsuri.nb4a.utils.JavaUtil;
|
||||||
|
|
||||||
|
public class ChainBean extends InternalBean {
|
||||||
|
|
||||||
|
public List<Long> proxies;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String displayName() {
|
||||||
|
if (JavaUtil.isNotBlank(name)) {
|
||||||
|
return name;
|
||||||
|
} else {
|
||||||
|
return "Chain " + Math.abs(hashCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
if (name == null) name = "";
|
||||||
|
|
||||||
|
if (proxies == null) {
|
||||||
|
proxies = new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeInt(1);
|
||||||
|
output.writeInt(proxies.size());
|
||||||
|
for (Long proxy : proxies) {
|
||||||
|
output.writeLong(proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
if (version < 1) {
|
||||||
|
input.readString();
|
||||||
|
input.readInt();
|
||||||
|
}
|
||||||
|
int length = input.readInt();
|
||||||
|
proxies = new ArrayList<>();
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
proxies.add(input.readLong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public ChainBean clone() {
|
||||||
|
return KryoConverters.deserialize(new ChainBean(), KryoConverters.serialize(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<ChainBean> CREATOR = new CREATOR<ChainBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ChainBean newInstance() {
|
||||||
|
return new ChainBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChainBean[] newArray(int size) {
|
||||||
|
return new ChainBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.internal;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean;
|
||||||
|
|
||||||
|
public abstract class InternalBean extends AbstractBean {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String displayAddress() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canICMPing() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canTCPing() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canMapping() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.naive;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||||
|
|
||||||
|
public class NaiveBean extends AbstractBean {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available proto: https, quic.
|
||||||
|
*/
|
||||||
|
public String proto;
|
||||||
|
public String username;
|
||||||
|
public String password;
|
||||||
|
public String extraHeaders;
|
||||||
|
public String sni;
|
||||||
|
public String certificates;
|
||||||
|
public Integer insecureConcurrency;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
if (serverPort == null) serverPort = 443;
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
if (proto == null) proto = "https";
|
||||||
|
if (username == null) username = "";
|
||||||
|
if (password == null) password = "";
|
||||||
|
if (extraHeaders == null) extraHeaders = "";
|
||||||
|
if (certificates == null) certificates = "";
|
||||||
|
if (sni == null) sni = "";
|
||||||
|
if (insecureConcurrency == null) insecureConcurrency = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeInt(2);
|
||||||
|
super.serialize(output);
|
||||||
|
output.writeString(proto);
|
||||||
|
output.writeString(username);
|
||||||
|
output.writeString(password);
|
||||||
|
// note: sequence is different from SagerNet,,,
|
||||||
|
output.writeString(extraHeaders);
|
||||||
|
output.writeString(certificates);
|
||||||
|
output.writeString(sni);
|
||||||
|
output.writeInt(insecureConcurrency);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
super.deserialize(input);
|
||||||
|
proto = input.readString();
|
||||||
|
username = input.readString();
|
||||||
|
password = input.readString();
|
||||||
|
extraHeaders = input.readString();
|
||||||
|
if (version >= 2) {
|
||||||
|
certificates = input.readString();
|
||||||
|
sni = input.readString();
|
||||||
|
}
|
||||||
|
if (version >= 1) {
|
||||||
|
insecureConcurrency = input.readInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public NaiveBean clone() {
|
||||||
|
return KryoConverters.deserialize(new NaiveBean(), KryoConverters.serialize(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<NaiveBean> CREATOR = new CREATOR<NaiveBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public NaiveBean newInstance() {
|
||||||
|
return new NaiveBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NaiveBean[] newArray(int size) {
|
||||||
|
return new NaiveBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.naive
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.fmt.LOCALHOST
|
||||||
|
import io.nekohasekai.sagernet.ktx.*
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
fun parseNaive(link: String): NaiveBean {
|
||||||
|
val proto = link.substringAfter("+").substringBefore(":")
|
||||||
|
val url = ("https://" + link.substringAfter("://")).toHttpUrlOrNull()
|
||||||
|
?: error("Invalid naive link: $link")
|
||||||
|
return NaiveBean().also {
|
||||||
|
it.proto = proto
|
||||||
|
}.apply {
|
||||||
|
serverAddress = url.host
|
||||||
|
serverPort = url.port
|
||||||
|
username = url.username
|
||||||
|
password = url.password
|
||||||
|
sni = url.queryParameter("sni")
|
||||||
|
certificates = url.queryParameter("cert")
|
||||||
|
extraHeaders = url.queryParameter("extra-headers")?.unUrlSafe()?.replace("\r\n", "\n")
|
||||||
|
insecureConcurrency = url.queryParameter("insecure-concurrency")?.toIntOrNull()
|
||||||
|
name = url.fragment
|
||||||
|
initializeDefaultValues()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NaiveBean.toUri(proxyOnly: Boolean = false): String {
|
||||||
|
val builder = linkBuilder().host(finalAddress).port(finalPort)
|
||||||
|
if (username.isNotBlank()) {
|
||||||
|
builder.username(username)
|
||||||
|
if (password.isNotBlank()) {
|
||||||
|
builder.password(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!proxyOnly) {
|
||||||
|
if (sni.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("sni", sni)
|
||||||
|
}
|
||||||
|
if (certificates.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("cert", certificates)
|
||||||
|
}
|
||||||
|
if (extraHeaders.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("extra-headers", extraHeaders)
|
||||||
|
}
|
||||||
|
if (name.isNotBlank()) {
|
||||||
|
builder.encodedFragment(name.urlSafe())
|
||||||
|
}
|
||||||
|
if (insecureConcurrency > 0) {
|
||||||
|
builder.addQueryParameter("insecure-concurrency", "$insecureConcurrency")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toLink(if (proxyOnly) proto else "naive+$proto", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NaiveBean.buildNaiveConfig(port: Int): String {
|
||||||
|
return JSONObject().apply {
|
||||||
|
// process ipv6
|
||||||
|
finalAddress = finalAddress.wrapIPV6Host()
|
||||||
|
serverAddress = serverAddress.wrapIPV6Host()
|
||||||
|
|
||||||
|
// process sni
|
||||||
|
if (sni.isNotBlank()) {
|
||||||
|
put("host-resolver-rules", "MAP $sni $finalAddress")
|
||||||
|
finalAddress = sni
|
||||||
|
} else {
|
||||||
|
if (serverAddress.isIpAddress()) {
|
||||||
|
// for naive, using IP as SNI name hardly happens
|
||||||
|
// and host-resolver-rules cannot resolve the SNI problem
|
||||||
|
// so do nothing
|
||||||
|
} else {
|
||||||
|
put("host-resolver-rules", "MAP $serverAddress $finalAddress")
|
||||||
|
finalAddress = serverAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
put("listen", "socks://$LOCALHOST:$port")
|
||||||
|
put("proxy", toUri(true))
|
||||||
|
if (extraHeaders.isNotBlank()) {
|
||||||
|
put("extra-headers", extraHeaders.split("\n").joinToString("\r\n"))
|
||||||
|
}
|
||||||
|
if (DataStore.enableLog) {
|
||||||
|
put("log", "")
|
||||||
|
}
|
||||||
|
if (insecureConcurrency > 0) {
|
||||||
|
put("insecure-concurrency", insecureConcurrency)
|
||||||
|
}
|
||||||
|
}.toStringPretty()
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.shadowsocks;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||||
|
import moe.matsuri.nb4a.utils.JavaUtil;
|
||||||
|
|
||||||
|
public class ShadowsocksBean extends AbstractBean {
|
||||||
|
|
||||||
|
public String method;
|
||||||
|
public String password;
|
||||||
|
public String plugin;
|
||||||
|
|
||||||
|
public Boolean sUoT;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
|
||||||
|
if (JavaUtil.isNullOrBlank(method)) method = "aes-256-gcm";
|
||||||
|
if (method == null) method = "";
|
||||||
|
if (password == null) password = "";
|
||||||
|
if (plugin == null) plugin = "";
|
||||||
|
if (sUoT == null) sUoT = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeInt(2);
|
||||||
|
super.serialize(output);
|
||||||
|
output.writeString(method);
|
||||||
|
output.writeString(password);
|
||||||
|
output.writeString(plugin);
|
||||||
|
output.writeBoolean(sUoT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
super.deserialize(input);
|
||||||
|
method = input.readString();
|
||||||
|
password = input.readString();
|
||||||
|
plugin = input.readString();
|
||||||
|
sUoT = input.readBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public ShadowsocksBean clone() {
|
||||||
|
return KryoConverters.deserialize(new ShadowsocksBean(), KryoConverters.serialize(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<ShadowsocksBean> CREATOR = new CREATOR<ShadowsocksBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ShadowsocksBean newInstance() {
|
||||||
|
return new ShadowsocksBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ShadowsocksBean[] newArray(int size) {
|
||||||
|
return new ShadowsocksBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.shadowsocks
|
||||||
|
|
||||||
|
import moe.matsuri.nb4a.SingBoxOptions
|
||||||
|
import io.nekohasekai.sagernet.ktx.*
|
||||||
|
import moe.matsuri.nb4a.utils.Util
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
fun parseShadowsocks(url: String): ShadowsocksBean {
|
||||||
|
|
||||||
|
if (url.substringBefore("#").contains("@")) {
|
||||||
|
var link = url.replace("ss://", "https://").toHttpUrlOrNull() ?: error(
|
||||||
|
"invalid ss-android link $url"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (link.username.isBlank()) { // fix justmysocks's shit link
|
||||||
|
link = (("https://" + url.substringAfter("ss://")
|
||||||
|
.substringBefore("#")
|
||||||
|
.decodeBase64UrlSafe()).toHttpUrlOrNull()
|
||||||
|
?: error("invalid jms link $url")
|
||||||
|
).newBuilder().fragment(url.substringAfter("#")).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ss-android style
|
||||||
|
|
||||||
|
if (link.password.isNotBlank()) {
|
||||||
|
return ShadowsocksBean().apply {
|
||||||
|
serverAddress = link.host
|
||||||
|
serverPort = link.port
|
||||||
|
method = link.username
|
||||||
|
password = link.password
|
||||||
|
plugin = link.queryParameter("plugin") ?: ""
|
||||||
|
name = link.fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val methodAndPswd = link.username.decodeBase64UrlSafe()
|
||||||
|
|
||||||
|
return ShadowsocksBean().apply {
|
||||||
|
serverAddress = link.host
|
||||||
|
serverPort = link.port
|
||||||
|
method = methodAndPswd.substringBefore(":")
|
||||||
|
password = methodAndPswd.substringAfter(":")
|
||||||
|
plugin = link.queryParameter("plugin") ?: ""
|
||||||
|
name = link.fragment
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// v2rayN style
|
||||||
|
var v2Url = url
|
||||||
|
|
||||||
|
if (v2Url.contains("#")) v2Url = v2Url.substringBefore("#")
|
||||||
|
|
||||||
|
val link = ("https://" + v2Url.substringAfter("ss://")
|
||||||
|
.decodeBase64UrlSafe()).toHttpUrlOrNull() ?: error("invalid v2rayN link $url")
|
||||||
|
|
||||||
|
return ShadowsocksBean().apply {
|
||||||
|
serverAddress = link.host
|
||||||
|
serverPort = link.port
|
||||||
|
method = link.username
|
||||||
|
password = link.password
|
||||||
|
plugin = ""
|
||||||
|
val remarks = url.substringAfter("#").unUrlSafe()
|
||||||
|
if (remarks.isNotBlank()) name = remarks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ShadowsocksBean.toUri(): String {
|
||||||
|
|
||||||
|
val builder = linkBuilder().username(Util.b64EncodeUrlSafe("$method:$password"))
|
||||||
|
.host(serverAddress)
|
||||||
|
.port(serverPort)
|
||||||
|
|
||||||
|
if (plugin.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("plugin", plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.isNotBlank()) {
|
||||||
|
builder.encodedFragment(name.urlSafe())
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toLink("ss").replace("$serverPort/", "$serverPort")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun JSONObject.parseShadowsocks(): ShadowsocksBean {
|
||||||
|
return ShadowsocksBean().apply {
|
||||||
|
serverAddress = getStr("server")
|
||||||
|
serverPort = getIntNya("server_port")
|
||||||
|
password = getStr("password")
|
||||||
|
method = getStr("method")
|
||||||
|
name = optString("remarks", "")
|
||||||
|
|
||||||
|
val pId = getStr("plugin")
|
||||||
|
if (!pId.isNullOrBlank()) {
|
||||||
|
plugin = pId + ";" + optString("plugin_opts", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildSingBoxOutboundShadowsocksBean(bean: ShadowsocksBean): SingBoxOptions.Outbound_ShadowsocksOptions {
|
||||||
|
return SingBoxOptions.Outbound_ShadowsocksOptions().apply {
|
||||||
|
type = "shadowsocks"
|
||||||
|
server = bean.serverAddress
|
||||||
|
server_port = bean.serverPort
|
||||||
|
password = bean.password
|
||||||
|
method = bean.method
|
||||||
|
udp_over_tcp = bean.sUoT
|
||||||
|
if (bean.plugin.isNotBlank()) {
|
||||||
|
plugin = bean.plugin.substringBefore(";")
|
||||||
|
plugin_opts = bean.plugin.substringAfter(";")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.socks;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||||
|
|
||||||
|
public class SOCKSBean extends AbstractBean {
|
||||||
|
|
||||||
|
public Integer protocol;
|
||||||
|
|
||||||
|
public int protocolVersion() {
|
||||||
|
switch (protocol) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
return 4;
|
||||||
|
default:
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String protocolName() {
|
||||||
|
switch (protocol) {
|
||||||
|
case 0:
|
||||||
|
return "SOCKS4";
|
||||||
|
case 1:
|
||||||
|
return "SOCKS4A";
|
||||||
|
default:
|
||||||
|
return "SOCKS5";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String protocolVersionName() {
|
||||||
|
switch (protocol) {
|
||||||
|
case 0:
|
||||||
|
return "4";
|
||||||
|
case 1:
|
||||||
|
return "4a";
|
||||||
|
default:
|
||||||
|
return "5";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String username;
|
||||||
|
public String password;
|
||||||
|
|
||||||
|
public static final int PROTOCOL_SOCKS4 = 0;
|
||||||
|
public static final int PROTOCOL_SOCKS4A = 1;
|
||||||
|
public static final int PROTOCOL_SOCKS5 = 2;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String network() {
|
||||||
|
if (protocol < PROTOCOL_SOCKS5) return "tcp";
|
||||||
|
return super.network();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
|
||||||
|
if (protocol == null) protocol = PROTOCOL_SOCKS5;
|
||||||
|
if (username == null) username = "";
|
||||||
|
if (password == null) password = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeInt(1);
|
||||||
|
super.serialize(output);
|
||||||
|
output.writeInt(protocol);
|
||||||
|
output.writeString(username);
|
||||||
|
output.writeString(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
super.deserialize(input);
|
||||||
|
if (version >= 1) {
|
||||||
|
protocol = input.readInt();
|
||||||
|
}
|
||||||
|
username = input.readString();
|
||||||
|
password = input.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public SOCKSBean clone() {
|
||||||
|
return KryoConverters.deserialize(new SOCKSBean(), KryoConverters.serialize(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<SOCKSBean> CREATOR = new CREATOR<SOCKSBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public SOCKSBean newInstance() {
|
||||||
|
return new SOCKSBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SOCKSBean[] newArray(int size) {
|
||||||
|
return new SOCKSBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.socks
|
||||||
|
|
||||||
|
import moe.matsuri.nb4a.SingBoxOptions
|
||||||
|
import io.nekohasekai.sagernet.ktx.*
|
||||||
|
import moe.matsuri.nb4a.utils.NGUtil
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
|
||||||
|
fun parseSOCKS(link: String): SOCKSBean {
|
||||||
|
if (!link.substringAfter("://").contains(":")) {
|
||||||
|
// v2rayN shit format
|
||||||
|
var url = link.substringAfter("://")
|
||||||
|
if (url.contains("#")) {
|
||||||
|
url = url.substringBeforeLast("#")
|
||||||
|
}
|
||||||
|
url = url.decodeBase64UrlSafe()
|
||||||
|
val httpUrl = "http://$url".toHttpUrlOrNull() ?: error("Invalid v2rayN link content: $url")
|
||||||
|
return SOCKSBean().apply {
|
||||||
|
serverAddress = httpUrl.host
|
||||||
|
serverPort = httpUrl.port
|
||||||
|
username = httpUrl.username.takeIf { it != "null" } ?: ""
|
||||||
|
password = httpUrl.password.takeIf { it != "null" } ?: ""
|
||||||
|
if (link.contains("#")) {
|
||||||
|
name = link.substringAfter("#").unUrlSafe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val url = ("http://" + link.substringAfter("://")).toHttpUrlOrNull()
|
||||||
|
?: error("Not supported: $link")
|
||||||
|
|
||||||
|
return SOCKSBean().apply {
|
||||||
|
protocol = when {
|
||||||
|
link.startsWith("socks4://") -> SOCKSBean.PROTOCOL_SOCKS4
|
||||||
|
link.startsWith("socks4a://") -> SOCKSBean.PROTOCOL_SOCKS4A
|
||||||
|
else -> SOCKSBean.PROTOCOL_SOCKS5
|
||||||
|
}
|
||||||
|
serverAddress = url.host
|
||||||
|
serverPort = url.port
|
||||||
|
username = url.username
|
||||||
|
password = url.password
|
||||||
|
name = url.fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SOCKSBean.toUri(): String {
|
||||||
|
|
||||||
|
val builder = HttpUrl.Builder().scheme("http").host(serverAddress).port(serverPort)
|
||||||
|
if (!username.isNullOrBlank()) builder.username(username)
|
||||||
|
if (!password.isNullOrBlank()) builder.password(password)
|
||||||
|
if (!name.isNullOrBlank()) builder.encodedFragment(name.urlSafe())
|
||||||
|
return builder.toLink("socks${protocolVersion()}")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SOCKSBean.toV2rayN(): String {
|
||||||
|
|
||||||
|
var link = ""
|
||||||
|
if (username.isNotBlank()) {
|
||||||
|
link += username.urlSafe() + ":" + password.urlSafe() + "@"
|
||||||
|
}
|
||||||
|
link += "$serverAddress:$serverPort"
|
||||||
|
link = "socks://" + NGUtil.encode(link)
|
||||||
|
if (name.isNotBlank()) {
|
||||||
|
link += "#" + name.urlSafe()
|
||||||
|
}
|
||||||
|
|
||||||
|
return link
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildSingBoxOutboundSocksBean(bean: SOCKSBean): SingBoxOptions.Outbound_SocksOptions {
|
||||||
|
return SingBoxOptions.Outbound_SocksOptions().apply {
|
||||||
|
type = "socks"
|
||||||
|
server = bean.serverAddress
|
||||||
|
server_port = bean.serverPort
|
||||||
|
username = bean.username
|
||||||
|
password = bean.password
|
||||||
|
version = bean.protocolVersionName()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.ssh;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||||
|
|
||||||
|
public class SSHBean extends AbstractBean {
|
||||||
|
|
||||||
|
public static final int AUTH_TYPE_NONE = 0;
|
||||||
|
public static final int AUTH_TYPE_PASSWORD = 1;
|
||||||
|
public static final int AUTH_TYPE_PRIVATE_KEY = 2;
|
||||||
|
|
||||||
|
public String username;
|
||||||
|
public Integer authType;
|
||||||
|
public String password;
|
||||||
|
public String privateKey;
|
||||||
|
public String privateKeyPassphrase;
|
||||||
|
public String publicKey;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
if (serverPort == null) serverPort = 22;
|
||||||
|
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
|
||||||
|
if (username == null) username = "root";
|
||||||
|
if (authType == null) authType = AUTH_TYPE_PASSWORD;
|
||||||
|
if (password == null) password = "";
|
||||||
|
if (privateKey == null) privateKey = "";
|
||||||
|
if (privateKeyPassphrase == null) privateKeyPassphrase = "";
|
||||||
|
if (publicKey == null) publicKey = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeInt(0);
|
||||||
|
super.serialize(output);
|
||||||
|
output.writeString(username);
|
||||||
|
output.writeInt(authType);
|
||||||
|
switch (authType) {
|
||||||
|
case AUTH_TYPE_NONE:
|
||||||
|
break;
|
||||||
|
case AUTH_TYPE_PASSWORD:
|
||||||
|
output.writeString(password);
|
||||||
|
break;
|
||||||
|
case AUTH_TYPE_PRIVATE_KEY:
|
||||||
|
output.writeString(privateKey);
|
||||||
|
output.writeString(privateKeyPassphrase);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
output.writeString(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
super.deserialize(input);
|
||||||
|
username = input.readString();
|
||||||
|
authType = input.readInt();
|
||||||
|
switch (authType) {
|
||||||
|
case AUTH_TYPE_NONE:
|
||||||
|
break;
|
||||||
|
case AUTH_TYPE_PASSWORD:
|
||||||
|
password = input.readString();
|
||||||
|
break;
|
||||||
|
case AUTH_TYPE_PRIVATE_KEY:
|
||||||
|
privateKey = input.readString();
|
||||||
|
privateKeyPassphrase = input.readString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
publicKey = input.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public SSHBean clone() {
|
||||||
|
return KryoConverters.deserialize(new SSHBean(), KryoConverters.serialize(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<SSHBean> CREATOR = new CREATOR<SSHBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public SSHBean newInstance() {
|
||||||
|
return new SSHBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SSHBean[] newArray(int size) {
|
||||||
|
return new SSHBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
22
app/src/main/java/io/nekohasekai/sagernet/fmt/ssh/SSHFmt.kt
Normal file
22
app/src/main/java/io/nekohasekai/sagernet/fmt/ssh/SSHFmt.kt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.ssh
|
||||||
|
|
||||||
|
import moe.matsuri.nb4a.SingBoxOptions
|
||||||
|
|
||||||
|
fun buildSingBoxOutboundSSHBean(bean: SSHBean): SingBoxOptions.Outbound_SSHOptions {
|
||||||
|
return SingBoxOptions.Outbound_SSHOptions().apply {
|
||||||
|
type = "ssh"
|
||||||
|
server = bean.serverAddress
|
||||||
|
server_port = bean.serverPort
|
||||||
|
user = bean.username
|
||||||
|
host_key = bean.privateKey.split("\n")
|
||||||
|
when (bean.authType) {
|
||||||
|
SSHBean.AUTH_TYPE_PRIVATE_KEY -> {
|
||||||
|
private_key = bean.privateKey
|
||||||
|
private_key_passphrase = bean.privateKeyPassphrase
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
password = bean.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.trojan;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||||
|
import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean;
|
||||||
|
|
||||||
|
public class TrojanBean extends StandardV2RayBean {
|
||||||
|
|
||||||
|
public String password;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
if (security == null || security.isEmpty()) security = "tls";
|
||||||
|
if (password == null) password = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeInt(2);
|
||||||
|
super.serialize(output);
|
||||||
|
output.writeString(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
if (version >= 2) {
|
||||||
|
super.deserialize(input); // StandardV2RayBean
|
||||||
|
password = input.readString();
|
||||||
|
} else {
|
||||||
|
// From AbstractBean
|
||||||
|
serverAddress = input.readString();
|
||||||
|
serverPort = input.readInt();
|
||||||
|
// From TrojanBean
|
||||||
|
password = input.readString();
|
||||||
|
security = input.readString();
|
||||||
|
sni = input.readString();
|
||||||
|
alpn = input.readString();
|
||||||
|
if (version == 1) allowInsecure = input.readBoolean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public TrojanBean clone() {
|
||||||
|
return KryoConverters.deserialize(new TrojanBean(), KryoConverters.serialize(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<TrojanBean> CREATOR = new CREATOR<TrojanBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public TrojanBean newInstance() {
|
||||||
|
return new TrojanBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TrojanBean[] newArray(int size) {
|
||||||
|
return new TrojanBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.trojan
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.v2ray.parseDuckSoft
|
||||||
|
import io.nekohasekai.sagernet.fmt.v2ray.toUri
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
|
||||||
|
fun parseTrojan(server: String): TrojanBean {
|
||||||
|
|
||||||
|
val link = server.replace("trojan://", "https://").toHttpUrlOrNull()
|
||||||
|
?: error("invalid trojan link $server")
|
||||||
|
|
||||||
|
return TrojanBean().apply {
|
||||||
|
parseDuckSoft(link)
|
||||||
|
link.queryParameter("allowInsecure")
|
||||||
|
?.apply { if (this == "1" || this == "true") allowInsecure = true }
|
||||||
|
link.queryParameter("peer")?.apply { if (this.isNotBlank()) sni = this }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TrojanBean.toUri(): String {
|
||||||
|
return toUri(true).replace("vmess://", "trojan://")
|
||||||
|
}
|
||||||
@ -0,0 +1,176 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.trojan_go;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||||
|
import moe.matsuri.nb4a.utils.JavaUtil;
|
||||||
|
|
||||||
|
public class TrojanGoBean extends AbstractBean {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trojan 的密码。
|
||||||
|
* 不可省略,不能为空字符串,不建议含有非 ASCII 可打印字符。
|
||||||
|
* 必须使用 encodeURIComponent 编码。
|
||||||
|
*/
|
||||||
|
public String password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义 TLS 的 SNI。
|
||||||
|
* 省略时默认与 trojan-host 同值。不得为空字符串。
|
||||||
|
* <p>
|
||||||
|
* 必须使用 encodeURIComponent 编码。
|
||||||
|
*/
|
||||||
|
public String sni;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 传输类型。
|
||||||
|
* 省略时默认为 original,但不可为空字符串。
|
||||||
|
* 目前可选值只有 original 和 ws,未来可能会有 h2、h2+ws 等取值。
|
||||||
|
* <p>
|
||||||
|
* 当取值为 original 时,使用原始 Trojan 传输方式,无法方便通过 CDN。
|
||||||
|
* 当取值为 ws 时,使用 wss 作为传输层。
|
||||||
|
*/
|
||||||
|
public String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义 HTTP Host 头。
|
||||||
|
* 可以省略,省略时值同 trojan-host。
|
||||||
|
* 可以为空字符串,但可能带来非预期情形。
|
||||||
|
* <p>
|
||||||
|
* 警告:若你的端口非标准端口(不是 80 / 443),RFC 标准规定 Host 应在主机名后附上端口号,例如 example.com:44333。至于是否遵守,请自行斟酌。
|
||||||
|
* <p>
|
||||||
|
* 必须使用 encodeURIComponent 编码。
|
||||||
|
*/
|
||||||
|
public String host;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当传输类型 type 取 ws、h2、h2+ws 时,此项有效。
|
||||||
|
* 不可省略,不可为空。
|
||||||
|
* 必须以 / 开头。
|
||||||
|
* 可以使用 URL 中的 & # ? 等字符,但应当是合法的 URL 路径。
|
||||||
|
* <p>
|
||||||
|
* 必须使用 encodeURIComponent 编码。
|
||||||
|
*/
|
||||||
|
public String path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于保证 Trojan 流量密码学安全的加密层。
|
||||||
|
* 可省略,默认为 none,即不使用加密。
|
||||||
|
* 不可以为空字符串。
|
||||||
|
* <p>
|
||||||
|
* 必须使用 encodeURIComponent 编码。
|
||||||
|
* <p>
|
||||||
|
* 使用 Shadowsocks 算法进行流量加密时,其格式为:
|
||||||
|
* <p>
|
||||||
|
* ss;method:password
|
||||||
|
* <p>
|
||||||
|
* 其中 ss 是固定内容,method 是加密方法,必须为下列之一:
|
||||||
|
* <p>
|
||||||
|
* aes-128-gcm
|
||||||
|
* aes-256-gcm
|
||||||
|
* chacha20-ietf-poly1305
|
||||||
|
*/
|
||||||
|
public String encryption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 额外的插件选项。本字段保留。
|
||||||
|
* 可省略,但不可以为空字符串。
|
||||||
|
*/
|
||||||
|
// not used in NB4A
|
||||||
|
public String plugin;
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
public Boolean allowInsecure;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
|
||||||
|
if (password == null) password = "";
|
||||||
|
if (sni == null) sni = "";
|
||||||
|
if (JavaUtil.isNullOrBlank(type)) type = "original";
|
||||||
|
if (host == null) host = "";
|
||||||
|
if (path == null) path = "";
|
||||||
|
if (JavaUtil.isNullOrBlank(encryption)) encryption = "none";
|
||||||
|
if (plugin == null) plugin = "";
|
||||||
|
if (allowInsecure == null) allowInsecure = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeInt(1);
|
||||||
|
super.serialize(output);
|
||||||
|
output.writeString(password);
|
||||||
|
output.writeString(sni);
|
||||||
|
output.writeString(type);
|
||||||
|
//noinspection SwitchStatementWithTooFewBranches
|
||||||
|
switch (type) {
|
||||||
|
case "ws": {
|
||||||
|
output.writeString(host);
|
||||||
|
output.writeString(path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.writeString(encryption);
|
||||||
|
output.writeString(plugin);
|
||||||
|
output.writeBoolean(allowInsecure);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
super.deserialize(input);
|
||||||
|
|
||||||
|
password = input.readString();
|
||||||
|
sni = input.readString();
|
||||||
|
type = input.readString();
|
||||||
|
//noinspection SwitchStatementWithTooFewBranches
|
||||||
|
switch (type) {
|
||||||
|
case "ws": {
|
||||||
|
host = input.readString();
|
||||||
|
path = input.readString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encryption = input.readString();
|
||||||
|
plugin = input.readString();
|
||||||
|
if (version >= 1) {
|
||||||
|
allowInsecure = input.readBoolean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyFeatureSettings(AbstractBean other) {
|
||||||
|
if (!(other instanceof TrojanGoBean)) return;
|
||||||
|
TrojanGoBean bean = ((TrojanGoBean) other);
|
||||||
|
if (allowInsecure) {
|
||||||
|
bean.allowInsecure = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public TrojanGoBean clone() {
|
||||||
|
return KryoConverters.deserialize(new TrojanGoBean(), KryoConverters.serialize(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<TrojanGoBean> CREATOR = new CREATOR<TrojanGoBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public TrojanGoBean newInstance() {
|
||||||
|
return new TrojanGoBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TrojanGoBean[] newArray(int size) {
|
||||||
|
return new TrojanGoBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,162 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.trojan_go
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.IPv6Mode
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.fmt.LOCALHOST
|
||||||
|
import io.nekohasekai.sagernet.ktx.*
|
||||||
|
import moe.matsuri.nb4a.Protocols
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
fun parseTrojanGo(server: String): TrojanGoBean {
|
||||||
|
val link = server.replace("trojan-go://", "https://").toHttpUrlOrNull() ?: error(
|
||||||
|
"invalid trojan-link link $server"
|
||||||
|
)
|
||||||
|
return TrojanGoBean().apply {
|
||||||
|
serverAddress = link.host
|
||||||
|
serverPort = link.port
|
||||||
|
password = link.username
|
||||||
|
link.queryParameter("sni")?.let {
|
||||||
|
sni = it
|
||||||
|
}
|
||||||
|
link.queryParameter("type")?.let { lType ->
|
||||||
|
type = lType
|
||||||
|
|
||||||
|
when (type) {
|
||||||
|
"ws" -> {
|
||||||
|
link.queryParameter("host")?.let {
|
||||||
|
host = it
|
||||||
|
}
|
||||||
|
link.queryParameter("path")?.let {
|
||||||
|
path = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
link.queryParameter("encryption")?.let {
|
||||||
|
encryption = it
|
||||||
|
}
|
||||||
|
link.queryParameter("plugin")?.let {
|
||||||
|
plugin = it
|
||||||
|
}
|
||||||
|
link.fragment.takeIf { !it.isNullOrBlank() }?.let {
|
||||||
|
name = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TrojanGoBean.toUri(): String {
|
||||||
|
val builder = linkBuilder().username(password).host(serverAddress).port(serverPort)
|
||||||
|
if (sni.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("sni", sni)
|
||||||
|
}
|
||||||
|
if (type.isNotBlank() && type != "original") {
|
||||||
|
builder.addQueryParameter("type", type)
|
||||||
|
|
||||||
|
when (type) {
|
||||||
|
"ws" -> {
|
||||||
|
if (host.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("host", host)
|
||||||
|
}
|
||||||
|
if (path.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("path", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type.isNotBlank() && type != "none") {
|
||||||
|
builder.addQueryParameter("encryption", encryption)
|
||||||
|
}
|
||||||
|
if (plugin.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("plugin", plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.isNotBlank()) {
|
||||||
|
builder.encodedFragment(name.urlSafe())
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toLink("trojan-go")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TrojanGoBean.buildTrojanGoConfig(port: Int): String {
|
||||||
|
return JSONObject().apply {
|
||||||
|
put("run_type", "client")
|
||||||
|
put("local_addr", LOCALHOST)
|
||||||
|
put("local_port", port)
|
||||||
|
put("remote_addr", finalAddress)
|
||||||
|
put("remote_port", finalPort)
|
||||||
|
put("password", JSONArray().apply {
|
||||||
|
put(password)
|
||||||
|
})
|
||||||
|
put("log_level", if (DataStore.enableLog) 0 else 2)
|
||||||
|
if (Protocols.shouldEnableMux("trojan-go")) put("mux", JSONObject().apply {
|
||||||
|
put("enabled", true)
|
||||||
|
put("concurrency", DataStore.muxConcurrency)
|
||||||
|
})
|
||||||
|
put("tcp", JSONObject().apply {
|
||||||
|
put("prefer_ipv4", DataStore.ipv6Mode <= IPv6Mode.ENABLE)
|
||||||
|
})
|
||||||
|
|
||||||
|
when (type) {
|
||||||
|
"original" -> {
|
||||||
|
}
|
||||||
|
"ws" -> put("websocket", JSONObject().apply {
|
||||||
|
put("enabled", true)
|
||||||
|
put("host", host)
|
||||||
|
put("path", path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sni.isBlank() && finalAddress == LOCALHOST && !serverAddress.isIpAddress()) {
|
||||||
|
sni = serverAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
put("ssl", JSONObject().apply {
|
||||||
|
if (sni.isNotBlank()) put("sni", sni)
|
||||||
|
if (allowInsecure) put("verify", false)
|
||||||
|
})
|
||||||
|
|
||||||
|
when {
|
||||||
|
encryption == "none" -> {
|
||||||
|
}
|
||||||
|
encryption.startsWith("ss;") -> put("shadowsocks", JSONObject().apply {
|
||||||
|
put("enabled", true)
|
||||||
|
put("method", encryption.substringAfter(";").substringBefore(":"))
|
||||||
|
put("password", encryption.substringAfter(":"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}.toStringPretty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun JSONObject.parseTrojanGo(): TrojanGoBean {
|
||||||
|
return TrojanGoBean().applyDefaultValues().apply {
|
||||||
|
serverAddress = optString("remote_addr", serverAddress)
|
||||||
|
serverPort = optInt("remote_port", serverPort)
|
||||||
|
when (val pass = get("password")) {
|
||||||
|
is String -> {
|
||||||
|
password = pass
|
||||||
|
}
|
||||||
|
is List<*> -> {
|
||||||
|
password = pass[0] as String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
optJSONArray("ssl")?.apply {
|
||||||
|
sni = optString("sni", sni)
|
||||||
|
}
|
||||||
|
optJSONArray("websocket")?.apply {
|
||||||
|
if (optBoolean("enabled", false)) {
|
||||||
|
type = "ws"
|
||||||
|
host = optString("host", host)
|
||||||
|
path = optString("path", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
optJSONArray("shadowsocks")?.apply {
|
||||||
|
if (optBoolean("enabled", false)) {
|
||||||
|
encryption = "ss;${optString("method", "")}:${optString("password", "")}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.tuic;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||||
|
|
||||||
|
public class TuicBean extends AbstractBean {
|
||||||
|
|
||||||
|
public String token;
|
||||||
|
public String caText;
|
||||||
|
public String udpRelayMode;
|
||||||
|
public String congestionController;
|
||||||
|
public String alpn;
|
||||||
|
public Boolean disableSNI;
|
||||||
|
public Boolean reduceRTT;
|
||||||
|
public Integer mtu;
|
||||||
|
public String sni;
|
||||||
|
public Boolean fastConnect;
|
||||||
|
public Boolean allowInsecure;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
if (token == null) token = "";
|
||||||
|
if (caText == null) caText = "";
|
||||||
|
if (udpRelayMode == null) udpRelayMode = "native";
|
||||||
|
if (congestionController == null) congestionController = "cubic";
|
||||||
|
if (alpn == null) alpn = "";
|
||||||
|
if (disableSNI == null) disableSNI = false;
|
||||||
|
if (reduceRTT == null) reduceRTT = false;
|
||||||
|
if (mtu == null) mtu = 1400;
|
||||||
|
if (sni == null) sni = "";
|
||||||
|
if (fastConnect == null) fastConnect = false;
|
||||||
|
if (allowInsecure == null) allowInsecure = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeInt(1);
|
||||||
|
super.serialize(output);
|
||||||
|
output.writeString(token);
|
||||||
|
output.writeString(caText);
|
||||||
|
output.writeString(udpRelayMode);
|
||||||
|
output.writeString(congestionController);
|
||||||
|
output.writeString(alpn);
|
||||||
|
output.writeBoolean(disableSNI);
|
||||||
|
output.writeBoolean(reduceRTT);
|
||||||
|
output.writeInt(mtu);
|
||||||
|
output.writeString(sni);
|
||||||
|
output.writeBoolean(fastConnect);
|
||||||
|
output.writeBoolean(allowInsecure);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
super.deserialize(input);
|
||||||
|
token = input.readString();
|
||||||
|
caText = input.readString();
|
||||||
|
udpRelayMode = input.readString();
|
||||||
|
congestionController = input.readString();
|
||||||
|
alpn = input.readString();
|
||||||
|
disableSNI = input.readBoolean();
|
||||||
|
reduceRTT = input.readBoolean();
|
||||||
|
mtu = input.readInt();
|
||||||
|
sni = input.readString();
|
||||||
|
if (version >= 1) {
|
||||||
|
fastConnect = input.readBoolean();
|
||||||
|
allowInsecure = input.readBoolean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public TuicBean clone() {
|
||||||
|
return KryoConverters.deserialize(new TuicBean(), KryoConverters.serialize(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<TuicBean> CREATOR = new CREATOR<TuicBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public TuicBean newInstance() {
|
||||||
|
return new TuicBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TuicBean[] newArray(int size) {
|
||||||
|
return new TuicBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.tuic
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.fmt.LOCALHOST
|
||||||
|
import io.nekohasekai.sagernet.ktx.isIpAddress
|
||||||
|
import io.nekohasekai.sagernet.ktx.toStringPretty
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import moe.matsuri.nb4a.plugin.Plugins
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
fun TuicBean.buildTuicConfig(port: Int, cacheFile: (() -> File)?): String {
|
||||||
|
if (Plugins.isUsingMatsuriExe("tuic-plugin")) {
|
||||||
|
if (!serverAddress.isIpAddress()) {
|
||||||
|
runBlocking {
|
||||||
|
finalAddress = withContext(Dispatchers.IO) {
|
||||||
|
InetAddress.getAllByName(serverAddress)
|
||||||
|
}?.firstOrNull()?.hostAddress ?: "127.0.0.1"
|
||||||
|
// TODO network on main thread, tuic don't support "sni"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JSONObject().apply {
|
||||||
|
put("relay", JSONObject().apply {
|
||||||
|
if (sni.isNotBlank()) {
|
||||||
|
put("server", sni)
|
||||||
|
put("ip", finalAddress)
|
||||||
|
} else if (serverAddress.isIpAddress()) {
|
||||||
|
put("server", finalAddress)
|
||||||
|
} else {
|
||||||
|
put("server", serverAddress)
|
||||||
|
put("ip", finalAddress)
|
||||||
|
}
|
||||||
|
put("port", finalPort)
|
||||||
|
put("token", token)
|
||||||
|
|
||||||
|
if (caText.isNotBlank() && cacheFile != null) {
|
||||||
|
val caFile = cacheFile()
|
||||||
|
caFile.writeText(caText)
|
||||||
|
put("certificates", JSONArray(listOf(caFile.absolutePath)))
|
||||||
|
}
|
||||||
|
|
||||||
|
put("udp_relay_mode", udpRelayMode)
|
||||||
|
if (alpn.isNotBlank()) {
|
||||||
|
put("alpn", JSONArray(alpn.split("\n")))
|
||||||
|
}
|
||||||
|
put("congestion_controller", congestionController)
|
||||||
|
put("disable_sni", disableSNI)
|
||||||
|
put("reduce_rtt", reduceRTT)
|
||||||
|
put("max_udp_relay_packet_size", mtu)
|
||||||
|
if (fastConnect) put("fast_connect", true)
|
||||||
|
if (allowInsecure) put("insecure", true)
|
||||||
|
})
|
||||||
|
put("local", JSONObject().apply {
|
||||||
|
put("ip", LOCALHOST)
|
||||||
|
put("port", port)
|
||||||
|
})
|
||||||
|
put("log_level", if (DataStore.enableLog) "debug" else "info")
|
||||||
|
}.toStringPretty()
|
||||||
|
}
|
||||||
@ -0,0 +1,194 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.v2ray;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean;
|
||||||
|
import moe.matsuri.nb4a.utils.JavaUtil;
|
||||||
|
|
||||||
|
public abstract class StandardV2RayBean extends AbstractBean {
|
||||||
|
|
||||||
|
public String uuid;
|
||||||
|
public String encryption; // or VLESS flow
|
||||||
|
|
||||||
|
//////// End of VMess & VLESS ////////
|
||||||
|
|
||||||
|
// "V2Ray Transport" tcp/http/ws/quic/grpc
|
||||||
|
public String type;
|
||||||
|
|
||||||
|
public String host;
|
||||||
|
|
||||||
|
public String path;
|
||||||
|
|
||||||
|
// --------------------------------------- tls?
|
||||||
|
|
||||||
|
public String security;
|
||||||
|
|
||||||
|
public String sni;
|
||||||
|
|
||||||
|
public String alpn;
|
||||||
|
|
||||||
|
public String utlsFingerprint;
|
||||||
|
|
||||||
|
public Boolean allowInsecure;
|
||||||
|
|
||||||
|
// --------------------------------------- reality
|
||||||
|
|
||||||
|
|
||||||
|
public String realityPubKey;
|
||||||
|
|
||||||
|
public String realityShortId;
|
||||||
|
|
||||||
|
|
||||||
|
// --------------------------------------- //
|
||||||
|
|
||||||
|
public Integer wsMaxEarlyData;
|
||||||
|
public String earlyDataHeaderName;
|
||||||
|
|
||||||
|
public String certificates;
|
||||||
|
|
||||||
|
// --------------------------------------- //
|
||||||
|
|
||||||
|
public Integer packetEncoding; // 1:packet 2:xudp
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
|
||||||
|
if (JavaUtil.isNullOrBlank(uuid)) uuid = "";
|
||||||
|
|
||||||
|
if (JavaUtil.isNullOrBlank(type)) type = "tcp";
|
||||||
|
else if ("h2".equals(type)) type = "http";
|
||||||
|
|
||||||
|
if (JavaUtil.isNullOrBlank(host)) host = "";
|
||||||
|
if (JavaUtil.isNullOrBlank(path)) path = "";
|
||||||
|
|
||||||
|
if (JavaUtil.isNullOrBlank(security)) security = "none";
|
||||||
|
if (JavaUtil.isNullOrBlank(sni)) sni = "";
|
||||||
|
if (JavaUtil.isNullOrBlank(alpn)) alpn = "";
|
||||||
|
|
||||||
|
if (JavaUtil.isNullOrBlank(certificates)) certificates = "";
|
||||||
|
if (JavaUtil.isNullOrBlank(earlyDataHeaderName)) earlyDataHeaderName = "";
|
||||||
|
if (JavaUtil.isNullOrBlank(utlsFingerprint)) utlsFingerprint = "";
|
||||||
|
|
||||||
|
if (wsMaxEarlyData == null) wsMaxEarlyData = 0;
|
||||||
|
if (allowInsecure == null) allowInsecure = false;
|
||||||
|
if (packetEncoding == null) packetEncoding = 0;
|
||||||
|
|
||||||
|
if (realityPubKey == null) realityPubKey = "";
|
||||||
|
if (realityShortId == null) realityShortId = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeInt(0);
|
||||||
|
super.serialize(output);
|
||||||
|
|
||||||
|
output.writeString(uuid);
|
||||||
|
output.writeString(encryption);
|
||||||
|
if (this instanceof VMessBean) {
|
||||||
|
output.writeInt(((VMessBean) this).alterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.writeString(type);
|
||||||
|
switch (type) {
|
||||||
|
case "tcp":
|
||||||
|
case "quic": {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "ws": {
|
||||||
|
output.writeString(host);
|
||||||
|
output.writeString(path);
|
||||||
|
output.writeInt(wsMaxEarlyData);
|
||||||
|
output.writeString(earlyDataHeaderName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "http": {
|
||||||
|
output.writeString(host);
|
||||||
|
output.writeString(path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "grpc": {
|
||||||
|
output.writeString(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.writeString(security);
|
||||||
|
if ("tls".equals(security)) {
|
||||||
|
output.writeString(sni);
|
||||||
|
output.writeString(alpn);
|
||||||
|
output.writeString(certificates);
|
||||||
|
output.writeBoolean(allowInsecure);
|
||||||
|
output.writeString(utlsFingerprint);
|
||||||
|
output.writeString(realityPubKey);
|
||||||
|
output.writeString(realityShortId);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.writeInt(packetEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
super.deserialize(input);
|
||||||
|
uuid = input.readString();
|
||||||
|
encryption = input.readString();
|
||||||
|
if (this instanceof VMessBean) {
|
||||||
|
((VMessBean) this).alterId = input.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
type = input.readString();
|
||||||
|
switch (type) {
|
||||||
|
case "tcp":
|
||||||
|
case "quic": {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "ws": {
|
||||||
|
host = input.readString();
|
||||||
|
path = input.readString();
|
||||||
|
wsMaxEarlyData = input.readInt();
|
||||||
|
earlyDataHeaderName = input.readString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "http": {
|
||||||
|
host = input.readString();
|
||||||
|
path = input.readString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "grpc": {
|
||||||
|
path = input.readString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
security = input.readString();
|
||||||
|
if ("tls".equals(security)) {
|
||||||
|
sni = input.readString();
|
||||||
|
alpn = input.readString();
|
||||||
|
certificates = input.readString();
|
||||||
|
allowInsecure = input.readBoolean();
|
||||||
|
utlsFingerprint = input.readString();
|
||||||
|
realityPubKey = input.readString();
|
||||||
|
realityShortId = input.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
packetEncoding = input.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyFeatureSettings(AbstractBean other) {
|
||||||
|
if (!(other instanceof StandardV2RayBean)) return;
|
||||||
|
StandardV2RayBean bean = ((StandardV2RayBean) other);
|
||||||
|
bean.allowInsecure = allowInsecure;
|
||||||
|
bean.utlsFingerprint = utlsFingerprint;
|
||||||
|
bean.realityPubKey = realityPubKey;
|
||||||
|
bean.realityShortId = realityShortId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVLESS() {
|
||||||
|
if (this instanceof VMessBean) {
|
||||||
|
return ((VMessBean) this).alterId == -1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
608
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt
Normal file
608
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.v2ray
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import io.nekohasekai.sagernet.fmt.http.HttpBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
|
||||||
|
import moe.matsuri.nb4a.SingBoxOptions.*
|
||||||
|
import io.nekohasekai.sagernet.ktx.*
|
||||||
|
import moe.matsuri.nb4a.utils.NGUtil
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
fun StandardV2RayBean.isTLS(): Boolean {
|
||||||
|
return security == "tls"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun StandardV2RayBean.setTLS(boolean: Boolean) {
|
||||||
|
security = if (boolean) "tls" else ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseV2Ray(link: String): StandardV2RayBean {
|
||||||
|
// Try parse stupid formats first
|
||||||
|
|
||||||
|
if (!link.contains("?")) {
|
||||||
|
try {
|
||||||
|
return parseV2RayN(link)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logs.i("try v2rayN: " + e.readableMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return tryResolveVmess4Kitsunebi(link)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logs.i("try Kitsunebi: " + e.readableMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// "std" format
|
||||||
|
|
||||||
|
val bean = VMessBean().apply { if (link.startsWith("vless://")) alterId = -1 }
|
||||||
|
val url = link.replace("vmess://", "https://").replace("vless://", "https://").toHttpUrl()
|
||||||
|
|
||||||
|
if (url.password.isNotBlank()) {
|
||||||
|
// https://github.com/v2fly/v2fly-github-io/issues/26 (rarely use)
|
||||||
|
bean.serverAddress = url.host
|
||||||
|
bean.serverPort = url.port
|
||||||
|
bean.name = url.fragment
|
||||||
|
|
||||||
|
var protocol = url.username
|
||||||
|
bean.type = protocol
|
||||||
|
bean.alterId = url.password.substringAfterLast('-').toInt()
|
||||||
|
bean.uuid = url.password.substringBeforeLast('-')
|
||||||
|
|
||||||
|
if (protocol.endsWith("+tls")) {
|
||||||
|
bean.security = "tls"
|
||||||
|
protocol = protocol.substring(0, protocol.length - 4)
|
||||||
|
|
||||||
|
url.queryParameter("tlsServerName")?.let {
|
||||||
|
if (it.isNotBlank()) {
|
||||||
|
bean.sni = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (protocol) {
|
||||||
|
// "tcp" -> {
|
||||||
|
// url.queryParameter("type")?.let { type ->
|
||||||
|
// if (type == "http") {
|
||||||
|
// bean.headerType = "http"
|
||||||
|
// url.queryParameter("host")?.let {
|
||||||
|
// bean.host = it
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
"http" -> {
|
||||||
|
url.queryParameter("path")?.let {
|
||||||
|
bean.path = it
|
||||||
|
}
|
||||||
|
url.queryParameter("host")?.let {
|
||||||
|
bean.host = it.split("|").joinToString(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"ws" -> {
|
||||||
|
url.queryParameter("path")?.let {
|
||||||
|
bean.path = it
|
||||||
|
}
|
||||||
|
url.queryParameter("host")?.let {
|
||||||
|
bean.host = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"grpc" -> {
|
||||||
|
url.queryParameter("serviceName")?.let {
|
||||||
|
bean.path = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// also vless format
|
||||||
|
bean.parseDuckSoft(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bean
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/XTLS/Xray-core/issues/91
|
||||||
|
// NO allowInsecure
|
||||||
|
fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) {
|
||||||
|
serverAddress = url.host
|
||||||
|
serverPort = url.port
|
||||||
|
name = url.fragment
|
||||||
|
|
||||||
|
if (this is TrojanBean) {
|
||||||
|
password = url.username
|
||||||
|
} else {
|
||||||
|
uuid = url.username
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathSegments.size > 1 || url.pathSegments[0].isNotBlank()) {
|
||||||
|
path = url.pathSegments.joinToString("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
type = url.queryParameter("type") ?: "tcp"
|
||||||
|
security = url.queryParameter("security")
|
||||||
|
if (security == null) {
|
||||||
|
security = if (this is TrojanBean) {
|
||||||
|
"tls"
|
||||||
|
} else {
|
||||||
|
"none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (security) {
|
||||||
|
"tls" -> {
|
||||||
|
url.queryParameter("sni")?.let {
|
||||||
|
sni = it
|
||||||
|
}
|
||||||
|
url.queryParameter("alpn")?.let {
|
||||||
|
alpn = it
|
||||||
|
}
|
||||||
|
url.queryParameter("cert")?.let {
|
||||||
|
certificates = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (type) {
|
||||||
|
"http" -> {
|
||||||
|
url.queryParameter("host")?.let {
|
||||||
|
host = it
|
||||||
|
}
|
||||||
|
url.queryParameter("path")?.let {
|
||||||
|
path = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"ws" -> {
|
||||||
|
url.queryParameter("host")?.let {
|
||||||
|
host = it
|
||||||
|
}
|
||||||
|
url.queryParameter("path")?.let {
|
||||||
|
path = it
|
||||||
|
}
|
||||||
|
url.queryParameter("ed")?.let { ed ->
|
||||||
|
wsMaxEarlyData = ed.toInt()
|
||||||
|
|
||||||
|
url.queryParameter("eh")?.let {
|
||||||
|
earlyDataHeaderName = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"grpc" -> {
|
||||||
|
url.queryParameter("serviceName")?.let {
|
||||||
|
path = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
url.queryParameter("packetEncoding")?.let {
|
||||||
|
when (it) {
|
||||||
|
"packet" -> packetEncoding = 1
|
||||||
|
"xudp" -> packetEncoding = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
url.queryParameter("flow")?.let {
|
||||||
|
if (isVLESS && it.contains("vision")) {
|
||||||
|
encryption = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不确定是谁的格式
|
||||||
|
private fun tryResolveVmess4Kitsunebi(server: String): VMessBean {
|
||||||
|
// vmess://YXV0bzo1YWY1ZDBlYy02ZWEwLTNjNDMtOTNkYi1jYTMwMDg1MDNiZGJAMTgzLjIzMi41Ni4xNjE6MTIwMg
|
||||||
|
// ?remarks=*%F0%9F%87%AF%F0%9F%87%B5JP%20-355%20TG@moon365free&obfsParam=%7B%22Host%22:%22183.232.56.161%22%7D&path=/v2ray&obfs=websocket&alterId=0
|
||||||
|
|
||||||
|
var result = server.replace("vmess://", "")
|
||||||
|
val indexSplit = result.indexOf("?")
|
||||||
|
if (indexSplit > 0) {
|
||||||
|
result = result.substring(0, indexSplit)
|
||||||
|
}
|
||||||
|
result = NGUtil.decode(result)
|
||||||
|
|
||||||
|
val arr1 = result.split('@')
|
||||||
|
if (arr1.count() != 2) {
|
||||||
|
throw IllegalStateException("invalid kitsunebi format")
|
||||||
|
}
|
||||||
|
val arr21 = arr1[0].split(':')
|
||||||
|
val arr22 = arr1[1].split(':')
|
||||||
|
if (arr21.count() != 2) {
|
||||||
|
throw IllegalStateException("invalid kitsunebi format")
|
||||||
|
}
|
||||||
|
|
||||||
|
return VMessBean().apply {
|
||||||
|
serverAddress = arr22[0]
|
||||||
|
serverPort = NGUtil.parseInt(arr22[1])
|
||||||
|
uuid = arr21[1]
|
||||||
|
encryption = arr21[0]
|
||||||
|
if (indexSplit < 0) return@apply
|
||||||
|
|
||||||
|
val url = ("https://localhost/path?" + server.substringAfter("?")).toHttpUrl()
|
||||||
|
url.queryParameter("remarks")?.apply { name = this }
|
||||||
|
url.queryParameter("alterId")?.apply { alterId = this.toInt() }
|
||||||
|
url.queryParameter("path")?.apply { path = this }
|
||||||
|
url.queryParameter("tls")?.apply { security = "tls" }
|
||||||
|
url.queryParameter("allowInsecure")
|
||||||
|
?.apply { if (this == "1" || this == "true") allowInsecure = true }
|
||||||
|
url.queryParameter("obfs")?.apply {
|
||||||
|
type = this.replace("websocket", "ws").replace("none", "tcp")
|
||||||
|
if (type == "ws") {
|
||||||
|
url.queryParameter("obfsParam")?.apply {
|
||||||
|
if (this.startsWith("{")) {
|
||||||
|
host = JSONObject(this).getStr("Host")
|
||||||
|
} else if (security == "tls") {
|
||||||
|
sni = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SagerNet's
|
||||||
|
// Do not support some format and then throw exception
|
||||||
|
fun parseV2RayN(link: String): VMessBean {
|
||||||
|
val result = link.substringAfter("vmess://").decodeBase64UrlSafe()
|
||||||
|
if (result.contains("= vmess")) {
|
||||||
|
return parseCsvVMess(result)
|
||||||
|
}
|
||||||
|
val bean = VMessBean()
|
||||||
|
val json = JSONObject(result)
|
||||||
|
|
||||||
|
bean.serverAddress = json.getStr("add") ?: ""
|
||||||
|
bean.serverPort = json.getIntNya("port") ?: 1080
|
||||||
|
bean.encryption = json.getStr("scy") ?: ""
|
||||||
|
bean.uuid = json.getStr("id") ?: ""
|
||||||
|
bean.alterId = json.getIntNya("aid") ?: 0
|
||||||
|
bean.type = json.getStr("net") ?: ""
|
||||||
|
bean.host = json.getStr("host") ?: ""
|
||||||
|
bean.path = json.getStr("path") ?: ""
|
||||||
|
|
||||||
|
when (bean.type) {
|
||||||
|
"grpc" -> {
|
||||||
|
bean.path = bean.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.name = json.getStr("ps") ?: ""
|
||||||
|
bean.sni = json.getStr("sni") ?: bean.host
|
||||||
|
bean.security = json.getStr("tls")
|
||||||
|
|
||||||
|
if (json.optInt("v", 2) < 2) {
|
||||||
|
when (bean.type) {
|
||||||
|
"ws" -> {
|
||||||
|
var path = ""
|
||||||
|
var host = ""
|
||||||
|
val lstParameter = bean.host.split(";")
|
||||||
|
if (lstParameter.isNotEmpty()) {
|
||||||
|
path = lstParameter[0].trim()
|
||||||
|
}
|
||||||
|
if (lstParameter.size > 1) {
|
||||||
|
path = lstParameter[0].trim()
|
||||||
|
host = lstParameter[1].trim()
|
||||||
|
}
|
||||||
|
bean.path = path
|
||||||
|
bean.host = host
|
||||||
|
}
|
||||||
|
"h2" -> {
|
||||||
|
var path = ""
|
||||||
|
var host = ""
|
||||||
|
val lstParameter = bean.host.split(";")
|
||||||
|
if (lstParameter.isNotEmpty()) {
|
||||||
|
path = lstParameter[0].trim()
|
||||||
|
}
|
||||||
|
if (lstParameter.size > 1) {
|
||||||
|
path = lstParameter[0].trim()
|
||||||
|
host = lstParameter[1].trim()
|
||||||
|
}
|
||||||
|
bean.path = path
|
||||||
|
bean.host = host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bean
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseCsvVMess(csv: String): VMessBean {
|
||||||
|
|
||||||
|
val args = csv.split(",")
|
||||||
|
|
||||||
|
val bean = VMessBean()
|
||||||
|
|
||||||
|
bean.serverAddress = args[1]
|
||||||
|
bean.serverPort = args[2].toInt()
|
||||||
|
bean.encryption = args[3]
|
||||||
|
bean.uuid = args[4].replace("\"", "")
|
||||||
|
|
||||||
|
args.subList(5, args.size).forEach {
|
||||||
|
|
||||||
|
when {
|
||||||
|
it == "over-tls=true" -> bean.security = "tls"
|
||||||
|
it.startsWith("tls-host=") -> bean.host = it.substringAfter("=")
|
||||||
|
it.startsWith("obfs=") -> bean.type = it.substringAfter("=")
|
||||||
|
it.startsWith("obfs-path=") || it.contains("Host:") -> {
|
||||||
|
runCatching {
|
||||||
|
bean.path = it.substringAfter("obfs-path=\"").substringBefore("\"obfs")
|
||||||
|
}
|
||||||
|
runCatching {
|
||||||
|
bean.host = it.substringAfter("Host:").substringBefore("[")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return bean
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class VmessQRCode(
|
||||||
|
var v: String = "",
|
||||||
|
var ps: String = "",
|
||||||
|
var add: String = "",
|
||||||
|
var port: String = "",
|
||||||
|
var id: String = "",
|
||||||
|
var aid: String = "0",
|
||||||
|
var scy: String = "",
|
||||||
|
var net: String = "",
|
||||||
|
var type: String = "",
|
||||||
|
var host: String = "",
|
||||||
|
var path: String = "",
|
||||||
|
var tls: String = "",
|
||||||
|
var sni: String = "",
|
||||||
|
var alpn: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
fun VMessBean.toV2rayN(): String {
|
||||||
|
if (isVLESS) return toUri(true)
|
||||||
|
return "vmess://" + VmessQRCode().apply {
|
||||||
|
v = "2"
|
||||||
|
ps = this@toV2rayN.name
|
||||||
|
add = this@toV2rayN.serverAddress
|
||||||
|
port = this@toV2rayN.serverPort.toString()
|
||||||
|
id = this@toV2rayN.uuid
|
||||||
|
aid = this@toV2rayN.alterId.toString()
|
||||||
|
net = this@toV2rayN.type
|
||||||
|
host = this@toV2rayN.host
|
||||||
|
path = this@toV2rayN.path
|
||||||
|
|
||||||
|
when (net) {
|
||||||
|
"grpc" -> {
|
||||||
|
path = this@toV2rayN.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tls = if (this@toV2rayN.security == "tls") "tls" else ""
|
||||||
|
sni = this@toV2rayN.sni
|
||||||
|
scy = this@toV2rayN.encryption
|
||||||
|
}.let {
|
||||||
|
NGUtil.encode(Gson().toJson(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun StandardV2RayBean.toUri(standard: Boolean = true): String {
|
||||||
|
if (this is VMessBean && alterId > 0) return toV2rayN()
|
||||||
|
|
||||||
|
val builder = linkBuilder()
|
||||||
|
.username(if (this is TrojanBean) password else uuid)
|
||||||
|
.host(serverAddress)
|
||||||
|
.port(serverPort)
|
||||||
|
.addQueryParameter("type", type)
|
||||||
|
if (this !is TrojanBean) builder.addQueryParameter("encryption", encryption)
|
||||||
|
|
||||||
|
when (type) {
|
||||||
|
"tcp" -> {}
|
||||||
|
"ws", "http" -> {
|
||||||
|
if (host.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("host", host)
|
||||||
|
}
|
||||||
|
if (path.isNotBlank()) {
|
||||||
|
if (standard) {
|
||||||
|
builder.addQueryParameter("path", path)
|
||||||
|
} else {
|
||||||
|
builder.encodedPath(path.pathSafe())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == "ws") {
|
||||||
|
if (wsMaxEarlyData > 0) {
|
||||||
|
builder.addQueryParameter("ed", "$wsMaxEarlyData")
|
||||||
|
if (earlyDataHeaderName.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("eh", earlyDataHeaderName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type == "http" && !isTLS()) {
|
||||||
|
return "" // no fmt?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"grpc" -> {
|
||||||
|
if (path.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("serviceName", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (security.isNotBlank() && security != "none") {
|
||||||
|
builder.addQueryParameter("security", security)
|
||||||
|
when (security) {
|
||||||
|
"tls" -> {
|
||||||
|
if (sni.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("sni", sni)
|
||||||
|
}
|
||||||
|
if (alpn.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("alpn", alpn)
|
||||||
|
}
|
||||||
|
if (certificates.isNotBlank()) {
|
||||||
|
builder.addQueryParameter("cert", certificates)
|
||||||
|
}
|
||||||
|
if (allowInsecure) builder.addQueryParameter("allowInsecure", "1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (packetEncoding) {
|
||||||
|
1 -> {
|
||||||
|
builder.addQueryParameter("packetEncoding", "packet")
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
builder.addQueryParameter("packetEncoding", "xudp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.isNotBlank()) {
|
||||||
|
builder.encodedFragment(name.urlSafe())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO vless flow: bean.encryption != "auto"
|
||||||
|
|
||||||
|
return builder.toLink(if (isVLESS) "vless" else "vmess")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildSingBoxOutboundStreamSettings(bean: StandardV2RayBean): V2RayTransportOptions? {
|
||||||
|
when (bean.type) {
|
||||||
|
"tcp" -> {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
"ws" -> {
|
||||||
|
return V2RayTransportOptions_WebsocketOptions().apply {
|
||||||
|
type = "ws"
|
||||||
|
headers = mutableMapOf()
|
||||||
|
|
||||||
|
if (bean.host.isNotBlank()) {
|
||||||
|
headers["Host"] = bean.host
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bean.path.contains("?ed=")) {
|
||||||
|
path = bean.path.substringBefore("?ed=")
|
||||||
|
max_early_data = bean.path.substringAfter("?ed=").toIntOrNull() ?: 2048
|
||||||
|
early_data_header_name = "Sec-WebSocket-Protocol"
|
||||||
|
} else {
|
||||||
|
path = bean.path.takeIf { it.isNotBlank() } ?: "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bean.wsMaxEarlyData > 0) {
|
||||||
|
max_early_data = bean.wsMaxEarlyData
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bean.earlyDataHeaderName.isNotBlank()) {
|
||||||
|
early_data_header_name = bean.earlyDataHeaderName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"http" -> {
|
||||||
|
return V2RayTransportOptions_HTTPOptions().apply {
|
||||||
|
type = "http"
|
||||||
|
if (bean.host.isNotBlank()) {
|
||||||
|
host = bean.host.split(",")
|
||||||
|
}
|
||||||
|
path = bean.path.takeIf { it.isNotBlank() } ?: "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"quic" -> {
|
||||||
|
return V2RayTransportOptions().apply {
|
||||||
|
type = "quic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"grpc" -> {
|
||||||
|
return V2RayTransportOptions_GRPCOptions().apply {
|
||||||
|
type = "grpc"
|
||||||
|
service_name = bean.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (needKeepAliveInterval) {
|
||||||
|
// sockopt = StreamSettingsObject.SockoptObject().apply {
|
||||||
|
// tcpKeepAliveInterval = keepAliveInterval
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildSingBoxOutboundTLS(bean: StandardV2RayBean): OutboundTLSOptions? {
|
||||||
|
if (bean.security != "tls") return null
|
||||||
|
return OutboundTLSOptions().apply {
|
||||||
|
enabled = true
|
||||||
|
insecure = bean.allowInsecure
|
||||||
|
if (bean.sni.isNotBlank()) server_name = bean.sni
|
||||||
|
if (bean.alpn.isNotBlank()) alpn = bean.alpn.split("\n")
|
||||||
|
if (bean.certificates.isNotBlank()) certificate = bean.certificates
|
||||||
|
if (bean.utlsFingerprint.isNotBlank()) {
|
||||||
|
utls = OutboundUTLSOptions().apply {
|
||||||
|
enabled = true
|
||||||
|
fingerprint = bean.utlsFingerprint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bean.realityPubKey.isNotBlank() && bean.realityShortId.isNotBlank()) {
|
||||||
|
reality = OutboundRealityOptions().apply {
|
||||||
|
enabled = true
|
||||||
|
public_key = bean.realityPubKey
|
||||||
|
short_id = bean.realityShortId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildSingBoxOutboundStandardV2RayBean(bean: StandardV2RayBean): Outbound {
|
||||||
|
when (bean) {
|
||||||
|
is HttpBean -> {
|
||||||
|
return Outbound_HTTPOptions().apply {
|
||||||
|
type = "http"
|
||||||
|
server = bean.serverAddress
|
||||||
|
server_port = bean.serverPort
|
||||||
|
username = bean.username
|
||||||
|
password = bean.password
|
||||||
|
tls = buildSingBoxOutboundTLS(bean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is VMessBean -> {
|
||||||
|
if (bean.isVLESS) return Outbound_VLESSOptions().apply {
|
||||||
|
type = "vless"
|
||||||
|
server = bean.serverAddress
|
||||||
|
server_port = bean.serverPort
|
||||||
|
uuid = bean.uuid
|
||||||
|
if (bean.encryption.isNotBlank() && bean.encryption != "auto") {
|
||||||
|
flow = bean.encryption
|
||||||
|
}
|
||||||
|
when (bean.packetEncoding) {
|
||||||
|
0 -> packet_encoding = ""
|
||||||
|
1 -> packet_encoding = "packetaddr"
|
||||||
|
2 -> packet_encoding = "xudp"
|
||||||
|
}
|
||||||
|
tls = buildSingBoxOutboundTLS(bean)
|
||||||
|
transport = buildSingBoxOutboundStreamSettings(bean)
|
||||||
|
}
|
||||||
|
return Outbound_VMessOptions().apply {
|
||||||
|
type = "vmess"
|
||||||
|
server = bean.serverAddress
|
||||||
|
server_port = bean.serverPort
|
||||||
|
uuid = bean.uuid
|
||||||
|
alter_id = bean.alterId
|
||||||
|
security = bean.encryption.takeIf { it.isNotBlank() } ?: "auto"
|
||||||
|
when (bean.packetEncoding) {
|
||||||
|
0 -> packet_encoding = ""
|
||||||
|
1 -> packet_encoding = "packetaddr"
|
||||||
|
2 -> packet_encoding = "xudp"
|
||||||
|
}
|
||||||
|
tls = buildSingBoxOutboundTLS(bean)
|
||||||
|
transport = buildSingBoxOutboundStreamSettings(bean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TrojanBean -> {
|
||||||
|
return Outbound_TrojanOptions().apply {
|
||||||
|
type = "trojan"
|
||||||
|
server = bean.serverAddress
|
||||||
|
server_port = bean.serverPort
|
||||||
|
password = bean.password
|
||||||
|
tls = buildSingBoxOutboundTLS(bean)
|
||||||
|
transport = buildSingBoxOutboundStreamSettings(bean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> throw IllegalStateException("can't reach")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.v2ray;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||||
|
import moe.matsuri.nb4a.utils.JavaUtil;
|
||||||
|
|
||||||
|
public class VMessBean extends StandardV2RayBean {
|
||||||
|
|
||||||
|
public Integer alterId; // alterID == -1 --> VLESS
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
|
||||||
|
alterId = alterId != null ? alterId : 0;
|
||||||
|
encryption = JavaUtil.isNotBlank(encryption) ? encryption : "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public VMessBean clone() {
|
||||||
|
return KryoConverters.deserialize(new VMessBean(), KryoConverters.serialize(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<VMessBean> CREATOR = new CREATOR<VMessBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public VMessBean newInstance() {
|
||||||
|
return new VMessBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VMessBean[] newArray(int size) {
|
||||||
|
return new VMessBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.wireguard;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferInput;
|
||||||
|
import com.esotericsoftware.kryo.io.ByteBufferOutput;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean;
|
||||||
|
import io.nekohasekai.sagernet.fmt.KryoConverters;
|
||||||
|
|
||||||
|
public class WireGuardBean extends AbstractBean {
|
||||||
|
|
||||||
|
public String localAddress;
|
||||||
|
public String privateKey;
|
||||||
|
public String peerPublicKey;
|
||||||
|
public String peerPreSharedKey;
|
||||||
|
public Integer mtu;
|
||||||
|
public String reserved;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeDefaultValues() {
|
||||||
|
super.initializeDefaultValues();
|
||||||
|
if (localAddress == null) localAddress = "";
|
||||||
|
if (privateKey == null) privateKey = "";
|
||||||
|
if (peerPublicKey == null) peerPublicKey = "";
|
||||||
|
if (peerPreSharedKey == null) peerPreSharedKey = "";
|
||||||
|
if (mtu == null) mtu = 1420;
|
||||||
|
if (reserved == null) reserved = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ByteBufferOutput output) {
|
||||||
|
output.writeInt(2);
|
||||||
|
super.serialize(output);
|
||||||
|
output.writeString(localAddress);
|
||||||
|
output.writeString(privateKey);
|
||||||
|
output.writeString(peerPublicKey);
|
||||||
|
output.writeString(peerPreSharedKey);
|
||||||
|
output.writeInt(mtu);
|
||||||
|
output.writeString(reserved);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deserialize(ByteBufferInput input) {
|
||||||
|
int version = input.readInt();
|
||||||
|
super.deserialize(input);
|
||||||
|
localAddress = input.readString();
|
||||||
|
privateKey = input.readString();
|
||||||
|
peerPublicKey = input.readString();
|
||||||
|
peerPreSharedKey = input.readString();
|
||||||
|
mtu = input.readInt();
|
||||||
|
reserved = input.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public WireGuardBean clone() {
|
||||||
|
return KryoConverters.deserialize(new WireGuardBean(), KryoConverters.serialize(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<WireGuardBean> CREATOR = new CREATOR<WireGuardBean>() {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public WireGuardBean newInstance() {
|
||||||
|
return new WireGuardBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WireGuardBean[] newArray(int size) {
|
||||||
|
return new WireGuardBean[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package io.nekohasekai.sagernet.fmt.wireguard
|
||||||
|
|
||||||
|
import moe.matsuri.nb4a.SingBoxOptions
|
||||||
|
|
||||||
|
fun buildSingBoxOutboundWireguardBean(bean: WireGuardBean): SingBoxOptions.Outbound_WireGuardOptions_Fix {
|
||||||
|
return SingBoxOptions.Outbound_WireGuardOptions_Fix().apply {
|
||||||
|
type = "wireguard"
|
||||||
|
server = bean.serverAddress
|
||||||
|
server_port = bean.serverPort
|
||||||
|
local_address = bean.localAddress.split("\n")
|
||||||
|
private_key = bean.privateKey
|
||||||
|
peer_public_key = bean.peerPublicKey
|
||||||
|
pre_shared_key = bean.peerPreSharedKey
|
||||||
|
mtu = bean.mtu
|
||||||
|
if (bean.reserved.isNotBlank()) reserved = bean.reserved
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,102 @@
|
|||||||
|
package io.nekohasekai.sagernet.group
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import io.nekohasekai.sagernet.R
|
||||||
|
import io.nekohasekai.sagernet.database.GroupManager
|
||||||
|
import io.nekohasekai.sagernet.database.ProxyGroup
|
||||||
|
import io.nekohasekai.sagernet.ktx.onMainDispatcher
|
||||||
|
import io.nekohasekai.sagernet.ktx.runOnMainDispatcher
|
||||||
|
import io.nekohasekai.sagernet.ui.ThemedActivity
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
class GroupInterfaceAdapter(val context: ThemedActivity) : GroupManager.Interface {
|
||||||
|
|
||||||
|
override suspend fun confirm(message: String): Boolean {
|
||||||
|
return suspendCoroutine {
|
||||||
|
runOnMainDispatcher {
|
||||||
|
MaterialAlertDialogBuilder(context).setTitle(R.string.confirm)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(R.string.yes) { _, _ -> it.resume(true) }
|
||||||
|
.setNegativeButton(R.string.no) { _, _ -> it.resume(false) }
|
||||||
|
.setOnCancelListener { _ -> it.resume(false) }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onUpdateSuccess(
|
||||||
|
group: ProxyGroup,
|
||||||
|
changed: Int,
|
||||||
|
added: List<String>,
|
||||||
|
updated: Map<String, String>,
|
||||||
|
deleted: List<String>,
|
||||||
|
duplicate: List<String>,
|
||||||
|
byUser: Boolean
|
||||||
|
) {
|
||||||
|
if (changed == 0 && duplicate.isEmpty()) {
|
||||||
|
if (byUser) context.snackbar(
|
||||||
|
context.getString(
|
||||||
|
R.string.group_no_difference, group.displayName()
|
||||||
|
)
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
context.snackbar(context.getString(R.string.group_updated, group.name, changed)).show()
|
||||||
|
|
||||||
|
var status = ""
|
||||||
|
if (added.isNotEmpty()) {
|
||||||
|
status += context.getString(
|
||||||
|
R.string.group_added, added.joinToString("\n", postfix = "\n\n")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (updated.isNotEmpty()) {
|
||||||
|
status += context.getString(R.string.group_changed,
|
||||||
|
updated.map { it }.joinToString("\n", postfix = "\n\n") {
|
||||||
|
if (it.key == it.value) it.key else "${it.key} => ${it.value}"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (deleted.isNotEmpty()) {
|
||||||
|
status += context.getString(
|
||||||
|
R.string.group_deleted, deleted.joinToString("\n", postfix = "\n\n")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (duplicate.isNotEmpty()) {
|
||||||
|
status += context.getString(
|
||||||
|
R.string.group_duplicate, duplicate.joinToString("\n", postfix = "\n\n")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMainDispatcher {
|
||||||
|
delay(1000L)
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(context).setTitle(
|
||||||
|
context.getString(
|
||||||
|
R.string.group_diff, group.displayName()
|
||||||
|
)
|
||||||
|
).setMessage(status.trim()).setPositiveButton(android.R.string.ok, null).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onUpdateFailure(group: ProxyGroup, message: String) {
|
||||||
|
onMainDispatcher {
|
||||||
|
context.snackbar(message).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun alert(message: String) {
|
||||||
|
return suspendCoroutine {
|
||||||
|
runOnMainDispatcher {
|
||||||
|
MaterialAlertDialogBuilder(context).setTitle(R.string.ooc_warning)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> it.resume(Unit) }
|
||||||
|
.setOnCancelListener { _ -> it.resume(Unit) }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
171
app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt
Normal file
171
app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
package io.nekohasekai.sagernet.group
|
||||||
|
|
||||||
|
import io.nekohasekai.sagernet.*
|
||||||
|
import io.nekohasekai.sagernet.database.DataStore
|
||||||
|
import io.nekohasekai.sagernet.database.GroupManager
|
||||||
|
import io.nekohasekai.sagernet.database.ProxyGroup
|
||||||
|
import io.nekohasekai.sagernet.database.SubscriptionBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.AbstractBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.http.HttpBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.naive.NaiveBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean
|
||||||
|
import io.nekohasekai.sagernet.fmt.v2ray.isTLS
|
||||||
|
import io.nekohasekai.sagernet.ktx.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||||
|
abstract class GroupUpdater {
|
||||||
|
|
||||||
|
abstract suspend fun doUpdate(
|
||||||
|
proxyGroup: ProxyGroup,
|
||||||
|
subscription: SubscriptionBean,
|
||||||
|
userInterface: GroupManager.Interface?,
|
||||||
|
byUser: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Progress(
|
||||||
|
var max: Int
|
||||||
|
) {
|
||||||
|
var progress by AtomicInteger()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected suspend fun forceResolve(
|
||||||
|
profiles: List<AbstractBean>, groupId: Long?
|
||||||
|
) {
|
||||||
|
val ipv6Mode = DataStore.ipv6Mode
|
||||||
|
val lookupPool = newFixedThreadPoolContext(5, "DNS Lookup")
|
||||||
|
val lookupJobs = mutableListOf<Job>()
|
||||||
|
val progress = Progress(profiles.size)
|
||||||
|
if (groupId != null) {
|
||||||
|
GroupUpdater.progress[groupId] = progress
|
||||||
|
GroupManager.postReload(groupId)
|
||||||
|
}
|
||||||
|
val ipv6First = ipv6Mode >= IPv6Mode.PREFER
|
||||||
|
|
||||||
|
for (profile in profiles) {
|
||||||
|
when (profile) {
|
||||||
|
// SNI rewrite unsupported
|
||||||
|
is NaiveBean -> continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.serverAddress.isIpAddress()) continue
|
||||||
|
|
||||||
|
lookupJobs.add(GlobalScope.launch(lookupPool) {
|
||||||
|
try {
|
||||||
|
val results = if (
|
||||||
|
SagerNet.underlyingNetwork != null &&
|
||||||
|
DataStore.enableFakeDns &&
|
||||||
|
DataStore.serviceState.started &&
|
||||||
|
DataStore.serviceMode == Key.MODE_VPN
|
||||||
|
) {
|
||||||
|
// FakeDNS
|
||||||
|
SagerNet.underlyingNetwork!!
|
||||||
|
.getAllByName(profile.serverAddress)
|
||||||
|
.filterNotNull()
|
||||||
|
} else {
|
||||||
|
// System DNS is enough (when VPN connected, it uses v2ray-core)
|
||||||
|
InetAddress.getAllByName(profile.serverAddress).filterNotNull()
|
||||||
|
}
|
||||||
|
if (results.isEmpty()) error("empty response")
|
||||||
|
rewriteAddress(profile, results, ipv6First)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logs.d("Lookup ${profile.serverAddress} failed: ${e.readableMessage}", e)
|
||||||
|
}
|
||||||
|
if (groupId != null) {
|
||||||
|
progress.progress++
|
||||||
|
GroupManager.postReload(groupId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
lookupJobs.joinAll()
|
||||||
|
lookupPool.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun rewriteAddress(
|
||||||
|
bean: AbstractBean, addresses: List<InetAddress>, ipv6First: Boolean
|
||||||
|
) {
|
||||||
|
val address = addresses.sortedBy { (it is Inet4Address) xor ipv6First }[0].hostAddress
|
||||||
|
|
||||||
|
with(bean) {
|
||||||
|
when (this) {
|
||||||
|
is HttpBean -> {
|
||||||
|
if (isTLS() && sni.isBlank()) sni = bean.serverAddress
|
||||||
|
}
|
||||||
|
is StandardV2RayBean -> {
|
||||||
|
when (security) {
|
||||||
|
"tls" -> if (sni.isBlank()) sni = bean.serverAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TrojanBean -> {
|
||||||
|
if (sni.isBlank()) sni = bean.serverAddress
|
||||||
|
}
|
||||||
|
is TrojanGoBean -> {
|
||||||
|
if (sni.isBlank()) sni = bean.serverAddress
|
||||||
|
}
|
||||||
|
is HysteriaBean -> {
|
||||||
|
if (sni.isBlank()) sni = bean.serverAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bean.serverAddress = address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val updating = Collections.synchronizedSet<Long>(mutableSetOf())
|
||||||
|
val progress = Collections.synchronizedMap<Long, Progress>(mutableMapOf())
|
||||||
|
|
||||||
|
fun startUpdate(proxyGroup: ProxyGroup, byUser: Boolean) {
|
||||||
|
runOnDefaultDispatcher {
|
||||||
|
executeUpdate(proxyGroup, byUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun executeUpdate(proxyGroup: ProxyGroup, byUser: Boolean): Boolean {
|
||||||
|
return coroutineScope {
|
||||||
|
if (!updating.add(proxyGroup.id)) cancel()
|
||||||
|
GroupManager.postReload(proxyGroup.id)
|
||||||
|
|
||||||
|
val subscription = proxyGroup.subscription!!
|
||||||
|
val connected = DataStore.serviceState.connected
|
||||||
|
val userInterface = GroupManager.userInterface
|
||||||
|
|
||||||
|
if (byUser && (subscription.link?.startsWith("http://") == true || subscription.updateWhenConnectedOnly) && !connected) {
|
||||||
|
if (userInterface == null || !userInterface.confirm(app.getString(R.string.update_subscription_warning))) {
|
||||||
|
finishUpdate(proxyGroup)
|
||||||
|
cancel()
|
||||||
|
return@coroutineScope true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
RawUpdater.doUpdate(proxyGroup, subscription, userInterface, byUser)
|
||||||
|
true
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logs.w(e)
|
||||||
|
userInterface?.onUpdateFailure(proxyGroup, e.readableMessage)
|
||||||
|
finishUpdate(proxyGroup)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun finishUpdate(proxyGroup: ProxyGroup) {
|
||||||
|
updating.remove(proxyGroup.id)
|
||||||
|
progress.remove(proxyGroup.id)
|
||||||
|
GroupManager.postUpdate(proxyGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user