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 a7d46d9fd..4d04eb42b 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 @@ -77,7 +77,9 @@ abstract class BoxInstance( is NaiveBean -> { initPlugin("naive-plugin") - pluginConfigs[port] = profile.type to bean.buildNaiveConfig(port) + val creds = config.localProxyCredentials[port] + pluginConfigs[port] = profile.type to + bean.buildNaiveConfig(port, creds?.first, creds?.second) } 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 331e5daee..d9b945128 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt @@ -22,6 +22,8 @@ import io.nekohasekai.sagernet.fmt.tuic.TuicBean import io.nekohasekai.sagernet.fmt.tuic.buildSingBoxOutboundTuicBean import io.nekohasekai.sagernet.fmt.juicity.JuicityBean import io.nekohasekai.sagernet.fmt.juicity.buildSingBoxOutboundJuicityBean +import io.nekohasekai.sagernet.fmt.naive.NaiveBean +import java.util.UUID import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean import io.nekohasekai.sagernet.fmt.v2ray.buildSingBoxOutboundStandardV2RayBean import io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean @@ -69,6 +71,12 @@ class ConfigBuildResult( var trafficMap: Map>, var profileTagMap: Map, val selectorGroupId: Long, + // Per-port credentials for authenticated local SOCKS loopbacks of external + // plugins (e.g. naive). Android does not isolate 127.0.0.1 per app, so an + // unauthenticated plugin SOCKS listener could be reached by any local app and + // leak the egress IP (issue #1166). The plugin listens with these creds and the + // sing-box socks outbound dials with them. + val localProxyCredentials: Map> = emptyMap(), ) { data class IndexEntity(var chain: LinkedHashMap) } @@ -95,6 +103,8 @@ fun buildConfig( val trafficMap = HashMap>() val tagMap = HashMap() val globalOutbounds = HashMap() + // Per-port credentials for authenticated external-plugin SOCKS loopbacks (#1166). + val localProxyCredentials = HashMap>() val readableNames = mutableSetOf(TAG_DIRECT, TAG_BYPASS, TAG_BLOCK, TAG_FRAGMENT, TAG_MIXED, TAG_PROXY) val group = SagerDatabase.groupDao.getById(proxy.groupId) @@ -377,6 +387,20 @@ fun buildConfig( type = "socks" server = LOCALHOST server_port = localPort + // Authenticate the local SOCKS loopback for plugins that support + // it (naive), so other apps on the device can't use this open + // 127.0.0.1 port to leak the egress IP (#1166). The plugin is + // configured to listen with the same generated credentials. + // Skip for export: the exported naive config (ProxyEntity. + // buildNaiveConfig without creds) would otherwise mismatch and + // produce a broken standalone config. + if (bean is NaiveBean && !forExport) { + val user = "neko" + val pass = UUID.randomUUID().toString().replace("-", "") + localProxyCredentials[localPort] = user to pass + username = user + password = pass + } } } else { // internal outbound @@ -941,7 +965,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..beac41ae4 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 @@ -54,7 +54,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, listenUsername: String? = null, listenPassword: String? = null): String { return JSONObject().apply { // process ipv6 finalAddress = finalAddress.wrapIPV6Host() @@ -75,7 +75,17 @@ fun NaiveBean.buildNaiveConfig(port: Int): String { } } - put("listen", "socks://$LOCALHOST:$port") + // Authenticate the local SOCKS listener so other apps on the device cannot use + // this loopback port as an open relay and leak the egress IP (#1166). The + // sing-box socks outbound dials with the same credentials. + if (!listenUsername.isNullOrBlank() && !listenPassword.isNullOrBlank()) { + put( + "listen", + "socks://${listenUsername.urlSafe()}:${listenPassword.urlSafe()}@$LOCALHOST:$port" + ) + } else { + put("listen", "socks://$LOCALHOST:$port") + } put("proxy", toUri(true)) if (extraHeaders.isNotBlank()) { put("extra-headers", extraHeaders.split("\n").joinToString("\r\n"))