diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt
index 10fc646d0..ea521798d 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt
@@ -41,6 +41,8 @@ object Key {
const val CONCURRENT_DIAL = "concurrentDial"
const val MIXED_PORT = "mixedPort"
+ const val MIXED_SECRET = "mixedSecret" // storage key for the generated inbound secret
+ const val MIXED_USERNAME = "neko" // username presented to the authed mixed inbound
const val ALLOW_ACCESS = "allowAccess"
const val SPEED_INTERVAL = "speedInterval"
const val SHOW_DIRECT_SPEED = "showDirectSpeed"
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
index 8af48d193..12596a288 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
@@ -236,6 +236,7 @@ class BaseService {
fun stopRunner(restart: Boolean = false, msg: String? = null) {
DataStore.baseService = null
DataStore.vpnService = null
+ DataStore.mixedInboundAuthed = false
if (data.state == State.Stopping) return
data.notification?.destroy()
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
index 9f16723de..d9aa300e2 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
@@ -46,6 +46,7 @@ abstract class BoxInstance(
protected open fun buildConfig() {
config = buildConfig(profile)
+ DataStore.mixedInboundAuthed = DataStore.mixedInboundNeedsAuth
}
protected open suspend fun loadConfig() {
@@ -219,4 +220,4 @@ abstract class BoxInstance(
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
index b34417e87..e50a49e38 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
@@ -1,6 +1,7 @@
package io.nekohasekai.sagernet.database
import android.os.Binder
+import android.os.Build
import androidx.preference.PreferenceDataStore
import io.nekohasekai.sagernet.CONNECTION_TEST_URL
import io.nekohasekai.sagernet.GroupType
@@ -27,6 +28,9 @@ object DataStore : OnPreferenceDataStoreChangeListener {
@Volatile
var serviceState = BaseService.State.Idle
+ @Volatile
+ var mixedInboundAuthed: Boolean = false
+
val configurationStore = RoomPreferenceDataStore(PublicDatabase.kvPairDao)
val profileCacheStore = RoomPreferenceDataStore(TempDatabase.profileCacheDao)
@@ -134,10 +138,27 @@ object DataStore : OnPreferenceDataStoreChangeListener {
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
+ val mixedSecret: String
+ @Synchronized get() {
+ var s = configurationStore.getString(Key.MIXED_SECRET)
+ if (s.isNullOrEmpty()) {
+ s = java.util.UUID.randomUUID().toString().replace("-", "")
+ configurationStore.putString(Key.MIXED_SECRET, s)
+ }
+ return s
+ }
+
var mixedPort: Int
get() = getLocalPort(Key.MIXED_PORT, 2080)
set(value) = saveLocalPort(Key.MIXED_PORT, value)
+ val mixedInboundNeedsAuth: Boolean
+ get() = serviceMode == Key.MODE_VPN &&
+ !(appendHttpProxy && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+
+ val mixedInboundUser: String get() = if (mixedInboundAuthed) Key.MIXED_USERNAME else ""
+ val mixedInboundPass: String get() = if (mixedInboundAuthed) mixedSecret else ""
+
fun initGlobal() {
if (configurationStore.getString(Key.MIXED_PORT) == null) {
mixedPort = mixedPort
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
index 9d815a194..b34ae0124 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
@@ -254,6 +254,12 @@ fun buildConfig(
domain_strategy = genDomainStrategy(DataStore.resolveDestination)
sniff = needSniff
sniff_override_destination = needSniffOverride
+ if (DataStore.mixedInboundNeedsAuth) {
+ users = listOf(User().also { u ->
+ u.username = Key.MIXED_USERNAME
+ u.password = DataStore.mixedSecret
+ })
+ }
})
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt
index 2eb029aef..069c3383a 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt
@@ -61,7 +61,11 @@ object RawUpdater : GroupUpdater() {
} else {
val response = Libcore.newHttpClient().apply {
- trySocks5(DataStore.mixedPort)
+ trySocks5(
+ DataStore.mixedPort,
+ DataStore.mixedInboundUser,
+ DataStore.mixedInboundPass
+ )
tryH3Direct()
when (DataStore.appTLSVersion) {
"1.3" -> restrictedTLS()
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt
index 1c9beffa2..af7a9d89d 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt
@@ -216,7 +216,11 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) {
try {
val client = Libcore.newHttpClient().apply {
modernTLS()
- trySocks5(DataStore.mixedPort)
+ trySocks5(
+ DataStore.mixedPort,
+ DataStore.mixedInboundUser,
+ DataStore.mixedInboundPass
+ )
}
val response = client.newRequest().apply {
if (checkPreview) {
@@ -276,4 +280,4 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) {
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt
index 267317f0c..22d7789ca 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt
@@ -282,7 +282,11 @@ class AssetsActivity : ThemedActivity() {
val client = Libcore.newHttpClient().apply {
modernTLS()
keepAlive()
- trySocks5(DataStore.mixedPort)
+ trySocks5(
+ DataStore.mixedPort,
+ DataStore.mixedInboundUser,
+ DataStore.mixedInboundPass
+ )
}
try {
@@ -345,7 +349,11 @@ class AssetsActivity : ThemedActivity() {
val client = Libcore.newHttpClient().apply {
modernTLS()
keepAlive()
- trySocks5(DataStore.mixedPort)
+ trySocks5(
+ DataStore.mixedPort,
+ DataStore.mixedInboundUser,
+ DataStore.mixedInboundPass
+ )
}
try {
val response = client.newRequest().apply {
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
index dab195f43..aee7f3564 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
@@ -175,7 +175,23 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
}
mixedPort.onPreferenceChangeListener = reloadListener
- appendHttpProxy.onPreferenceChangeListener = reloadListener
+ appendHttpProxy.setOnPreferenceChangeListener { _, newValue ->
+ if (newValue as Boolean) {
+ MaterialAlertDialogBuilder(requireContext()).apply {
+ setTitle(R.string.append_http_proxy_security_title)
+ setMessage(R.string.append_http_proxy_security_message)
+ setNegativeButton(android.R.string.cancel, null)
+ setPositiveButton(R.string.enable_anyway) { _, _ ->
+ appendHttpProxy.isChecked = true
+ needReload()
+ }
+ }.show()
+ false
+ } else {
+ needReload()
+ true
+ }
+ }
strictRoute.onPreferenceChangeListener = reloadListener
showDirectSpeed.onPreferenceChangeListener = reloadListener
trafficSniffing.onPreferenceChangeListener = reloadListener
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index a8c85d2b4..66d48210e 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -326,6 +326,9 @@
بدون پاسخ
پراکسی HTTP را به VPN اضافه کنید
پروکسی HTTP مستقیماً از (مرورگر/برخی برنامههای پشتیبانیشده) بدون استفاده از دستگاه NIC مجازی (نسخه اندروید بالاتر از 10) استفاده میشود.
+ اطلاعیه امنیتی
+ پس از فعالسازی، برنامههای دیگر در این دستگاه میتوانند مستقیماً از پورت پروکسی محلی استفاده کنند.\n\nتوصیه: تنها در صورتی که به تمام برنامههای نصبشده اعتماد دارید، این گزینه را فعال کنید.
+ همچنان فعال کن
تنظیمات پروتکلها
تأمینکننده Trojan
ابتدایی
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 857b6023f..d58b487f9 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -303,6 +303,9 @@
タイムアウト
VPN に HTTP プロキシを追加
HTTP プロキシは仮想 NIC デバイスを経由せず、(ブラウザ/一部の対応アプリ) から直接使用されます (Android 10+)
+ セキュリティに関する通知
+ 有効にすると、このデバイス上の他のアプリがローカルプロキシポートを直接使用できるようになります。\n\n推奨:インストールされているすべてのアプリを信頼している場合にのみ有効にしてください。
+ それでも有効にする
厳格なルーティング
プロトコル設定
Trojan プロバイダー
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index eb9c030ea..86ad67155 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -297,6 +297,9 @@
시간 초과
VPN에 HTTP 프록시 추가
HTTP 프록시는 가상 NIC 장치를 거치지 않고 (브라우저/일부 지원 앱)에서 직접 사용됩니다 (Android 10+)
+ 보안 알림
+ 활성화하면 이 기기의 다른 앱이 로컬 프록시 포트를 직접 사용할 수 있습니다.\n\n권장 사항: 설치된 모든 앱을 신뢰하는 경우에만 활성화하세요.
+ 그래도 활성화
엄격한 라우팅
프로토콜 설정
Trojan 공급자
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 975111f04..48f759740 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -41,6 +41,9 @@
Версия
Добавить HTTP-прокси к VPN
HTTP-прокси будет использоваться напрямую из (браузера / некоторых поддерживаемых приложений), без использования виртуального сетевого интерфейса (Android 10+)
+Уведомление о безопасности
+При включении другие приложения на этом устройстве смогут напрямую использовать локальный прокси-порт.\n\nРекомендация: Включайте эту функцию только если доверяете всем установленным приложениям.
+Всё равно включить
Применить
Приложения
%d приложений
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 888024d6e..ad6e88709 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -303,6 +303,9 @@
开关
追加 HTTP 代理至 VPN
浏览器 / 一些支持的应用 将直接使用 HTTP 代理, 而不经过虚拟网卡设备 (Android 10+)
+ 安全提示
+ 开启后,本机其他应用可以直接使用本地代理端口。\n\n建议:仅在你信任所有已安装应用时开启。
+ 仍然开启
严格路由
ICMPing 不可用
协议设置
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 831a80370..89c6b260b 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -291,6 +291,9 @@
逾時
為 VPN 附加 HTTP 代理
HTTP 代理將直接被支援的瀏覽器/應用程式使用,而無需經過虛擬網路卡(Android 10+)
+ 安全提示
+ 開啟後,本機其他應用程式可以直接使用本機代理連接埠。\n\n建議:僅在你信任所有已安裝應用程式時開啟。
+ 仍然開啟
Trojan 提供程式
協議設定
基本
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 249a4d688..efecad9a7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -358,6 +358,9 @@
Append HTTP Proxy to VPN
HTTP proxy will be used directly from (browser/ some
supported apps), without going through the virtual NIC device (Android 10+)
+ Security Notice
+ When enabled, other apps on this device can directly use the local proxy port.\n\nRecommendation: Only enable this if you trust all installed apps.
+ Enable Anyway
Strict Route
Protocol Settings
Trojan Provider
diff --git a/libcore/http.go b/libcore/http.go
index c4a8db500..ef8288952 100644
--- a/libcore/http.go
+++ b/libcore/http.go
@@ -36,7 +36,7 @@ type HTTPClient interface {
ModernTLS()
PinnedTLS12()
PinnedSHA256(sumHex string)
- TrySocks5(port int32)
+ TrySocks5(port int32, username string, password string)
TryH3Direct()
KeepAlive()
NewRequest() HTTPRequest
@@ -114,7 +114,7 @@ func (c *httpClient) PinnedSHA256(sumHex string) {
}
}
-func (c *httpClient) TrySocks5(port int32) {
+func (c *httpClient) TrySocks5(port int32, username string, password string) {
dialer := new(net.Dialer)
c.h1h2Transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
for {
@@ -125,7 +125,7 @@ func (c *httpClient) TrySocks5(port int32) {
}
break
}
- _, err = socks.ClientHandshake5(socksConn, socks5.CommandConnect, metadata.ParseSocksaddr(addr), "", "")
+ _, err = socks.ClientHandshake5(socksConn, socks5.CommandConnect, metadata.ParseSocksaddr(addr), username, password)
if err != nil {
if c.tryH3Direct {
return nil, errFailConnectSocks5