Repository navigation
security: require mixed inbound auth when exposed to LAN - #4
Conversation
The mixed inbound binds to 0.0.0.0 when allowAccess is enabled (in both VPN and Proxy modes), but authentication was only applied in VPN mode and was dropped entirely when appendHttpProxy was active on Android Q+. That left an unauthenticated open proxy reachable from the LAN. - mixedInboundNeedsAuth now requires auth whenever allowAccess is set, regardless of service mode or appendHttpProxy. - Loopback-only behavior is unchanged (appendHttpProxy still works). - When allowAccess and appendHttpProxy are both on (Q+), skip VpnService.setHttpProxy, since Android's system HTTP proxy cannot supply credentials; log a note explaining why.
|
@coderabbitai full review Please focus on whether appendHttpProxy or the mixed inbound can expose an unauthenticated proxy on LAN/Wi-Fi interfaces, and whether this fix prevents IP-leak or open-proxy behavior without breaking localhost-only use. In particular verify: (1) the mixed inbound is created in both VPN and Proxy modes and binds 0.0.0.0 when allowAccess is on; (2) mixedInboundNeedsAuth now requires auth in both modes when allowAccess is set; (3) internal fetchers (RawUpdater, AssetsActivity, AboutFragment) still authenticate via mixedInboundUser/mixedInboundPass; (4) skipping setHttpProxy in the allowAccess+appendHttpProxy combo is correct. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughSummary by CodeRabbit
WalkthroughWhen ChangesLAN Access Proxy Authentication
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
|
@coderabbitai review |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== DataStore.kt - mixedInbound* properties ==="
rg -n "mixedInbound|allowAccess|appendHttpProxy" --type kotlin -C 3
echo ""
echo "=== VpnService.kt - setHttpProxy and allowAccess sections ==="
cat app/src/main/java/io/nekohasekai/sagernet/bg/VpnService.kt
echo ""
echo "=== DataStore.kt full relevant section ==="
cat app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
echo ""
echo "=== Mixed inbound creation - search for where mixed inbound is configured ==="
rg -n "mixed|Mixed|MIXED|inbound|Inbound" --type kotlin -C 2
echo ""
echo "=== Internal fetchers - RawUpdater ==="
fd -e kt -i "rawupdater" --exec cat {}
echo ""
echo "=== Internal fetchers - AssetsActivity ==="
fd -e kt -i "assetsactivity" --exec cat {}
echo ""
echo "=== Internal fetchers - AboutFragment ==="
fd -e kt -i "aboutfragment" --exec cat {}
echo ""
echo "=== mixedInboundUser / mixedInboundPass usage ==="
rg -n "mixedInboundUser|mixedInboundPass|mixedInboundNeedsAuth" --type kotlin -C 3
echo ""
echo "=== BaseService or wherever inbound is created ==="
fd -e kt -i "baseservice" --exec cat {}
echo ""
echo "=== ProxyService or where proxy mode inbounds are set up ==="
fd -e kt -i "proxyservice" --exec cat {}Length of output: 171990
Security Review — PR
|
| allowAccess | serviceMode | appendHttpProxy (Q+) | needsAuth |
|---|---|---|---|
| ON | VPN | ON | true (fixed) |
| ON | VPN | OFF | true |
| ON | Proxy | — | true (was unaddressed before) |
| OFF | VPN | ON | false |
| OFF | VPN | OFF | true |
| OFF | Proxy | — | false (loopback-only; acceptable) |
The last row (Proxy mode, allowAccess=OFF, loopback-only, unauthenticated) is pre-existing behavior and not a regression — the inbound is bound to 127.0.0.1 only, making it reachable by local apps only, which is the intended "local proxy" use-case.
Point 3 ✅ — Internal fetchers authenticate correctly
The authentication chain is intact:
BoxInstance.buildConfig()(line 49):DataStore.mixedInboundAuthed = DataStore.mixedInboundNeedsAuth— snapshotted at startup before the core starts listening.DataStore.mixedInboundUser/Pass: return real credentials iffmixedInboundAuthed=true, otherwise empty strings.BaseService.stopRunner()(line 239): resetsmixedInboundAuthed = false, so fetchers called after teardown do not supply stale credentials to a dead port.
All three fetchers correctly consume this:
RawUpdater.ktlines 65–68:trySocks5(mixedPort, mixedInboundUser, mixedInboundPass)✅AssetsActivity.ktlines 285–288 and 351–355: same pattern in bothupdateAssetandupdateCustomAsset✅AboutFragment.ktlines 219–222: same pattern ✅
When allowAccess=ON (auth required), mixedInboundUser="neko" and mixedInboundPass=<UUID secret> are supplied. When auth is not required (loopback-only Proxy mode), both are empty and trySocks5 connects without credentials, matching the open inbound.
Point 4 ✅ — Skipping setHttpProxy in the allowAccess+appendHttpProxy combination is correct
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && DataStore.appendHttpProxy) {
if (DataStore.allowAccess) {
Logs.w("Append HTTP proxy was skipped …")
} else {
builder.setHttpProxy(ProxyInfo.buildDirectProxy(LOCALHOST, DataStore.mixedPort))
}
}ProxyInfo.buildDirectProxy(host, port) accepts only host and port — there is no mechanism in the Android system HTTP proxy API to supply a username or password. If the system proxy were registered pointing at an auth-required inbound, every app using it would receive a 407 Proxy Authentication Required response and fail silently. More importantly, registering it at all would advertise the LAN-reachable mixedPort to all apps on the device as the system proxy, which could cause IP/routing leaks. Skipping it is the correct and only safe option.
Minor observations (not blockers)
-
No UI feedback when system proxy is silently skipped. When
allowAccess=ONandappendHttpProxy=ON, the skip is logged only viaLogs.w(...)(logcat), invisible to the user. Consider a one-time Toast or a settings summary text when both options are simultaneously enabled, so users understand why the system HTTP proxy is not being applied. -
mixedInboundAuthedis@Volatilebut not atomically paired with config deployment. There is a narrow window betweenDataStore.mixedInboundAuthed = true(inbuildConfig()) and the core actually beginning to listen with auth enforced. Any fetcher invoked in that window would supply credentials to a port that isn't yet open, resulting in a connection refused error — not an open-proxy exposure. This is safe in practice but worth being aware of. -
Proxy mode +
allowAccess=OFF: unauthenticated loopback inbound. This is pre-existing and intentional (local proxy use-case), but it means any local app can use the proxy without credentials. If hardening is desired in a future PR, auth could be extended to Proxy mode as well. Out of scope for this PR.
Summary
The fix correctly closes the open-proxy exposure for all identified scenarios. The two changed files are sufficient — no routing, DNS, or database changes are required. The internal fetchers continue to authenticate transparently via the mixedInboundAuthed snapshot mechanism.
The only actionable suggestion is a UI/UX improvement (item 1 above): surface to the user that the system HTTP proxy is inactive when allowAccess+appendHttpProxy are both on. This is low-priority and can be addressed in a follow-up.
✅ Action performedReview finished.
|
Summary
Closes a hole where the local mixed (SOCKS/HTTP) inbound could be exposed to the
LAN without authentication, turning the device into an open proxy.
Root cause
0.0.0.0wheneverallowAccessis enabled(
ConfigBuilder.kt:143). This inbound is created in both VPN and Proxy modes(it is not gated by
isVPN).mixedInboundNeedsAuthonly required auth in VPN mode, and additionally droppedauth when
appendHttpProxywas active on Android Q+ (because Android's system HTTPproxy cannot send credentials).
allowAccess=ON+appendHttpProxy=ON(Q+), orallowAccess=ONin Proxymode, exposed an unauthenticated proxy to the local network.
Fix
DataStore.mixedInboundNeedsAuthnow requires auth wheneverallowAccessis set,in any service mode, overriding the appendHttpProxy exception. Loopback-only behavior
is unchanged.
VpnService: whenallowAccessandappendHttpProxyare both on (Q+), skipsetHttpProxy(...)instead of weakening inbound auth, and log why. Android's systemHTTP proxy cannot authenticate, so pointing it at the now-authenticated inbound would
fail anyway.
Intentional behavior change
This intentionally makes Allow LAN access and the Android system appendHttpProxy
incompatible:
(it cannot provide credentials through that path).
continue to work because they already authenticate via
mixedInboundUser/mixedInboundPass, which trackmixedInboundNeedsAuth.Behavior matrix (VPN mode)
Behavior matrix (Proxy mode)
Callsites audited
allowAccess,appendHttpProxy,mixedInboundNeedsAuth,mixedInboundUser,mixedInboundPass,setHttpProxy— confirmed:ConfigBuilder.kt:143decides bind; onlyConfigBuilder.kt:257applies inboundusers; only
VpnService.kt:191registers the system proxy.mixedInboundAuthed(set frommixedInboundNeedsAuthinBoxInstance.kt:49) drivesthe credentials used by
RawUpdater,AssetsActivity,AboutFragment.Build/test (AWS Linux builder)
./gradlew app:assembleOssDebugBUILD SUCCESSFUL; all 4 ABI debug APKs produced.Scope
DataStore.kt(auth predicate),VpnService.kt(skip system proxy in theconflicting combo). No UI rewrite, no DB/serialization migration, no routing/DNS changes.
Known limitations
headless builder; can be validated on a metal instance later if desired). The change is
logic on existing settings paths, verified by build + matrix reasoning + callsite audit.