Skip to content

Commit 14957bc

Browse files
committed
Refactor mixed inbound authentication
1 parent 70e619b commit 14957bc

21 files changed

Lines changed: 148 additions & 99 deletions

File tree

REPO_SCHEMA.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Android 端的代码主要分为两个核心包:
3535
- `SingBoxOutboundParser.kt`: 将 sing-box 配置中的单个 outbound JSON 还原为原生协议 Bean(支持 shadowsocks/vmess/vless/trojan/hysteria/hysteria2/tuic/socks/http/wireguard/anytls,含 TLS/transport/multiplex 子块解析)。用于订阅返回完整 sing-box 配置(含 `outbounds`)的场景:`RawUpdater.parseJSON``outbounds` 分支对每个 outbound 优先调用 `parseSingBoxOutbound()` 还原原生节点,不支持的类型或解析失败时回退为 `ConfigBean`(自定义 JSON);`dns`/`block`/`direct`/`selector`/`urltest` 类型的 outbound 始终跳过。
3636
- [`io/nekohasekai/sagernet/ui/`](app/src/main/java/io/nekohasekai/sagernet/ui): 各种 Activity 和 Fragment 界面。
3737
- `MainActivity.kt`: 应用主界面。预览版启动时弹出提示对话框(标题为 `BuildConfig.PRE_VERSION_NAME`),提供「不再显示」按钮(`preview_hint_dont_show_again`):点击后把当前版本号写入 `DataStore.previewHintDismissedVersion``Key.PREVIEW_HINT_DISMISSED_VERSION`),之后仅对已忽略的版本不再弹窗,发布新预览版时会重新提示。
38-
- `SettingsFragment.kt`: 设置界面。「入站设置」中含「禁用混合入站」开关(`disableMixedInbound`,见 `SettingsPreferenceFragment.kt`):仅在 TUN 模式下真正生效(`DataStore.mixedInboundDisabled`),开启后 `ConfigBuilder` 不再生成 mixed 入站及其专属的 `inbound = [mixed-in]` 路由规则、不再监听本地代理端口,`VpnService` 同时跳过 `appendHttpProxy`;此时「代理端口」设置项变灰且摘要显示「已禁用」,「追加 HTTP 代理至 VPN」开关被强制关闭(持久化,避免置灰勾选态造成"锁定开启"错觉)并变灰(其依赖项 `httpProxyBypass` 经 dependency 级联一并变灰)。在系统代理模式下开启该开关会 Toast 提示"禁用只在TUN模式有效,当前监听端口:xxx"且不影响混合入站。
38+
- `SettingsFragment.kt`: 设置界面。「入站设置」:TUN 模式(Android 10+)下只要混合入站存在,`VpnService` 即无条件 `setHttpProxy` 指向本地代理端口(原 nb4a「追加 HTTP 代理至 VPN」开关 `appendHttpProxy` 已移除——其作用与混合入站存在性重叠,回环访问控制改由认证字段表达)。「配置身份验证」项(`mixedAuthConfig`,仅 UI 查找键)点击弹出用户名/密码对话框:两项均留空则混合入站免认证(本机回环免密直连),用户名非空则 `ConfigBuilder` 为 mixed 入站配置该账密(`DataStore.mixedInboundNeedsAuth`,仅 TUN 模式);原 nb4a 时代自动生成随机 `mixedSecret` 的隐藏鉴权机制已移除,app 内部组件(订阅更新/资产下载/版本检查经 `DataStore.mixedInboundUser/Pass`)跟随运行时 `mixedInboundAuthed` 自动带票。「禁用混合入站」开关(`disableMixedInbound`,见 `SettingsPreferenceFragment.kt`):仅在 TUN 模式下真正生效(`DataStore.mixedInboundDisabled`),开启后 `ConfigBuilder` 不再生成 mixed 入站及其专属的 `inbound = [mixed-in]` 路由规则、不再监听本地代理端口,`VpnService` 同时跳过 `setHttpProxy`;此时「代理端口」「配置身份验证」「HTTP 代理绕过列表」设置项一并变灰,端口摘要显示「已禁用」。在系统代理模式下开启该开关会 Toast 提示"禁用只在TUN模式有效,当前监听端口:xxx"且不影响混合入站。
3939
- `AboutFragment.kt`: 关于界面。版本更新检查:「检查正式版更新」为灰色禁用项(Throne 尚未发布正式版,点击仅弹出 toast `release_not_available`);「检查预览版更新」请求 GitHub `releases/latest` API,将远端 release 名(即 git tag,形如 `v1.4.2-m20-10`)与本地 `BuildConfig.VERSION_NAME``-` 分段、逐段提取数字组从左到右比较(左侧段优先级高于右侧,如 `v1.2.3-m21-1` > `v1.2.3-m20-100`),见 `compareVersionNames()`
4040
- [`io/nekohasekai/sagernet/widget/`](app/src/main/java/io/nekohasekai/sagernet/widget): 自定义 UI 控件。
4141

app/src/main/java/io/nekohasekai/sagernet/Constants.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ object Key {
4444

4545
const val MIXED_PORT = "mixedPort"
4646
const val DISABLE_MIXED_INBOUND = "disableMixedInbound"
47-
const val MIXED_SECRET = "mixedSecret" // storage key for the generated inbound secret
48-
const val MIXED_USERNAME = "neko" // username presented to the authed mixed inbound
47+
const val MIXED_USERNAME = "mixedUsername" // 混合入站认证用户名;留空即不启用认证
48+
const val MIXED_PASSWORD = "mixedPassword" // 混合入站认证密码
49+
const val MIXED_AUTH_CONFIG = "mixedAuthConfig" // 设置页「配置身份验证」入口,仅 UI 查找用
4950
const val ALLOW_ACCESS = "allowAccess"
5051
const val SPEED_INTERVAL = "speedInterval"
5152
const val SHOW_DIRECT_SPEED = "showDirectSpeed"
5253

53-
const val APPEND_HTTP_PROXY = "appendHttpProxy"
5454
const val HTTP_PROXY_BYPASS = "httpProxyBypass"
5555
const val DNS_HOSTS = "dnsHosts"
5656
const val STRICT_ROUTE = "strictRoute"

app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,8 @@ class VpnService : BaseVpnService(),
188188
}
189189
}
190190

