Skip to content

Commit de02d0b

Browse files
authored
Authenticate naive local SOCKS loopback (MatsuriDayo#1166 part 2) (#45)
* feat(security): authenticate naive local SOCKS loopback (MatsuriDayo#1166 part 2) Generate per-port credentials for the naive external-plugin SOCKS listener and dial it from the sing-box socks outbound with the same creds. Android does not isolate 127.0.0.1 per app, so an unauthenticated plugin SOCKS listener could be reached by any local app to leak the egress IP. Verified on-device: the naive SOCKS port now rejects unauthenticated connections (curl: 'No authentication method was acceptable') and accepts with creds. Scoped to naive only; other external plugins (mieru/trojan-go/hysteria v1) need separate per-plugin auth verification before enabling. * review: address Greptile feedback (skip creds on export, rename shadowed params) - ConfigBuilder: gate naive loopback creds on !forExport so the exported sing-box config stays credential-free and matches the credential-free exported naive config (ProxyEntity.buildNaiveConfig), avoiding a broken standalone export. - NaiveFmt: rename buildNaiveConfig params username/password -> listenUsername/ listenPassword to stop shadowing NaiveBean.username/.password receiver properties.
1 parent d391cba commit de02d0b

3 files changed

Lines changed: 41 additions & 4 deletions

File tree

app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ abstract class BoxInstance(
7777

7878
is NaiveBean -> {
7979
initPlugin("naive-plugin")
80-
pluginConfigs[port] = profile.type to bean.buildNaiveConfig(port)
80+
val creds = config.localProxyCredentials[port]
81+
pluginConfigs[port] = profile.type to
82+
bean.buildNaiveConfig(port, creds?.first, creds?.second)
8183
}
8284

8385
is HysteriaBean -> {

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import io.nekohasekai.sagernet.fmt.tuic.TuicBean
2222
import io.nekohasekai.sagernet.fmt.tuic.buildSingBoxOutboundTuicBean
2323
import io.nekohasekai.sagernet.fmt.juicity.JuicityBean
2424
import io.nekohasekai.sagernet.fmt.juicity.buildSingBoxOutboundJuicityBean
25+
import io.nekohasekai.sagernet.fmt.naive.NaiveBean
26+
import java.util.UUID
2527
import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean
2628
import io.nekohasekai.sagernet.fmt.v2ray.buildSingBoxOutboundStandardV2RayBean
2729
import io.nekohasekai.sagernet.fmt.shadowsocksr.ShadowsocksRBean
@@ -69,6 +71,12 @@ class ConfigBuildResult(
6971
var trafficMap: Map<String, List<ProxyEntity>>,
7072
var profileTagMap: Map<Long, String>,
7173
val selectorGroupId: Long,
74+
// Per-port credentials for authenticated local SOCKS loopbacks of external
75+
// plugins (e.g. naive). Android does not isolate 127.0.0.1 per app, so an
76+
// unauthenticated plugin SOCKS listener could be reached by any local app and
77+
// leak the egress IP (issue #1166). The plugin listens with these creds and the
78+
// sing-box socks outbound dials with them.
79+
val localProxyCredentials: Map<Int, Pair<String, String>> = emptyMap(),
7280
) {
7381
data class IndexEntity(var chain: LinkedHashMap<Int, ProxyEntity>)
7482
}
@@ -95,6 +103,8 @@ fun buildConfig(
95103
val trafficMap = HashMap<String, List<ProxyEntity>>()
96104
val tagMap = HashMap<Long, String>()
97105
val globalOutbounds = HashMap<Long, String>()
106+
// Per-port credentials for authenticated external-plugin SOCKS loopbacks (#1166).
107+
val localProxyCredentials = HashMap<Int, Pair<String, String>>()
98108
val readableNames = mutableSetOf(TAG_DIRECT, TAG_BYPASS, TAG_BLOCK, TAG_FRAGMENT, TAG_MIXED, TAG_PROXY)
99109
val group = SagerDatabase.groupDao.getById(proxy.groupId)
100110

@@ -377,6 +387,20 @@ fun buildConfig(
377387
type = "socks"
378388
server = LOCALHOST
379389
server_port = localPort
390+
// Authenticate the local SOCKS loopback for plugins that support
391+
// it (naive), so other apps on the device can't use this open
392+
// 127.0.0.1 port to leak the egress IP (#1166). The plugin is
393+
// configured to listen with the same generated credentials.
394+
// Skip for export: the exported naive config (ProxyEntity.
395+
// buildNaiveConfig without creds) would otherwise mismatch and
396+
// produce a broken standalone config.
397+
if (bean is NaiveBean && !forExport) {
398+
val user = "neko"
399+
val pass = UUID.randomUUID().toString().replace("-", "")
400+
localProxyCredentials[localPort] = user to pass
401+
username = user
402+
password = pass
403+
}
380404
}
381405
} else {
382406
// internal outbound
@@ -941,7 +965,8 @@ fun buildConfig(
941965
proxy.id,
942966
trafficMap,
943967
tagMap,
944-
if (buildSelector) group.id else -1L
968+
if (buildSelector) group.id else -1L,
969+
localProxyCredentials,
945970
)
946971
}
947972

app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveFmt.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ fun NaiveBean.toUri(proxyOnly: Boolean = false): String {
5454
return builder.toLink(if (proxyOnly) proto else "naive+$proto", false)
5555
}
5656

57-
fun NaiveBean.buildNaiveConfig(port: Int): String {
57+
fun NaiveBean.buildNaiveConfig(port: Int, listenUsername: String? = null, listenPassword: String? = null): String {
5858
return JSONObject().apply {
5959
// process ipv6
6060
finalAddress = finalAddress.wrapIPV6Host()
@@ -75,7 +75,17 @@ fun NaiveBean.buildNaiveConfig(port: Int): String {
7575
}
7676
}
7777

78-
put("listen", "socks://$LOCALHOST:$port")
78+
// Authenticate the local SOCKS listener so other apps on the device cannot use
79+
// this loopback port as an open relay and leak the egress IP (#1166). The
80+
// sing-box socks outbound dials with the same credentials.
81+
if (!listenUsername.isNullOrBlank() && !listenPassword.isNullOrBlank()) {
82+
put(
83+
"listen",
84+
"socks://${listenUsername.urlSafe()}:${listenPassword.urlSafe()}@$LOCALHOST:$port"
85+
)
86+
} else {
87+
put("listen", "socks://$LOCALHOST:$port")
88+
}
7989
put("proxy", toUri(true))
8090
if (extraHeaders.isNotBlank()) {
8191
put("extra-headers", extraHeaders.split("\n").joinToString("\r\n"))

0 commit comments

Comments
 (0)