diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt index caf363f6f..be2b4d02b 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt @@ -37,7 +37,10 @@ object Key { const val BYPASS_LAN_IN_CORE = "bypassLanInCore" const val MIXED_PORT = "mixedPort" + const val MIXED_USERNAME = "mixedUsername" + const val MIXED_PASSWORD = "mixedPassword" const val ALLOW_ACCESS = "allowAccess" + const val ENABLE_MIXED = "enableMixed" const val SPEED_INTERVAL = "speedInterval" const val SHOW_DIRECT_SPEED = "showDirectSpeed" 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..05920b2c7 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 @@ -56,6 +56,8 @@ abstract class BoxInstance( buildConfig() for ((chain) in config.externalIndex) { chain.entries.forEachIndexed { index, (port, profile) -> + val creds = config.localProxyCredentials[port] ?: error("No local proxy credentials for port $port") + val (localProxyUsername, localProxyPassword) = creds when (val bean = profile.requireBean()) { is TrojanGoBean -> { initPlugin("trojan-go-plugin") @@ -69,7 +71,7 @@ abstract class BoxInstance( is NaiveBean -> { initPlugin("naive-plugin") - pluginConfigs[port] = profile.type to bean.buildNaiveConfig(port) + pluginConfigs[port] = profile.type to bean.buildNaiveConfig(port, localProxyUsername, localProxyPassword) } is HysteriaBean -> { 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 06bb50133..6ed2afcfb 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt @@ -20,6 +20,7 @@ import io.nekohasekai.sagernet.ktx.string import io.nekohasekai.sagernet.ktx.stringToInt import io.nekohasekai.sagernet.ktx.stringToIntIfExists import moe.matsuri.nb4a.TempDatabase +import moe.matsuri.nb4a.utils.Util object DataStore : OnPreferenceDataStoreChangeListener { @@ -128,13 +129,20 @@ object DataStore : OnPreferenceDataStoreChangeListener { var mixedPort: Int get() = getLocalPort(Key.MIXED_PORT, 2080) set(value) = saveLocalPort(Key.MIXED_PORT, value) + var mixedUsername by configurationStore.string(Key.MIXED_USERNAME) { "User" } + var mixedPassword by configurationStore.string(Key.MIXED_PASSWORD) { Util.generateCryptoSecurePassword() } fun initGlobal() { if (configurationStore.getString(Key.MIXED_PORT) == null) { mixedPort = mixedPort } + + if (configurationStore.getString(Key.MIXED_PASSWORD) == null) { + mixedPassword = Util.generateCryptoSecurePassword() + } } + var enableMixed by configurationStore.boolean(Key.ENABLE_MIXED) private fun getLocalPort(key: String, default: Int): Int { return parsePort(configurationStore.getString(key), default + userIndex) diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt index b975695cb..edb9e0523 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt @@ -288,8 +288,11 @@ data class ProxyEntity( } is NaiveBean -> { + val localProxyCredentials = config.localProxyCredentials[port] + val localProxyUsername = localProxyCredentials?.first ?: "" + val localProxyPassword = localProxyCredentials?.second ?: "" append("\n\n") - append(bean.buildNaiveConfig(port)) + append(bean.buildNaiveConfig(port, localProxyUsername, localProxyPassword)) } is HysteriaBean -> { 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 8fe0a2944..798244493 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt @@ -55,6 +55,7 @@ class ConfigBuildResult( var trafficMap: Map>, var profileTagMap: Map, val selectorGroupId: Long, + val localProxyCredentials: Map>, ) { data class IndexEntity(var chain: LinkedHashMap) } @@ -62,7 +63,8 @@ class ConfigBuildResult( fun buildConfig( proxy: ProxyEntity, forTest: Boolean = false, forExport: Boolean = false ): ConfigBuildResult { - + val localProxyCredentials = HashMap>() + if (proxy.type == TYPE_CONFIG) { val bean = proxy.requireBean() as ConfigBean if (bean.type == 0) { @@ -72,7 +74,8 @@ fun buildConfig( proxy.id, // mapOf(TAG_PROXY to listOf(proxy)), // mapOf(proxy.id to TAG_PROXY), // - -1L + -1L, + localProxyCredentials ) } } @@ -224,15 +227,23 @@ fun buildConfig( } } }) - inbounds.add(Inbound_MixedOptions().apply { - type = "mixed" - tag = TAG_MIXED - listen = bind - listen_port = DataStore.mixedPort - domain_strategy = genDomainStrategy(DataStore.resolveDestination) - sniff = needSniff - sniff_override_destination = needSniffOverride - }) + if (DataStore.enableMixed) { + inbounds.add(Inbound_MixedOptions().apply { + type = "mixed" + tag = TAG_MIXED + listen = bind + listen_port = DataStore.mixedPort + domain_strategy = genDomainStrategy(DataStore.resolveDestination) + sniff = needSniff + sniff_override_destination = needSniffOverride + users = listOf( + User().apply { + username = DataStore.mixedUsername + password = DataStore.mixedPassword + }, + ) + }) + } } outbounds = mutableListOf() @@ -326,11 +337,16 @@ fun buildConfig( if (proxyEntity.needExternal()) { // externel outbound val localPort = mkPort() + val localProxyUsername = Util.generateCryptoSecurePassword() + val localProxyPassword = Util.generateCryptoSecurePassword() externalChainMap[localPort] = proxyEntity + localProxyCredentials[localPort] = localProxyUsername to localProxyPassword currentOutbound = Outbound_SocksOptions().apply { type = "socks" server = LOCALHOST server_port = localPort + username = localProxyUsername + password = localProxyPassword } } else { // internal outbound @@ -748,7 +764,8 @@ fun buildConfig( proxy.id, trafficMap, tagMap, - if (buildSelector) group.id else -1L + if (buildSelector) group.id else -1L, + localProxyCredentials ) } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveFmt.kt index cab350e65..0b967bb5e 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveFmt.kt @@ -5,6 +5,7 @@ import io.nekohasekai.sagernet.fmt.LOCALHOST import io.nekohasekai.sagernet.ktx.* import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.json.JSONObject +import android.net.Uri fun parseNaive(link: String): NaiveBean { val proto = link.substringAfter("+").substringBefore(":") @@ -54,7 +55,7 @@ fun NaiveBean.toUri(proxyOnly: Boolean = false): String { return builder.toLink(if (proxyOnly) proto else "naive+$proto", false) } -fun NaiveBean.buildNaiveConfig(port: Int): String { +fun NaiveBean.buildNaiveConfig(port: Int, localProxyUsername: String, localProxyPassword: String): String { return JSONObject().apply { // process ipv6 finalAddress = finalAddress.wrapIPV6Host() @@ -75,7 +76,10 @@ fun NaiveBean.buildNaiveConfig(port: Int): String { } } - put("listen", "socks://$LOCALHOST:$port") + val usernameEnc = Uri.encode(localProxyUsername) + val passwordEnc = Uri.encode(localProxyPassword) + + put("listen", "socks://$usernameEnc:$passwordEnc@$LOCALHOST:$port") put("proxy", toUri(true)) if (extraHeaders.isNotBlank()) { put("extra-headers", extraHeaders.split("\n").joinToString("\r\n")) 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 18d645455..7700a9b88 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt @@ -62,6 +62,9 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { true } val mixedPort = findPreference(Key.MIXED_PORT)!! + val mixedUsername = findPreference(Key.MIXED_USERNAME)!! + val mixedPassword = findPreference(Key.MIXED_PASSWORD)!! + val enableMixed = findPreference(Key.ENABLE_MIXED)!! val serviceMode = findPreference(Key.SERVICE_MODE)!! val allowAccess = findPreference(Key.ALLOW_ACCESS)!! val appendHttpProxy = findPreference(Key.APPEND_HTTP_PROXY)!! @@ -148,7 +151,30 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() { true } + val mixedPrefsToToggle = listOf( + findPreference("mixedPort"), + findPreference("mixedUsername"), + findPreference("mixedPassword"), + findPreference("appendHttpProxy"), + findPreference("allowAccess") + ) + + fun updateMixedPrefsState(enabled: Boolean) { + mixedPrefsToToggle.forEach { it?.isEnabled = enabled } + } + + updateMixedPrefsState(enableMixed?.isChecked == true) + + enableMixed?.setOnPreferenceChangeListener { _, newValue -> + updateMixedPrefsState(newValue as Boolean) + needReload() + true + } + mixedPort.onPreferenceChangeListener = reloadListener + mixedUsername.onPreferenceChangeListener = reloadListener + mixedPassword.onPreferenceChangeListener = reloadListener + //enableMixed.onPreferenceChangeListener = reloadListener appendHttpProxy.onPreferenceChangeListener = reloadListener showDirectSpeed.onPreferenceChangeListener = reloadListener trafficSniffing.onPreferenceChangeListener = reloadListener diff --git a/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt b/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt index c5ae09abb..30e6bf9ee 100644 --- a/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt +++ b/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt @@ -197,4 +197,12 @@ object Util { val encoded = match?.groupValues?.get(1) ?: "" return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name()) } + + fun generateCryptoSecurePassword(length: Int = 10): String { + val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()" + val secureRandom = java.security.SecureRandom() + return (1..length) + .map { chars[secureRandom.nextInt(chars.length)] } + .joinToString("") + } } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bc8c4b2ae..0f632a9b0 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -27,6 +27,10 @@ Реклама Разрешить подключения из локальной сети Привязать входящие серверы к 0.0.0.0 +Логин +Пароль +Включить прокси SOCKS5 +Использование прокси SOCKS5 может раскрыть внешний IP VPN-сервера Разрешить небезопасное подключение Отключить проверку сертификатов при обновлении подписок Отключить проверку сертификата. При включении эта функция настолько же безопасна как передача пароля в открытом виде diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1995ff051..086a81f95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,6 +58,10 @@ Route Settings Allow Connections from the LAN Bind inbound servers to 0.0.0.0 + Username + Password + Enable SOCKS5 proxy + Enabled SOCKS5 proxy may expose the VPN server\'s external IP Inbound Settings App Settings Enable HTTP inbound diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml index 64f4f7901..03318dbd8 100644 --- a/app/src/main/res/xml/global_preferences.xml +++ b/app/src/main/res/xml/global_preferences.xml @@ -189,11 +189,26 @@ + + +