191-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && DataStore.appendHttpProxy &&
192-
!DataStore.mixedInboundDisabled
193-
) {
191+
// 混合入站存在时始终向系统追加 HTTP 代理(Android 10+)
192+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !DataStore.mixedInboundDisabled) {
194193
builder.setHttpProxy(
195194
ProxyInfo.buildDirectProxy(
196195
LOCALHOST,

app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.nekohasekai.sagernet.database
22

33
import android.os.Binder
4-
import android.os.Build
54
import androidx.preference.PreferenceDataStore
65
import io.nekohasekai.sagernet.CONNECTION_TEST_URL
76
import io.nekohasekai.sagernet.GroupType
@@ -142,15 +141,6 @@ object DataStore : OnPreferenceDataStoreChangeListener {
142141

143142
// hopefully hashCode = mHandle doesn't change, currently this is true from KitKat to Nougat
144143
private val userIndex by lazy { Binder.getCallingUserHandle().hashCode() }
145-
val mixedSecret: String
146-
@Synchronized get() {
147-
var s = configurationStore.getString(Key.MIXED_SECRET)
148-
if (s.isNullOrEmpty()) {
149-
s = java.util.UUID.randomUUID().toString().replace("-", "")
150-
configurationStore.putString(Key.MIXED_SECRET, s)
151-
}
152-
return s
153-
}
154144

155145
var mixedPort: Int
156146
get() = getLocalPort(Key.MIXED_PORT, 2080)
@@ -162,12 +152,15 @@ object DataStore : OnPreferenceDataStoreChangeListener {
162152
val mixedInboundDisabled: Boolean
163153
get() = disableMixedInbound && serviceMode == Key.MODE_VPN
164154

155+
// 混合入站账密由用户设置决定:用户名留空即不启用认证(本机回环免密直连)
156+
var mixedUsername by configurationStore.string(Key.MIXED_USERNAME) { "" }
157+
var mixedPassword by configurationStore.string(Key.MIXED_PASSWORD) { "" }
158+
165159
val mixedInboundNeedsAuth: Boolean
166-
get() = serviceMode == Key.MODE_VPN && !mixedInboundDisabled &&
167-
!(appendHttpProxy && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
160+
get() = serviceMode == Key.MODE_VPN && !mixedInboundDisabled && mixedUsername.isNotBlank()
168161

169-
val mixedInboundUser: String get() = if (mixedInboundAuthed) Key.MIXED_USERNAME else ""
170-
val mixedInboundPass: String get() = if (mixedInboundAuthed) mixedSecret else ""
162+
val mixedInboundUser: String get() = if (mixedInboundAuthed) mixedUsername else ""
163+
val mixedInboundPass: String get() = if (mixedInboundAuthed) mixedPassword else ""
171164

172165
fun initGlobal() {
173166
if (configurationStore.getString(Key.MIXED_PORT) == null) {
@@ -194,7 +187,6 @@ object DataStore : OnPreferenceDataStoreChangeListener {
194187

195188
val persistAcrossReboot by configurationStore.boolean(Key.PERSIST_ACROSS_REBOOT) { false }
196189

197-
var appendHttpProxy by configurationStore.boolean(Key.APPEND_HTTP_PROXY)
198190
var httpProxyBypass by configurationStore.string(Key.HTTP_PROXY_BYPASS) { "" }
199191
var dnsHosts by configurationStore.string(Key.DNS_HOSTS) { "" }
200192
var strictRoute by configurationStore.boolean(Key.STRICT_ROUTE) { true }

app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,8 @@ fun buildConfig(
319319
listen_port = DataStore.mixedPort
320320
if (DataStore.mixedInboundNeedsAuth) {
321321
users = listOf(User().also { u ->
322-
u.username = Key.MIXED_USERNAME
323-
u.password = DataStore.mixedSecret
322+
u.username = DataStore.mixedUsername
323+
u.password = DataStore.mixedPassword
324324
})
325325
}
326326
})

app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
9696
val disableMixedInbound = findPreference<SwitchPreference>(Key.DISABLE_MIXED_INBOUND)!!
9797
val serviceMode = findPreference<Preference>(Key.SERVICE_MODE)!!
9898
val allowAccess = findPreference<Preference>(Key.ALLOW_ACCESS)!!
99-
val appendHttpProxy = findPreference<SwitchPreference>(Key.APPEND_HTTP_PROXY)!!
99+
val mixedAuthConfig = findPreference<Preference>(Key.MIXED_AUTH_CONFIG)!!
100100
val httpProxyBypass = findPreference<EditTextPreference>(Key.HTTP_PROXY_BYPASS)!!
101101
val dnsHosts = findPreference<EditTextPreference>(Key.DNS_HOSTS)!!
102102
val strictRoute = findPreference<SwitchPreference>(Key.STRICT_ROUTE)!!
@@ -202,13 +202,12 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
202202
true
203203
}
204204

205-
// 禁用混合入站:开启时代理端口/追加HTTP代理设置项变灰,端口摘要显示「已禁用」
205+
// 禁用混合入站:开启时代理端口/身份验证/绕过列表设置项变灰,端口摘要显示「已禁用」
206206
fun updateMixedPortState(disabled: Boolean = DataStore.disableMixedInbound) {
207207
mixedPort.isEnabled = !disabled
208-
// httpProxyBypass 通过 dependency 级联随 appendHttpProxy 一起变灰
209-
appendHttpProxy.isEnabled = !disabled
208+
mixedAuthConfig.isEnabled = !disabled
209+
httpProxyBypass.isEnabled = !disabled
210210
if (disabled) {
211-
appendHttpProxy.isChecked = false
212211
mixedPort.summaryProvider = null
213212
mixedPort.summary = getString(R.string.mixed_inbound_disabled)
214213
} else {
@@ -229,24 +228,34 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
229228
needReload()
230229
true
231230
}
232-
mixedPort.onPreferenceChangeListener = reloadListener
233-
appendHttpProxy.setOnPreferenceChangeListener { _, newValue ->
234-
if (newValue as Boolean) {
235-
MaterialAlertDialogBuilder(requireContext()).apply {
236-
setTitle(R.string.append_http_proxy_security_title)
237-
setMessage(R.string.append_http_proxy_security_message)
238-
setNegativeButton(android.R.string.cancel, null)
239-
setPositiveButton(R.string.enable_anyway) { _, _ ->
240-
appendHttpProxy.isChecked = true
241-
needReload()
242-
}
243-
}.show()
244-
false
245-
} else {
246-
needReload()
247-
true
248-
}
231+
// 配置身份验证:弹窗编辑混合入站用户名/密码,两项均留空则不启用认证
232+
fun updateMixedAuthSummary() {
233+
mixedAuthConfig.summary = DataStore.mixedUsername.takeIf { it.isNotBlank() }
234+
?.let { getString(R.string.mixed_auth_enabled_sum, it) }
235+
?: getString(R.string.mixed_auth_no_auth)
249236
}
237+
updateMixedAuthSummary()
238+
mixedAuthConfig.setOnPreferenceClickListener {
239+
val view = layoutInflater.inflate(R.layout.layout_mixed_auth_dialog, null)
240+
val usernameEdit = view.findViewById<EditText>(R.id.mixed_username_edit)
241+
val passwordEdit = view.findViewById<EditText>(R.id.mixed_password_edit)
242+
usernameEdit.setText(DataStore.mixedUsername)
243+
passwordEdit.setText(DataStore.mixedPassword)
244+
MaterialAlertDialogBuilder(requireContext())
245+
.setTitle(R.string.mixed_auth_config)
246+
.setView(view)
247+
.setPositiveButton(android.R.string.ok) { _, _ ->
248+
DataStore.mixedUsername = usernameEdit.text.toString().trim()
249+
DataStore.mixedPassword = passwordEdit.text.toString()
250+
updateMixedAuthSummary()
251+
needReload()
252+
}
253+
.setNegativeButton(android.R.string.cancel, null)
254+
.show()
255+
true
256+
}
257+
258+
mixedPort.onPreferenceChangeListener = reloadListener
250259
httpProxyBypass.onPreferenceChangeListener = reloadListener
251260
dnsHosts.onPreferenceChangeListener = reloadListener
252261
strictRoute.onPreferenceChangeListener = reloadListener
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
android:layout_width="match_parent"
5+
android:layout_height="wrap_content"
6+
android:gravity="center_horizontal"
7+
android:orientation="vertical"
8+
android:paddingTop="12dp">
9+
10+
<com.google.android.material.textfield.TextInputLayout
11+
android:layout_width="match_parent"
12+
android:layout_height="wrap_content"
13+
android:layout_marginStart="20dp"
14+
android:layout_marginLeft="20dp"
15+
android:layout_marginEnd="20dp"
16+
android:layout_marginRight="20dp"
17+
android:hint="@string/mixed_auth_username_hint">
18+
19+
<com.google.android.material.textfield.TextInputEditText
20+
android:id="@+id/mixed_username_edit"
21+
android:layout_width="match_parent"
22+
android:layout_height="wrap_content"
23+
android:inputType="text"
24+
android:minHeight="48dp"
25+
android:singleLine="true" />
26+
</com.google.android.material.textfield.TextInputLayout>
27+
28+
<com.google.android.material.textfield.TextInputLayout
29+
android:layout_width="match_parent"
30+
android:layout_height="wrap_content"
31+
android:layout_marginStart="20dp"
32+
android:layout_marginLeft="20dp"
33+
android:layout_marginEnd="20dp"
34+
android:layout_marginRight="20dp"
35+
android:hint="@string/password"
36+
app:passwordToggleEnabled="true">
37+
38+
<com.google.android.material.textfield.TextInputEditText
39+
android:id="@+id/mixed_password_edit"
40+
android:layout_width="match_parent"
41+
android:layout_height="wrap_content"
42+
android:inputType="textPassword"
43+
android:minHeight="48dp"
44+
android:singleLine="true" />
45+
</com.google.android.material.textfield.TextInputLayout>
46+
</LinearLayout>

app/src/main/res/values-ar/strings.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,10 @@
271271
<string name="group_basic">أساسي</string>
272272
<string name="trojan_provider">Trojan مزود</string>
273273
<string name="protocol_settings">إعدادات البروتوكول</string>
274-
<string name="append_http_proxy_sum">سيتم استخدام بروکسی HTTP مباشرة من (المتصفح/ بعض التطبيقات المدعومة)، دون المرور عبر جهاز NIC الافتراضي (Android 10+)</string>
275-
<string name="append_http_proxy">إلحاق بروکسی HTTP ب VPN</string>
274+
<string name="mixed_auth_config">إعداد المصادقة</string>
275+
<string name="mixed_auth_username_hint">اسم المستخدم (اترك الحقلين فارغين لعدم المصادقة)</string>
276+
<string name="mixed_auth_no_auth">غير مُعد (بدون مصادقة)</string>
277+
<string name="mixed_auth_enabled_sum">اسم المستخدم: %1$s</string>
276278
<string name="connection_test_timeout_error">نفذ الوقت</string>
277279
<string name="connection_test_unreachable">لا يمكن الوصول إليه</string>
278280
<string name="connection_test_refused">الاتصال مرفوض</string>

app/src/main/res/values-es/strings.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,10 @@
305305
<string name="connection_test_refused">Conexión denegada</string>
306306
<string name="connection_test_unreachable">Inalcanzable</string>
307307
<string name="connection_test_timeout_error">Se acabó el tiempo</string>
308-
<string name="append_http_proxy">Añadir proxy HTTP a la VPN</string>
309-
<string name="append_http_proxy_sum">El proxy HTTP se utilizará directamente desde (navegador/algunas aplicaciones compatibles), sin pasar por el dispositivo NIC virtual (Android 10+)</string>
308+
<string name="mixed_auth_config">Configurar autenticación</string>
309+
<string name="mixed_auth_username_hint">Nombre de usuario (deja ambos en blanco para no usar autenticación)</string>
310+
<string name="mixed_auth_no_auth">Sin configurar (sin autenticación)</string>
311+
<string name="mixed_auth_enabled_sum">Nombre de usuario: %1$s</string>
310312
<string name="protocol_settings">Ajustes de protocolo</string>
311313
<string name="trojan_provider">Proveedor Trojan</string>
312314
<string name="group_basic">Básico</string>

app/src/main/res/values-fa/strings.xml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,13 +330,12 @@
330330
<string name="connection_test_refused">اتصال رد شد</string>
331331
<string name="connection_test_unreachable">متصل نمی‌شود</string>
332332
<string name="connection_test_timeout_error">بدون پاسخ</string>
333-
<string name="append_http_proxy">پراکسی HTTP را به VPN اضافه کنید</string>
334-
<string name="append_http_proxy_sum">پروکسی HTTP مستقیماً از (مرورگر/برخی برنامه‌های پشتیبانی‌شده) بدون استفاده از دستگاه NIC مجازی (نسخه اندروید بالاتر از 10) استفاده می‌شود.</string>
333+
<string name="mixed_auth_config">پیکربندی احراز هویت</string>
334+
<string name="mixed_auth_username_hint">نام کاربری (خالی گذاشتن هر دو یعنی بدون احراز هویت)</string>
335+
<string name="mixed_auth_no_auth">تنظیم نشده (بدون احراز هویت)</string>
336+
<string name="mixed_auth_enabled_sum">نام کاربری: %1$s</string>
335337
<string name="http_proxy_bypass">فهرست دور زدن پروکسی HTTP</string>
336338
<string name="http_proxy_bypass_dialog_message">در هر خط یک مورد. خطوطی که با # شروع می‌شوند نظر هستند. مثال: *zhihu.com</string>
337-
<string name="append_http_proxy_security_title">اطلاعیه امنیتی</string>
338-
<string name="append_http_proxy_security_message">پس از فعال‌سازی، برنامه‌های دیگر در این دستگاه می‌توانند مستقیماً از پورت پروکسی محلی استفاده کنند.\n\nتوصیه: تنها در صورتی که به تمام برنامه‌های نصب‌شده اعتماد دارید، این گزینه را فعال کنید.</string>
339-
<string name="enable_anyway">همچنان فعال کن</string>
340339
<string name="protocol_settings">تنظیمات پروتکل‌ها</string>
341340
<string name="trojan_provider">تأمین‌کننده Trojan</string>
342341
<string name="group_basic">ابتدایی</string>

0 commit comments

Comments
 (0)