Skip to content

Authenticate naive local SOCKS loopback (#1166 part 2)#45

Merged
hawkff merged 2 commits into
mainfrom
feat/outbound-loopback-creds
Jun 20, 2026
Merged

Authenticate naive local SOCKS loopback (#1166 part 2)#45
hawkff merged 2 commits into
mainfrom
feat/outbound-loopback-creds

Conversation

@hawkff

@hawkff hawkff commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to PR #44. Authenticates the naive external-plugin local SOCKS loopback so
other apps on the device cannot use it as an open relay to learn the egress IP.

Android does not isolate 127.0.0.1 per app, so the naive sidecar's SOCKS listener (bound to
loopback) was reachable by any local app without credentials — the same class of leak the
authenticated mixed inbound already closes, on a port that auth did not cover.

Changes

  • ConfigBuilder.ktConfigBuildResult gains localProxyCredentials: Map<Int, Pair<String,String>>.
    For a naive external outbound, generate per-port neko:<random> credentials, set them on the
    sing-box Outbound_SocksOptions, and record them.
  • NaiveFmt.ktbuildNaiveConfig(port, username?, password?) emits
    listen = socks://user:pass@127.0.0.1:port (URL-encoded) when creds are provided; unchanged
    otherwise (the config-preview path stays credential-free).
  • BoxInstance.kt — pass the per-port creds to buildNaiveConfig at runtime.

Scope

Naive only. Other external plugins (mieru, trojan-go, hysteria v1) use the same local socks
loopback but need separate per-plugin auth verification before enabling — deliberately not
touched here.

Testing

  • CodeRabbit CLI: No findings.
  • Verified on Android (Naive-media profile):
    • naive sidecar starts normally with the authenticated listen URL;
    • curl --socks5 127.0.0.1:<port> without creds → No authentication method was acceptable (rejected);
    • curl --socks5 neko:<secret>@127.0.0.1:<port>SOCKS5 request granted, traffic flows;
    • generated config confirmed: listen: socks://neko:…@127.0.0.1:45165.

Notes

  • naiveproxy's --listen URI accepts a [user:pass@] userinfo for the socks proto and
    enforces it (empirically confirmed above).
  • The credential-free buildNaiveConfig(port) overload is retained for the config-preview/export
    path so no live per-session secret is shown.

Greptile Summary

This PR authenticates the naiveproxy external-plugin local SOCKS loopback by generating a per-session random credential pair (neko:<uuid-hex>) in buildConfig, threading it through ConfigBuildResult.localProxyCredentials, and passing it to both the sing-box SOCKS outbound options and the naive --listen URI. This closes the open-relay hole on 127.0.0.1 that other on-device apps could exploit to probe the egress IP via the naive sidecar.

  • ConfigBuilder.kt: Generates per-port credentials for NaiveBean external outbounds when !forExport, stores them in localProxyCredentials, and sets matching username/password on the sing-box Outbound_SocksOptions. The export guard keeps the export path credential-free so exported profiles remain self-consistent.
  • NaiveFmt.kt: Adds optional listenUsername/listenPassword parameters (resolving the previously flagged property-shadowing concern); when provided, emits socks://user:pass@127.0.0.1:port as the listen URL.
  • BoxInstance.kt: Minimal wiring — looks up per-port credentials from config.localProxyCredentials and forwards them to buildNaiveConfig.

Confidence Score: 5/5

Safe to merge — the credential wiring is consistent across all three files, the export path is correctly guarded, and both halves of every code path agree on whether credentials are present.

Credentials are generated once in buildConfig, stored in localProxyCredentials keyed by localPort, and consumed symmetrically in the sing-box Outbound_SocksOptions and buildNaiveConfig listen URL. The !forExport guard keeps both halves of the export path credential-free. No port-key mismatch, no race between config build and plugin startup, and the UUID-hex password is URL-safe so urlSafe() round-trips cleanly.

No files require special attention.

Important Files Changed

Filename Overview
app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt Adds per-port credential generation for NaiveBean external SOCKS outbounds. Correctly guards with !forExport so the export code path stays credential-free. localProxyCredentials is propagated through ConfigBuildResult and keyed consistently by localPort.
app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveFmt.kt Adds optional listenUsername/listenPassword parameters (renamed from the previously flagged shadowing issue) and conditionally builds an authenticated socks://user:pass@127.0.0.1:port listen URL.
app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt Minimal change: looks up per-port credentials from config.localProxyCredentials and passes them to buildNaiveConfig. Correctly handles the null case (no creds → unauthenticated path).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant BC as buildConfig()
    participant CBR as ConfigBuildResult
    participant BI as BoxInstance.init()
    participant NF as buildNaiveConfig()
    participant SB as sing-box (SOCKS client)
    participant NP as naiveproxy (SOCKS server)

    BC->>BC: mkPort() → localPort
    BC->>BC: UUID.randomUUID() → pass
    BC->>CBR: "localProxyCredentials[localPort] = (neko, pass)"
    BC->>CBR: "Outbound_SocksOptions username=neko password=pass"

    BI->>CBR: "config = buildConfig(profile)"
    BI->>CBR: "creds = localProxyCredentials[port]"
    BI->>NF: buildNaiveConfig(port, neko, pass)
    NF->>NP: "listen = socks://neko:pass@127.0.0.1:port"

    SB->>NP: "SOCKS5 connect username=neko password=pass"
    NP-->>SB: Auth accepted → traffic forwarded
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant BC as buildConfig()
    participant CBR as ConfigBuildResult
    participant BI as BoxInstance.init()
    participant NF as buildNaiveConfig()
    participant SB as sing-box (SOCKS client)
    participant NP as naiveproxy (SOCKS server)

    BC->>BC: mkPort() → localPort
    BC->>BC: UUID.randomUUID() → pass
    BC->>CBR: "localProxyCredentials[localPort] = (neko, pass)"
    BC->>CBR: "Outbound_SocksOptions username=neko password=pass"

    BI->>CBR: "config = buildConfig(profile)"
    BI->>CBR: "creds = localProxyCredentials[port]"
    BI->>NF: buildNaiveConfig(port, neko, pass)
    NF->>NP: "listen = socks://neko:pass@127.0.0.1:port"

    SB->>NP: "SOCKS5 connect username=neko password=pass"
    NP-->>SB: Auth accepted → traffic forwarded
Loading

Reviews (2): Last reviewed commit: "review: address Greptile feedback (skip ..." | Re-trigger Greptile

…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.
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Three files are modified to add optional SOCKS authentication for NaiveBean local proxy listeners. NaiveFmt.kt extends buildNaiveConfig with optional credentials. ConfigBuilder.kt adds a localProxyCredentials map to ConfigBuildResult and generates UUID-based credentials per NaiveBean outbound port. BoxInstance.kt looks up and forwards those credentials when launching the plugin.

Changes

NaiveBean Authenticated Local SOCKS

Layer / File(s) Summary
buildNaiveConfig signature and authenticated listen URL
app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveFmt.kt
Adds optional username and password parameters to buildNaiveConfig; conditionally emits socks://username:password@LOCALHOST:port when both are present, falling back to the unauthenticated form.
ConfigBuildResult extension and credential generation
app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
Adds localProxyCredentials: Map<Int, Pair<String, String>> to ConfigBuildResult; during outbound construction, generates UUID-based credentials for NaiveBean outbounds, stores them keyed by local port, sets them on the SOCKS outbound, and includes the map in the build result.
BoxInstance credential lookup and propagation
app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
Retrieves credentials from config.localProxyCredentials[port] and passes them to bean.buildNaiveConfig when launching a NaiveBean plugin instance.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A rabbit hops past an open door,
"No strangers allowed on port 127.4!"
A UUID key, a password too,
Only NaiveBean knows what to do. 🐇🔐
The loopback is safe, the egress won't leak—
Credentials guard every proxy technique!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: authenticating the naive local SOCKS loopback connection, which is the core purpose of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is directly related to the changeset, providing clear context about authentication of the naive external-plugin local SOCKS loopback and detailing all changes made.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt Outdated
Comment thread app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveFmt.kt Outdated
…wed 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.
@hawkff hawkff merged commit de02d0b into main Jun 20, 2026
5 checks passed
@hawkff hawkff deleted the feat/outbound-loopback-creds branch June 20, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant