Skip to content

feat: MasterDnsVPN sidecar (DNS-tunnel client, VPN-mode capable)#18

Merged
hawkff merged 17 commits into
mainfrom
feature/masterdnsvpn-sidecar
Jun 18, 2026
Merged

feat: MasterDnsVPN sidecar (DNS-tunnel client, VPN-mode capable)#18
hawkff merged 17 commits into
mainfrom
feature/masterdnsvpn-sidecar

Conversation

@hawkff

@hawkff hawkff commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Draft — do not merge yet. Depends on the paired MasterDnsVPN client change
(Android VpnService socket-protection hook) landing first, since this branch's build
script pulls the client from that branch.

Summary

Integrates masterking32/MasterDnsVPN (DNS-tunneling VPN) as a bundled native-binary
sidecar — a new protocol type — reusing the Mieru/Hysteria2 sidecar pattern. VPN-mode
capable: the forked client protects its upstream resolver sockets from the TUN via
FD_CONTROL_UNIX_SOCKET, pointed at libcore's existing protect_path server.

Emulator validation (x86_64) — PASSED

  • App installs, launches, MainActivity resumed, no crash (new TYPE_MASTERDNSVPN
    • DataStore wiring don't break the DB layer).
  • libmasterdnsvpn.so (built from the fork) extracted to nativeLibraryDir, executable.
  • Run with a config generated by buildMasterDnsVpnConfig + a resolvers file: the client
    parses every field (Protocol SOCKS5, Encryption, Least-Loss strategy, domains,
    "Loaded Resolvers: 2 endpoints"), starts, and reaches out to the resolvers.
    (MTU test fails only because the test domain has no real server — expected.)
  • Protect hook proven: with FD_CONTROL_UNIX_SOCKET set to a stub server, the stub
    received PROTECT REQUEST: payload=1 byte, fds=[8],[9],[12]... — i.e. every upstream
    resolver socket fd is sent for protection, using the exact 0x01 + SCM_RIGHTS wire
    protocol that libcore's protect_server implements. This is the proof VPN mode won't loop.

Changes

  • MasterDnsVpnBean (TYPE_MASTERDNSVPN=25): full field set; needExternal=true,
    canMapping=false.
  • MasterDnsVpnFmt.buildMasterDnsVpnConfig (JSON, uppercase keys, FD_CONTROL_UNIX_SOCKET,
    loopback SOCKS5 no-auth; advancedJson merged with try-catch) + resolverLines().
  • ProxyEntity / TypeMap / KryoConverters wired; settings activity mapped.
  • BoxInstance init/launch: writes config + resolvers files, runs
    libmasterdnsvpn.so -json <cfg> -resolvers <file>.
  • buildScript/lib/masterdnsvpn.sh: cross-compile the fork (4 ABIs); validates Go >= 1.25.
  • PluginManager + Executable + add_profile_menu + full preferences UI (all tuning knobs).

Defaults / security

  • Encryption method defaults to None (0) (not XOR) to avoid false security — the user
    must pick a cipher + key that matches their server.

NOT done in this PR (follow-ups)

  • CI wiring: the sidecars job needs a masterdnsvpn build step + verify across the 4
    workflows. Deferred because the gh token lacks workflow scope (and SSH push is currently
    unavailable). To be added when push access to .github/workflows/ is restored.
  • The review-fix commit (try-catch / default / docs / shell guard) was not re-built on CI
    the prior commit was fully emulator-validated and
    these changes don't alter the validated runtime path structurally.

No sing-box core change.

Greptile Summary

This PR integrates MasterDnsVPN as a new native-binary sidecar protocol (TYPE_MASTERDNSVPN=25), following the Mieru/Hysteria2 pattern. It adds the full data model, JSON config builder, share-link serialization, settings UI, CI wiring, and a cross-compilation build script for all four Android ABIs.

  • New protocol sidecar: MasterDnsVpnBean + buildMasterDnsVpnConfig generate a loopback SOCKS5 config with VPN-mode socket protection via FD_CONTROL_UNIX_SOCKET, and awaitExternalProcessesReady blocks VPN startup until the listener is connectable (up to 60 s for MasterDnsVPN, 1 s floor for others).
  • Build pipeline: masterdnsvpn.sh cross-compiles the forked Go client; all four workflow files now include the sidecar in cache keys and artifact verification, and actions/cache was pinned to a commit SHA.
  • DB schema: proxy_entities gains a masterDnsVpnBean BLOB column (schema version 8 → 9), but the autoMigrations list in SagerDatabase.kt still stops at from = 7, to = 8 — the missing AutoMigration(from = 8, to = 9) entry means devices upgrading from version 8 will hit fallbackToDestructiveMigration() and lose all saved proxies.

Confidence Score: 3/5

Not safe to merge: the database schema bumps from 8 to 9 but the AutoMigration entry for that step is still missing, meaning any user upgrading from the prior release will have all proxy profiles silently wiped by Room's destructive-migration fallback.

The core protocol implementation is solid and follows the established sidecar pattern well. However, the missing AutoMigration(from=8, to=9) in SagerDatabase.kt is a live data-loss path for users upgrading from the previous release, unaddressed across three prior rounds of review. The new awaitExternalProcessesReady call in startProcesses also introduces a hard IOException failure path for all existing sidecar types (Mieru, NaïVe, TrojanGo, Hysteria) that was not present before this PR.

app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt (AutoMigration 8→9 entry still absent) and app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt (awaitExternalProcessesReady now throws for all sidecar protocols).

Important Files Changed

Filename Overview
app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/9.json Schema bumped to version 9 adding masterDnsVpnBean BLOB column, but SagerDatabase.kt still lacks AutoMigration(from=8, to=9); devices on DB v8 will lose all proxies via fallbackToDestructiveMigration.
app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt Adds MasterDnsVPN launch block and the new awaitExternalProcessesReady suspend function; the 1-second floor timeout now affects all existing sidecar protocols and throws IOException on expiry — a new hard failure mode that was not there before this PR.
app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnFmt.kt New file: config builder, URI serializer/parser. Fail-fast guards for empty domains, empty resolvers, and missing encryption key are present; advancedJson merge correctly blocks safety-critical keys.
buildScript/lib/masterdnsvpn.sh Cross-compiles four ABIs using NDK; MDVPN_COMMIT takes precedence over MDVPN_REF so builds are pinned to a specific commit SHA; Go 1.25+ version check is present.
app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnBean.java New data model following the existing sidecar bean pattern; serialization version field (v0), all fields have null-safe defaults, Kryo round-trip looks correct.
app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt Adds 20 MDV_* DataStore properties matching the bean fields; default values are consistent with MasterDnsVpnBean.initializeDefaultValues().
app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt Adds masterDnsVpnBean nullable column and TYPE_MASTERDNSVPN=25 constant; TYPE_AWG=26 comment guards against type-ID collision on merge.
app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java Adds masterDnsVpnDeserialize TypeConverter; generic serialize() already handles AbstractBean subclasses, so only the deserializer direction is needed — correct.
app/src/main/java/io/nekohasekai/sagernet/ui/profile/MasterDnsVpnSettingsActivity.kt New settings activity with complete init/serialize symmetry; numeric keyboard modifiers applied to all integer fields; password summary provider used for encryption key.
.github/workflows/build.yml Adds masterdnsvpn.sh to sidecar status hash and MDVPN_COMMIT to cache key; actions/cache pinned to commit SHA, but actions/checkout and actions/setup-go remain on mutable version tags.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant BS as BaseService
    participant BI as BoxInstance
    participant GP as GuardedProcessPool
    participant MC as libmasterdnsvpn.so
    participant LP as libcore protect_path

    BS->>BI: init()
    BI->>BI: buildMasterDnsVpnConfig(port, protectPath)
    Note over BI: validates domains, key, resolvers
    BI->>BI: loadConfig() → Libcore.newSingBoxInstance

    BS->>BI: launch()
    BI->>GP: "start([libmasterdnsvpn.so, -json cfg, -resolvers file], GODEBUG=asyncpreemptoff=1)"
    GP->>MC: spawn process

    BS->>BI: awaitExternalProcessesReady()
    loop every 50ms until port connectable or 60s timeout
        BI->>BI: Socket().connect(127.0.0.1:port, 100ms)
    end
    Note over BI: throws IOException if timeout

    MC->>LP: FD_CONTROL_UNIX_SOCKET protect request (SCM_RIGHTS)
    LP-->>MC: protected fd returned
    MC->>MC: DNS tunnel established
    MC-->>BI: SOCKS5 listener bound on 127.0.0.1:port

    BI->>BI: box.start()
    Note over BI: sing-box dials 127.0.0.1:port via socks outbound
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 BS as BaseService
    participant BI as BoxInstance
    participant GP as GuardedProcessPool
    participant MC as libmasterdnsvpn.so
    participant LP as libcore protect_path

    BS->>BI: init()
    BI->>BI: buildMasterDnsVpnConfig(port, protectPath)
    Note over BI: validates domains, key, resolvers
    BI->>BI: loadConfig() → Libcore.newSingBoxInstance

    BS->>BI: launch()
    BI->>GP: "start([libmasterdnsvpn.so, -json cfg, -resolvers file], GODEBUG=asyncpreemptoff=1)"
    GP->>MC: spawn process

    BS->>BI: awaitExternalProcessesReady()
    loop every 50ms until port connectable or 60s timeout
        BI->>BI: Socket().connect(127.0.0.1:port, 100ms)
    end
    Note over BI: throws IOException if timeout

    MC->>LP: FD_CONTROL_UNIX_SOCKET protect request (SCM_RIGHTS)
    LP-->>MC: protected fd returned
    MC->>MC: DNS tunnel established
    MC-->>BI: SOCKS5 listener bound on 127.0.0.1:port

    BI->>BI: box.start()
    Note over BI: sing-box dials 127.0.0.1:port via socks outbound
Loading

Comments Outside Diff (3)

  1. app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt, line 20-26 (link)

    P1 Missing AutoMigration from 8 → 9 will destroy all user data

    The database version bumps from 8 to 9, but the autoMigrations list only goes up to from = 7, to = 8. Because fallbackToDestructiveMigration() is set (line 42), Room will wipe the entire proxy_entities table for any user upgrading from DB version 8 — all proxy profiles are silently deleted. Adding a nullable BLOB column is exactly what Room AutoMigration handles automatically; AutoMigration(from = 8, to = 9) just needs to be appended to the list alongside the schema JSON file already committed as 9.json.

  2. app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt, line 20-26 (link)

    P1 Missing AutoMigration from 8 → 9 will silently wipe all proxy profiles on upgrade

    The database version bumps from 8 to 9, but the autoMigrations list only goes up to from = 7, to = 8. Because fallbackToDestructiveMigration() is set on line 42, Room will drop and recreate the proxy_entities table for any user whose device holds DB version 8 — silently deleting every saved proxy profile. Adding a nullable BLOB column (for masterDnsVpnBean) is exactly the case Room AutoMigration handles automatically with zero extra code; an AutoMigration(from = 8, to = 9) annotation alongside the already-committed 9.json schema file is all that is needed.

  3. app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt, line 18-26 (link)

    P1 Missing AutoMigration(from = 8, to = 9) will silently wipe all proxy profiles on upgrade

    The database version bumps from 8 to 9, but the autoMigrations list stops at from = 7, to = 8. Because fallbackToDestructiveMigration() is set (line 42), Room will drop and recreate the proxy_entities table for any user whose device holds DB version 8 — silently deleting every saved proxy profile. Adding a nullable BLOB column is exactly what Room AutoMigration handles without extra code; appending AutoMigration(from = 8, to = 9) alongside the committed 9.json schema file is all that is needed.

Reviews (6): Last reviewed commit: "ci: scope release.yml contents:write to ..." | Re-trigger Greptile

hawkff added 3 commits June 16, 2026 15:03
Integrate masterking32/MasterDnsVPN (DNS-tunneling VPN) as a bundled native-binary
sidecar, using the hawkff fork that adds an Android FD_CONTROL_UNIX_SOCKET protect hook
(wire-compatible with libcore's protect_path server: SCM_RIGHTS fd + 0x01 status).

- MasterDnsVpnBean (TYPE_MASTERDNSVPN=25): full field set (domains, encryption method/key,
  resolvers, balancing strategy, packet duplication, compression, MTU bounds, local DNS,
  log level, advanced JSON override); needExternal=true, canMapping=false (binary connects
  to resolvers directly with protected sockets).
- MasterDnsVpnFmt.buildMasterDnsVpnConfig: emits the client JSON config (uppercase keys,
  FD_CONTROL_UNIX_SOCKET=protect_path, loopback SOCKS5 no-auth) + resolverLines() for the
  resolvers file; advancedJson merged last.
- ProxyEntity/TypeMap/KryoConverters wired; settings activity mapped.
- BoxInstance: init builds config; launch writes config+resolvers files and runs
  libmasterdnsvpn.so -json <cfg> -resolvers <file>.
- buildScript/lib/masterdnsvpn.sh: cross-compile the fork (branch
  android-vpnservice-protect-hook) for all 4 ABIs -> app/executableSo/<abi>/libmasterdnsvpn.so.
- PluginManager.initNativeInternal + Executable.EXECUTABLES + add_profile_menu + full
  preferences UI (MasterDnsVpnSettingsActivity, arrays, strings).

VPN-mode capable (fork protects upstream sockets). Sidecar-only; no sing-box core change.
- buildMasterDnsVpnConfig: wrap advancedJson parse in try-catch (malformed
  override must not crash config build / VPN start; logged + ignored).
- Default encryption method 1 (XOR) -> 0 (None) in Bean/DataStore/XML: avoid
  false security (XOR with an empty key); user picks a cipher + key explicitly.
- Fix KDoc to match the actual launch flags (-json <file>, not -json-base64).
- masterdnsvpn.sh: validate Go >= 1.25 before building.
- Simplify serverAddress placeholder in the settings init.
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds MasterDnsVPN as a new external proxy profile type (TYPE_MASTERDNSVPN = 25) with a full MasterDnsVpnBean configuration model, masterdns:// URI parsing/serialization, sidecar JSON config generation, Room persistence, BoxInstance process launch with TCP readiness probing, a settings UI backed by DataStore MDV_* keys, cross-compilation build script for libmasterdnsvpn.so, and CI workflow updates. Also extends SingBoxOptionSerializer.applyHackConfig() to recurse into Map<?,?> fields.

Changes

MasterDnsVPN Proxy Profile Type Integration

Layer / File(s) Summary
MasterDnsVpnBean data model, constants, and DataStore persistence
app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnBean.java, app/src/main/java/io/nekohasekai/sagernet/Constants.kt, app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
New MasterDnsVpnBean Java class with 21 public config fields, initializeDefaultValues(), Kryo serialize/deserialize with version marker, clone() via Kryo, and capability overrides returning false. MDV_* constants added to Key object; 21 matching preference-backed properties added to DataStore.
MasterDnsVpnFmt config, resolver, and URI generation
app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnFmt.kt, app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt
buildMasterDnsVpnConfig() generates validated sidecar JSON with SOCKS5 listener, optional local-DNS, FD_CONTROL_UNIX_SOCKET, and safe advancedJson merging. resolverLines() normalizes resolver strings. toUri() serializes to masterdns:// links; parseMasterDnsVpn() parses them. Formats.parseProxies() dispatches the new scheme.
ProxyEntity TYPE_MASTERDNSVPN type support and Room wiring
app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt, app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java, app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt, app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/9.json
TYPE_MASTERDNSVPN = 25 and masterDnsVpnBean field added to ProxyEntity; all dispatch paths wired (putByteArray, displayType, requireBean, needExternal, putBean, toStdLink, settingIntent). KryoConverters.masterDnsVpnDeserialize() Room type converter added. TypeMap entry for "masterdnsvpn" registered. Room schema snapshot version 9 adds masterDnsVpnBean BLOB column.
BoxInstance sidecar initialization, launch, readiness probing, and process management
app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt, app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt, app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt, app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt
BoxInstance.init() and launch() extended with MasterDnsVpnBean branches writing config/resolvers files and starting masterdnsvpn-plugin with GODEBUG=asyncpreemptoff=1. New awaitExternalProcessesReady() TCP-probes sidecar SOCKS ports with extended timeout for MasterDnsVPN. BaseService.startProcesses() calls it after launch(). PluginManager resolves libmasterdnsvpn.so; Executable.EXECUTABLES includes it for kill.
Settings Activity, preferences XML, and resource definitions
app/src/main/java/io/nekohasekai/sagernet/ui/profile/MasterDnsVpnSettingsActivity.kt, app/src/main/res/xml/masterdnsvpn_preferences.xml, app/src/main/res/values/strings.xml, app/src/main/res/values/arrays.xml
MasterDnsVpnSettingsActivity wires init()/serialize() to DataStore MDV_* fields, forces serverAddress="masterdnsvpn" and serverPort=0, configures password summary for encryption key. Preferences XML defines proxy/resilience/compression/MTU/misc categories. 26 string resources and 6 string-arrays for dropdowns added.
Menu item, activity navigation, and manifest registration
app/src/main/res/menu/add_profile_menu.xml, app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt, app/src/main/AndroidManifest.xml
"masterdns vpn" submenu item added to the add-profile menu. ConfigurationFragment handles action_new_masterdnsvpn to launch settings. Manifest declares MasterDnsVpnSettingsActivity with android:configChanges="uiMode" and registers the masterdns scheme in MainActivity's intent filter.
Cross-compilation build script for libmasterdnsvpn.so
buildScript/lib/masterdnsvpn.sh
New Bash script locates NDK LLVM toolchain, validates Go ≥1.25, clones forked MasterDnsVPN repo at pinned commit, and cross-compiles libmasterdnsvpn.so for arm64-v8a, armeabi-v7a, x86, and x86_64 using -trimpath -ldflags='-s -w'.
GitHub Actions workflow updates for sidecar build and caching
.github/workflows/build.yml, .github/workflows/ci.yml, .github/workflows/preview.yml, .github/workflows/release.yml
All four workflows gain MDVPN_REF/MDVPN_COMMIT env vars, include masterdnsvpn.sh in sidecars status hash, extend cache keys with MDVPN_COMMIT, pin actions/cache revisions, add "MasterDnsVPN Build" step on cache miss, and extend artifact verification to require libmasterdnsvpn.so. ci.yml also enables APK artifact upload.

SingBoxOptions Recursive Map Hack Configuration

Layer / File(s) Summary
Recursive hack configuration application into Map fields
app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java
SingBoxOptionSerializer.applyHackConfig() extended to iterate Map<?,?> reflected fields: when the corresponding JSON node is a JsonObject, it looks up each map entry key as a JSON property and recursively applies hack configuration to nested SingBoxOption values.

Sequence Diagram(s)

sequenceDiagram
  participant User as User/Import
  participant MainActivity
  participant BoxInstance
  participant MasterDnsVpnFmt
  participant libmasterdnsvpn as libmasterdnsvpn.so
  participant BaseService

  rect rgba(100, 149, 237, 0.5)
    Note over User,MainActivity: Profile Import via masterdns:// URI
    User->>MainActivity: Intent with masterdns:// scheme
    MainActivity->>MasterDnsVpnFmt: parseMasterDnsVpn(url)
    MasterDnsVpnFmt-->>MainActivity: MasterDnsVpnBean
  end

  rect rgba(144, 238, 144, 0.5)
    Note over BaseService,libmasterdnsvpn: Sidecar Startup
    BaseService->>BoxInstance: launch()
    BoxInstance->>MasterDnsVpnFmt: buildMasterDnsVpnConfig(port, protectPath)
    MasterDnsVpnFmt-->>BoxInstance: JSON config
    BoxInstance->>libmasterdnsvpn: start -json config.json -resolvers resolvers.txt
    BoxInstance-->>BaseService: launch() done

    BaseService->>BoxInstance: awaitExternalProcessesReady()
    loop TCP probe until ready or timeout
      BoxInstance->>libmasterdnsvpn: connect LOCALHOST:socks_port
      libmasterdnsvpn-->>BoxInstance: accept
    end
    BoxInstance-->>BaseService: ready
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • hawkff/NekoBoxForAndroid#12: Both PRs extend the same bundled-native sidecar mechanism by updating PluginManager.initNativeInternal to resolve a bundled plugin .so and adding it to Executable.EXECUTABLES for process cleanup.
  • hawkff/NekoBoxForAndroid#17: Both PRs implement bundled sidecar support by updating BoxInstance sidecar init/launch branching, Executable.EXECUTABLES kill-allowlists, and CI/workflow sidecar artifact handling for different protocols.
  • hawkff/NekoBoxForAndroid#25: Both PRs extend the same ProxyEntity profile-dispatch plumbing (TypeMap, KryoConverters) to register new proxy profile types.

Poem

🐇 A new tunnel digs through the DNS night,
MasterDnsVPN hops into sight!
JSON configs fly, resolvers align,
masterdns:// links sparkle and shine.
The rabbit compiles for arm, x86, and more—
Four ABIs built, from toolchain to store! 🏗️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.12% 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 directly and clearly summarizes the main change: adding MasterDnsVPN as a new DNS-tunnel sidecar protocol with VPN mode capability.
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 comprehensively explains MasterDnsVPN integration as a native-binary sidecar, covering bean implementation, config generation, UI wiring, build scripts, and critical runtime fixes. It directly relates to the changeset provided.

✏️ 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.

@hawkff hawkff marked this pull request as ready for review June 16, 2026 20:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt`:
- Around line 249-257: The filenames for configFile and resolversFile are
generated using only SystemClock.elapsedRealtime(), which causes collisions when
multiple MasterDnsVPN sidecars start within the same millisecond, leading to
file overwrites. Replace the current filename generation approach with a
collision-safe method such as using File.createTempFile() or appending a unique
identifier (like UUID.randomUUID()) in addition to the timestamp when
constructing the filenames for both the config file and the resolvers file,
ensuring each sidecar instance gets uniquely named artifacts in the cache
directory.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a833eab-9903-43cc-bc2f-d8ba766626f6

📥 Commits

Reviewing files that changed from the base of the PR and between ae7c87c and 33523bd.

📒 Files selected for processing (18)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/io/nekohasekai/sagernet/Constants.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
  • app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnBean.java
  • app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnFmt.kt
  • app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/profile/MasterDnsVpnSettingsActivity.kt
  • app/src/main/res/menu/add_profile_menu.xml
  • app/src/main/res/values/arrays.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/xml/masterdnsvpn_preferences.xml
  • buildScript/lib/masterdnsvpn.sh

Comment thread app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt Outdated
…mp files

- Add a MasterDnsVPN build step to the combined 'sidecars' job across ci/build/
  preview/release: builds libmasterdnsvpn.so (4 ABIs) from the fork ref pinned via
  MDV_REF, caches it with the other sidecars (cache key + status hash include
  masterdnsvpn.sh and MDV_REF), and the build job's verify covers libmasterdnsvpn.so.
  Go 1.26.4 already satisfies the fork's Go 1.25+ requirement, so no extra setup-go.
- BoxInstance: namespace the MasterDnsVPN config/resolvers temp files by port (unique
  per chain entry) to avoid same-millisecond collisions (CodeRabbit).
@hawkff

hawkff commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Addressed the temp-file collision finding: MasterDnsVPN config/resolvers files are now namespaced by the (unique-per-chain-entry) local port, so two sidecars can't overwrite each other. Also wired the masterdnsvpn build into the combined 'sidecars' CI job across all 4 workflows (build + cache + verify), pinned via MDV_REF.

Note: this feature was validated on an x86_64 Android emulator before review — the binary parses the generated config, opens SOCKS, and the FD_CONTROL_UNIX_SOCKET protect hook sends upstream socket fds to a stub server using the exact 0x01 + SCM_RIGHTS protocol libcore's protect_server implements.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

@hawkff: Thanks for the update — the port-namespaced temp files and CI wiring sound like solid improvements. I'll kick off a full review of the current state of the PR now.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build.yml:
- Line 12: The workflow file `.github/workflows/build.yml` defines and uses the
variable `MDV_REF`, but the build script `buildScript/lib/masterdnsvpn.sh`
expects the variable to be named `MDVPN_REF`. This mismatch means the build
reference pinning is ineffective because the script never receives the value.
Rename all instances of `MDV_REF` to `MDVPN_REF` throughout the workflow file:
at the definition on line 12 (anchor), and at all usage points on lines 55,
67-69, and 97 (siblings) to align with what the build script actually reads.
- Line 51: The actions/cache steps are using a floating version tag `@v5` instead
of being pinned to a specific commit SHA, which violates the stated policy.
Replace the floating tag `@v5` with a pinned commit SHA for the actions/cache
action. This change needs to be applied at all locations where actions/cache is
used in the workflow file to ensure consistent policy compliance across all
cache steps.

In @.github/workflows/ci.yml:
- Line 12: The environment variable name is inconsistent between the CI workflow
and the build script. The workflow uses MDV_REF at lines 12, 60, 72-74, and 107,
but the build script (./run lib masterdnsvpn) expects MDVPN_REF. Rename all
occurrences of MDV_REF to MDVPN_REF throughout the workflow file to ensure the
cache keys and the actual compiled source reference are controlled by the same
variable and match what the build script reads.
- Line 56: The `actions/cache@v5` references are unpinned semantic version
references that violate policy and should be replaced with immutable commit SHAs
for reproducibility and security. Pin both occurrences of `actions/cache@v5` to
their corresponding commit SHA (look up the commit SHA for the v5 release of the
actions/cache repository) by changing the reference from `actions/cache@v5` to
`actions/cache@<commit-sha>`.

In @.github/workflows/preview.yml:
- Line 8: The preview workflow defines a variable called MDV_REF but the
masterdnsvpn.sh script expects the variable to be named MDVPN_REF, causing the
workflow's value to not be read by the script. Rename all occurrences of MDV_REF
to MDVPN_REF throughout the preview.yml workflow file at line 8, lines 51, lines
63-65, and line 93 to ensure the variable naming is consistent and the script
actually uses the pinned branch/commit value during the build execution.

In @.github/workflows/release.yml:
- Line 53: The actions/cache action references in the release workflow are
unpinned (using `@v5` tag). Replace the unpinned version reference with an
immutable commit SHA reference for the actions/cache action at both affected
locations (line 53 and line 95). Look for instances of `uses: actions/cache@v5`
and change them to use a full commit SHA instead of the version tag to ensure
security and reproducibility.
- Line 14: The release workflow uses the environment variable name `MDV_REF` but
the masterdnsvpn.sh script reads `MDVPN_REF`, causing a mismatch that makes ref
pinning ineffective. Choose one consistent variable name and update all
occurrences across both the workflow configuration (where `MDV_REF` is set) and
the buildScript/lib/masterdnsvpn.sh script (where it is read) to use the same
variable name end-to-end, ensuring the release ref is properly passed and cache
keys are correctly generated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e31a00cc-0544-48fb-8597-a5b13b4e3d40

📥 Commits

Reviewing files that changed from the base of the PR and between 33523bd and 22b10d5.

📒 Files selected for processing (5)
  • .github/workflows/build.yml
  • .github/workflows/ci.yml
  • .github/workflows/preview.yml
  • .github/workflows/release.yml
  • app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt

Comment thread .github/workflows/build.yml Outdated
Comment thread .github/workflows/build.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/preview.yml Outdated
Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/src/main/res/xml/masterdnsvpn_preferences.xml (1)

116-119: ⚡ Quick win

Constrain mdvLogLevel to valid options.

mdvLogLevel is currently free-text, unlike your other enum-like settings. Invalid values can flow into generated config and break sidecar startup; prefer a menu-backed preference with fixed entries.

Suggested XML change
-        <EditTextPreference
-            app:key="mdvLogLevel"
-            app:title="`@string/mdv_log_level`"
-            app:useSimpleSummaryProvider="true" />
+        <moe.matsuri.nb4a.ui.SimpleMenuPreference
+            app:defaultValue="INFO"
+            app:entries="`@array/mdv_log_level`"
+            app:entryValues="`@array/mdv_log_level_value`"
+            app:key="mdvLogLevel"
+            app:title="`@string/mdv_log_level`"
+            app:useSimpleSummaryProvider="true" />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/xml/masterdnsvpn_preferences.xml` around lines 116 - 119,
The mdvLogLevel preference currently uses EditTextPreference which allows
arbitrary free-text input that can break sidecar startup. Replace the
EditTextPreference element with app:key="mdvLogLevel" with a ListPreference that
has fixed app:entries and app:entryValues attributes defining the valid log
level options, ensuring only valid values can be selected instead of allowing
free-text input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@buildScript/lib/masterdnsvpn.sh`:
- Around line 24-25: The script currently reads the MDVPN_REF variable, but the
workflow defines and caches using MDV_REF, causing a mismatch between the cache
key reference and the actual built reference. Update the MDVPN_REF variable
assignment to read from the MDV_REF workflow variable instead, ensuring
consistency between what the workflow sets and what the script actually uses for
the build reference.

---

Nitpick comments:
In `@app/src/main/res/xml/masterdnsvpn_preferences.xml`:
- Around line 116-119: The mdvLogLevel preference currently uses
EditTextPreference which allows arbitrary free-text input that can break sidecar
startup. Replace the EditTextPreference element with app:key="mdvLogLevel" with
a ListPreference that has fixed app:entries and app:entryValues attributes
defining the valid log level options, ensuring only valid values can be selected
instead of allowing free-text input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1fd56c20-2bff-45ec-bf6a-f578817913ca

📥 Commits

Reviewing files that changed from the base of the PR and between ae7c87c and 22b10d5.

📒 Files selected for processing (22)
  • .github/workflows/build.yml
  • .github/workflows/ci.yml
  • .github/workflows/preview.yml
  • .github/workflows/release.yml
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/io/nekohasekai/sagernet/Constants.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
  • app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnBean.java
  • app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnFmt.kt
  • app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/profile/MasterDnsVpnSettingsActivity.kt
  • app/src/main/res/menu/add_profile_menu.xml
  • app/src/main/res/values/arrays.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/xml/masterdnsvpn_preferences.xml
  • buildScript/lib/masterdnsvpn.sh

The workflows set MDV_REF but masterdnsvpn.sh reads MDVPN_REF, so the ref
pin was ineffective (script used its hardcoded default) and the cache key
referenced a var that didn't drive the build. Rename to MDVPN_REF so the
workflow env actually selects the fork ref and the cache invalidates on change.
@hawkff

hawkff commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Fixed the ref-name mismatch: renamed the workflow env MDV_REF -> MDVPN_REF to match what buildScript/lib/masterdnsvpn.sh reads (${MDVPN_REF:-...}), so the ref pin now actually selects the fork branch and the cache key invalidates on change.

On 'pin actions/cache by commit SHA': acknowledged but declining for this PR — every action across all 4 workflows is tag-pinned (actions/checkout@v5, setup-java@v5, cache@v5, etc.); SHA-pinning is a repo-wide supply-chain hardening change, not specific to this feature, and would be inconsistent to apply to only the cache action. Better as a separate dedicated PR.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

@hawkff: Thanks for the fix — the MDV_REF → MDVPN_REF rename is exactly the right correction; having the variable name match what the build script reads is important for the pin to actually work. The rationale for deferring SHA-pinning to a dedicated repo-wide PR is well-reasoned and consistent with how the rest of the workflows are handled.

Kicking off a full review now.

✅ Action performed

Full review finished.

hawkff added 2 commits June 16, 2026 17:01
Enable the (previously commented) upload-artifact step so each CI run publishes
the assembled OSS debug APKs (all ABIs) for download — useful for on-device testing.
advancedJson is merged last, so a user could override FD_CONTROL_UNIX_SOCKET /
LISTEN_IP / LISTEN_PORT / PROTOCOL_TYPE / SOCKS5_AUTH and silently break VPN-mode
socket protection or the loopback SOCKS contract. Skip those keys (logged) so the
override can only set tuning fields.
@hawkff

hawkff commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Latest commits since the last review:

  • Protect safety-critical keys (FD_CONTROL_UNIX_SOCKET, LISTEN_IP/PORT, PROTOCOL_TYPE, SOCKS5_AUTH) from advancedJson override.
  • Fixed the workflow ref env name (MDV_REF -> MDVPN_REF) so the fork-ref pin actually drives the build + cache key.
  • Enabled debug-APK artifact upload in CI.

Also: the paired MasterDnsVPN client change now only requires ENCRYPTION_KEY when DATA_ENCRYPTION_METHOD != 0, fixing the default-profile startup case. End-to-end validated against a live server: ChaCha20 tunnel establishes and real traffic egresses through the server.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

@hawkff: Thanks for the detailed update — the safety-critical key guard (FD_CONTROL_UNIX_SOCKET, LISTEN_IP/PORT, PROTOCOL_TYPE, SOCKS5_AUTH) from advancedJson override is the right call for preventing silent VPN-mode breakage. The MDVPN_REF rename, artifact upload enablement, and the paired fork fix for optional ENCRYPTION_KEY all look like solid improvements. Kicking off a full review now.

✅ Action performed

Full review finished.

@hawkff

hawkff commented Jun 16, 2026

Copy link
Copy Markdown
Owner Author

Status on the latest review's inline findings (most are stale against the current head 9c0ba5b):

Already fixed (commit 22b10d5 / fbf4a88, after those comments were posted):

  • 'Release/preview/ci ref pinning ineffective' & 'wire the workflow ref into masterdnsvpn.sh' — fixed: the workflow env is now MDVPN_REF (matching what the script reads, ${MDVPN_REF:-...}), and it's in the cache key. The pin now actually drives the build.
  • 'Use collision-safe temp files' — fixed: config/resolvers files are namespaced by the unique local port.

Declining (documented):

  • 'Pin actions/cache by commit SHA' — every action across all 4 workflows is tag-pinned (checkout@v5, setup-java@v5, setup-go@v6, cache@v5). SHA-pinning is a repo-wide supply-chain hardening change, not specific to this feature; better as a dedicated PR than applied inconsistently to only the cache action here.

The feature is end-to-end validated against a live MasterDnsVPN server (ChaCha20 tunnel over DNS delegation v.nvim.space; real HTTP egress through the server). Leaving this PR open (not merging) per plan.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@buildScript/lib/masterdnsvpn.sh`:
- Around line 24-25: Replace the mutable branch reference in MDVPN_REF with a
specific commit hash to ensure reproducible builds. The variable currently
defaults to "android-vpnservice-protect-hook" which is a branch name that can
change over time without any modifications to this repository's code. Pin it to
a specific commit SHA instead so that the same source code is always used for
builds, improving reproducibility and supply-chain security. Additionally, check
lines 44-54 for any similar mutable references and apply the same commit pinning
approach to those locations as well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3a5d5541-62d8-4e04-838a-8b11ed3d9c0b

📥 Commits

Reviewing files that changed from the base of the PR and between ae7c87c and 9c0ba5b.

📒 Files selected for processing (22)
  • .github/workflows/build.yml
  • .github/workflows/ci.yml
  • .github/workflows/preview.yml
  • .github/workflows/release.yml
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/io/nekohasekai/sagernet/Constants.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
  • app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnBean.java
  • app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnFmt.kt
  • app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/profile/MasterDnsVpnSettingsActivity.kt
  • app/src/main/res/menu/add_profile_menu.xml
  • app/src/main/res/values/arrays.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/xml/masterdnsvpn_preferences.xml
  • buildScript/lib/masterdnsvpn.sh

Comment thread buildScript/lib/masterdnsvpn.sh
hawkff added 4 commits June 16, 2026 19:53
Implement toUri()/parseMasterDnsVpn() so MasterDnsVPN profiles can be
shared and imported via a friendly masterdns:// scheme, in addition to
the existing universal sn://masterdnsvpn link. The encryption key is
carried as URL userinfo; tunnel domains and resolvers as comma-joined
query params; only non-default tuning values are emitted to keep links
short (defaults re-applied on parse via initializeDefaultValues).

- Wire parseMasterDnsVpn into parseProxies dispatch (Formats.kt)
- Wire toUri into ProxyEntity.toStdLink so the standard QR/clipboard
  share menu uses the new scheme
- Register masterdns scheme in the profile-import intent-filter for
  external deep links

QR scanning already routes through RawUpdater.parseRaw -> parseProxies,
so scanning a masterdns:// code now imports directly.
The custom SingBoxOptionSerializer obtained a delegate adapter from the same
Gson instance it was registered on, so serializing a SingBoxOption re-entered
the same type-hierarchy serializer and recursed until StackOverflowError,
surfacing to the user as a 'stack size NNNNKB' connection failure.

Serialize with a plain Gson tree instead, then merge _hack_config_map and
_hack_custom_config recursively over the resulting JSON object (including
nested SingBoxOption fields and collections) without re-entering the custom
adapter.
Two issues prevented the MasterDnsVPN sidecar from coming up in-app:

1. The CGO/PIE Go client could fault (SIGSEGV) during its first protected
   upstream socket dial when launched in the VpnService process context, due
   to the Go runtime's signal-based async preemption. Launch it with
   GODEBUG=asyncpreemptoff=1; the sidecar is I/O-bound so scheduling latency
   is unaffected.

2. The service reported Connected immediately after launching sidecars, so the
   socks outbound could race a listener that had not bound yet. Add
   awaitExternalProcessesReady() to wait for each sidecar's local SOCKS port to
   accept connections first. MasterDnsVPN only listens after MTU probing and
   session setup (tens of seconds, with retries) so it gets a longer readiness
   window than sidecars that bind immediately.
- SingBoxOptionSerializer: also recurse into Map values that are
  SingBoxOption instances (e.g. handshake_for_server_name, fallback_for_alpn)
  so _hack_config_map merging matches the previous delegate-based behavior.
- buildMasterDnsVpnConfig: fail fast when no tunnel domain is configured,
  mirroring the existing encryption-key validation.
- toUri: refuse to build a share link for a profile with no tunnel domain
  instead of emitting an unusable link.
@hawkff

hawkff commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

Update: MasterDnsVPN sidecar now starts reliably under VpnService

Pushed three fixes (CI green, commit 31f3a0c):

1. fix: avoid infinite recursion in sing-box option serializer
SingBoxOptionSerializer fetched a delegate adapter from the same Gson
instance it was registered on, so serializing a SingBoxOption re-entered the
same type-hierarchy serializer and recursed until StackOverflowError (surfaced
to the user as a stack size NNNNKB connection failure). It now serializes with
a plain Gson tree and merges _hack_config_map / _hack_custom_config
recursively over the resulting object — including nested SingBoxOption fields,
collection elements, and map values — without re-entering the custom adapter.

2. fix: reliably start the MasterDnsVPN sidecar under VpnService

  • The CGO/PIE Go client could fault (SIGSEGV) during its first protected
    upstream socket dial when launched in the VpnService process context, due to
    the Go runtime's signal-based async preemption. It is now launched with
    GODEBUG=asyncpreemptoff=1; the sidecar is I/O-bound, so scheduling latency is
    unaffected in practice.
  • The service reported Connected immediately after launching sidecars, so the
    socks outbound could race a listener that had not bound yet.
    awaitExternalProcessesReady() now waits for each sidecar's local SOCKS port
    to accept connections first. MasterDnsVPN only listens after MTU probing and
    session setup (which can take tens of seconds with retries), so it gets a
    longer readiness window than sidecars that bind immediately.

3. fix: address review feedback on serializer and MasterDnsVPN config
Map-value recursion in the serializer, plus fail-fast validation in
buildMasterDnsVpnConfig / toUri when no tunnel domain is configured.

Verified

Connected end-to-end on a physical arm64 device (Android 13): tunnel
established, real egress confirmed through the MasterDnsVPN server, no crash.

Declined review findings

  • Pin MDVPN_REF to a commit SHA (buildScript/lib/masterdnsvpn.sh)
    intentionally tracks the upstream fork branch while that work is in flight;
    will pin once it stabilizes.
  • mdvLocalDnsPort default of 53 — this is the optional local-DNS feature,
    which is disabled by default (localDnsEnabled = false); 53 is the expected
    default when a user enables it.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

@hawkff: Thanks for the detailed update — the safety-critical key guard (FD_CONTROL_UNIX_SOCKET, LISTEN_IP/PORT, PROTOCOL_TYPE, SOCKS5_AUTH) from advancedJson override is the right call for preventing silent VPN-mode breakage. The MDVPN_REF rename, artifact upload enablement, and the paired fork fix for optional ENCRYPTION_KEY all look like solid improvements. Kicking off a full review now.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build.yml:
- Line 12: The MDVPN_REF environment variable currently uses a mutable branch
reference (android-vpnservice-protect-hook) which can change without affecting
the cache keys used at lines 55 and 97, leading to stale artifacts. Replace the
branch name in the MDVPN_REF variable with a specific commit SHA instead of the
branch reference. Ensure this commit SHA is also updated consistently wherever
MDVPN_REF is used in cache operations throughout the workflow.

In `@app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt`:
- Around line 86-87: The new masterDnsVpnBean field added to the ProxyEntity
class modifies the database schema but requires a corresponding version bump and
migration. Locate the Room database version constant (currently at 8) and bump
it to 9. Then create a new migration that handles the schema change from version
8 to 9, which should include an ALTER TABLE statement to add the
masterDnsVpnBean column to the proxy_entities table. Ensure the column is
defined with an appropriate type and nullability to match the masterDnsVpnBean
property definition in ProxyEntity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 458be052-57a3-4f91-92be-e0621d385df3

📥 Commits

Reviewing files that changed from the base of the PR and between ae7c87c and 31f3a0c.

📒 Files selected for processing (25)
  • .github/workflows/build.yml
  • .github/workflows/ci.yml
  • .github/workflows/preview.yml
  • .github/workflows/release.yml
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/io/nekohasekai/sagernet/Constants.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
  • app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnBean.java
  • app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnFmt.kt
  • app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt
  • app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/profile/MasterDnsVpnSettingsActivity.kt
  • app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java
  • app/src/main/res/menu/add_profile_menu.xml
  • app/src/main/res/values/arrays.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/xml/masterdnsvpn_preferences.xml
  • buildScript/lib/masterdnsvpn.sh

Comment thread .github/workflows/build.yml
Comment on lines +86 to 87
var masterDnsVpnBean: MasterDnsVpnBean? = null,
) : Serializable() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ProxyEntity field change =="
rg -n "masterDnsVpnBean|TYPE_MASTERDNSVPN" app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt

echo
echo "== Room database version/migrations =="
fd -a 'SagerDatabase.kt' | xargs -r rg -n "version\\s*=|AutoMigration|autoMigrations"

echo
echo "== Generated schema artifacts mentioning proxy_entities/masterDnsVpnBean (if present) =="
fd -a 'schemas' || true
rg -n "proxy_entities|masterDnsVpnBean" -g "**/*.json" || true

Repository: hawkff/NekoBoxForAndroid

Length of output: 1984


Bump Room schema version and add migration for the new masterDnsVpnBean column.

Line 86 adds masterDnsVpnBean as a persistent ProxyEntity field, which modifies the proxy_entities table schema. However, the database version remains 8 with migrations only up to 7→8. No version bump to 9 or corresponding migration exists, which will cause database open failures with a schema mismatch.

Suggested fix shape
-@Database(..., version = 8, autoMigrations = [
-    ...
-    AutoMigration(from = 7, to = 8)
-])
+@Database(..., version = 9, autoMigrations = [
+    ...
+    AutoMigration(from = 7, to = 8),
+    AutoMigration(from = 8, to = 9)
+])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt` around
lines 86 - 87, The new masterDnsVpnBean field added to the ProxyEntity class
modifies the database schema but requires a corresponding version bump and
migration. Locate the Room database version constant (currently at 8) and bump
it to 9. Then create a new migration that handles the schema change from version
8 to 9, which should include an ALTER TABLE statement to add the
masterDnsVpnBean column to the proxy_entities table. Ensure the column is
defined with an appropriate type and nullability to match the masterDnsVpnBean
property definition in ProxyEntity.

@hawkff

hawkff commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

New commits since the last review:

  • Fixed infinite recursion in the sing-box option serializer (delegate adapter
    re-entered the same type-hierarchy serializer → StackOverflowError surfaced as
    a "stack size NNNNKB" failure). Now serializes via a plain Gson tree and merges
    hack-config recursively over fields, collection elements, and map values.
  • MasterDnsVPN sidecar reliability: launch with GODEBUG=asyncpreemptoff=1 to avoid
    a runtime fault under the VpnService process context, and add
    awaitExternalProcessesReady() so the service waits for each sidecar's local
    SOCKS listener before reporting Connected (with a longer readiness window for
    MasterDnsVPN, which only listens after MTU probing and session setup).
  • Fail-fast validation in buildMasterDnsVpnConfig / toUri when no tunnel domain is
    configured.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

@hawkff: Thanks for the detailed summary of the latest commits — the serializer recursion fix, GODEBUG=asyncpreemptoff=1 launch flag, awaitExternalProcessesReady() readiness gating, and the fail-fast domain validation are all solid improvements. Kicking off a full review now.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java (1)

133-139: 💤 Low value

Consider caching the JsonArray reference.

childTree.getAsJsonArray() is called on each iteration. Extracting it before the loop would be slightly cleaner.

♻️ Optional improvement
 } else if (child instanceof Collection<?> && childTree.isJsonArray()) {
     int index = 0;
+    var childArray = childTree.getAsJsonArray();
     for (Object item : (Collection<?>) child) {
-        if (index >= childTree.getAsJsonArray().size()) break;
-        applyHackConfig(item, childTree.getAsJsonArray().get(index));
+        if (index >= childArray.size()) break;
+        applyHackConfig(item, childArray.get(index));
         index++;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java` around lines 133 -
139, The method childTree.getAsJsonArray() is being called multiple times within
the loop that iterates over the Collection. Extract this JsonArray reference
once before the loop begins and store it in a local variable, then use that
variable inside the loop instead of repeatedly calling getAsJsonArray() on the
conditional check and in the applyHackConfig call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java`:
- Around line 133-139: The method childTree.getAsJsonArray() is being called
multiple times within the loop that iterates over the Collection. Extract this
JsonArray reference once before the loop begins and store it in a local
variable, then use that variable inside the loop instead of repeatedly calling
getAsJsonArray() on the conditional check and in the applyHackConfig call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 12153985-57da-4496-ac2a-4360eddbfffe

📥 Commits

Reviewing files that changed from the base of the PR and between ae7c87c and 31f3a0c.

📒 Files selected for processing (25)
  • .github/workflows/build.yml
  • .github/workflows/ci.yml
  • .github/workflows/preview.yml
  • .github/workflows/release.yml
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/io/nekohasekai/sagernet/Constants.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt
  • app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
  • app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnBean.java
  • app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnFmt.kt
  • app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt
  • app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/profile/MasterDnsVpnSettingsActivity.kt
  • app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java
  • app/src/main/res/menu/add_profile_menu.xml
  • app/src/main/res/values/arrays.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/xml/masterdnsvpn_preferences.xml
  • buildScript/lib/masterdnsvpn.sh

hawkff added 2 commits June 17, 2026 10:54
The masterDnsVpnBean column was added to the proxy_entities entity without a
schema version bump, so the exported schema and database version no longer
matched the entity. Bump the database version 8 -> 9 and add AutoMigration(8, 9)
so the new column is added in place, and commit the generated 9.json schema.
Without this, existing installs fall back to a destructive rebuild (losing saved
profiles) on upgrade.
The masterDnsVpnBean column was added to the ProxyEntity in an earlier commit
without ever updating the exported v8 schema, so existing installs already carry
the column at schema version 8. The explicit AutoMigration(8,9) then issued
ALTER TABLE ADD COLUMN masterDnsVpnBean, which fails with 'duplicate column' on
those databases and crash-loops the app on launch.

Keep database version 9 (and 9.json) but remove the explicit 8->9 automigration
so the existing fallbackToDestructiveMigration() handles the 8->9 transition,
which rebuilds cleanly regardless of whether the column already exists. Verified
on-device against a simulated v8-with-column database: no duplicate-column error,
app launches, DB rebuilt to v9.
Comment thread buildScript/lib/masterdnsvpn.sh
// opens its local SOCKS listener. Disabling async preemption avoids
// the fault; the sidecar workload is I/O-bound so scheduling latency
// is unaffected in practice.
val env = mutableMapOf("GODEBUG" to "asyncpreemptoff=1")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 GODEBUG=asyncpreemptoff=1 disables preemption process-wide, not just during the protected dial

This env var turns off Go's signal-based goroutine preemption for the entire libmasterdnsvpn.so process lifetime, including all subsequent goroutines (e.g., the SOCKS listener, data forwarding loops). The stated reason is a fault during the first protected socket dial, but this mitigation is very broad. If the root cause is a signal-safety issue specific to the cgo/VpnService boundary during FD_CONTROL_UNIX_SOCKET sends, it would be worth confirming via GOTRACEBACK=crash logs rather than masking it globally — future Go releases may tighten runtime requirements in ways that asyncpreemptoff can't paper over.

Comment on lines +299 to +335
suspend fun awaitExternalProcessesReady() {
if (!::processes.isInitialized || processes.processCount == 0) return
val ports = config.externalIndex.flatMap { it.chain.keys }.distinct()
if (ports.isEmpty()) return

val hasMasterDnsVpn = config.externalIndex.any { idx ->
idx.chain.values.any { it.requireBean() is MasterDnsVpnBean }
}
val readinessTimeoutMs = if (hasMasterDnsVpn) {
maxOf(60_000L, DataStore.connectionTestTimeout.toLong())
} else {
maxOf(1_000L, DataStore.connectionTestTimeout.toLong())
}

withContext(Dispatchers.IO) {
val deadline = SystemClock.elapsedRealtime() + readinessTimeoutMs
val pending = ports.toMutableSet()
while (pending.isNotEmpty() && SystemClock.elapsedRealtime() < deadline) {
val iterator = pending.iterator()
while (iterator.hasNext()) {
val port = iterator.next()
try {
Socket().use {
it.connect(InetSocketAddress(LOCALHOST, port), 100)
}
iterator.remove()
} catch (_: IOException) {
// not ready yet
}
}
if (pending.isNotEmpty()) delay(50)
}
if (pending.isNotEmpty()) {
throw IOException("sidecar listener not ready on port(s): ${pending.joinToString()}")
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 awaitExternalProcessesReady with a hard 1-second floor is a new failure mode for all existing sidecar protocols

Before this PR, startProcesses launched sidecars and returned immediately. Now awaitExternalProcessesReady blocks until every external port is connectable or the timeout fires, and then throws IOException. For Mieru, NaïVe, TrojanGo, and Hysteria profiles the effective floor is maxOf(1_000, connectionTestTimeout). On a busy low-end device those sidecars can occasionally take a few seconds to bind; previously that was harmless (the first sing-box dial would retry), but now it becomes a hard VPN-start failure. Consider a graceful path (log a warning rather than throw) for non-MasterDnsVPN sidecar timeout, or at least document the new failure contract.

@hawkff hawkff mentioned this pull request Jun 18, 2026
4 tasks
hawkff added a commit that referenced this pull request Jun 18, 2026
Clarify that TYPE_AWG=26 deliberately skips 25, which is reserved for the
MasterDnsVPN sidecar type on PR #18, so persisted type IDs stay stable when
both branches merge. (Greptile)
…te resolvers

Addresses CodeRabbit/Greptile review on PR #18:
- Pin all actions/cache@v5 to commit 27d5ce7 (v5.0.5) across build/ci/preview/
  release workflows. (CodeRabbit Major, policy)
- Pin the MasterDnsVPN sidecar source to an immutable commit: masterdnsvpn.sh
  now honors MDVPN_COMMIT (default 7750c77) and fetches it explicitly (git
  init + fetch <sha> + checkout FETCH_HEAD, since --branch rejects a SHA). The
  workflows set MDVPN_COMMIT and key the sidecar cache on it for integrity.
  (CodeRabbit Major, Greptile P1)
- buildMasterDnsVpn now fails fast when no resolvers are configured, matching
  the existing domain/key validation, instead of writing an empty resolvers
  file the sidecar rejects at runtime. (Greptile P2)
@hawkff

hawkff commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Pushed e40bd18 addressing the live review findings:

Fixed:

  • actions/cache@v5 unpinned (CodeRabbit Major): all 20 occurrences across build/ci/preview/release now pinned to actions/cache@27d5ce7… (v5.0.5).
  • MasterDnsVPN source on a mutable branch (CodeRabbit Major / Greptile P1): masterdnsvpn.sh now honors MDVPN_COMMIT (default 7750c77) and fetches that exact commit (git init + fetch <sha> + checkout FETCH_HEAD, since git clone --branch rejects a SHA). Workflows set MDVPN_COMMIT and the sidecar cache key now uses it, so the cache is commit-resolved.
  • Empty resolver list not validated (Greptile P2): buildMasterDnsVpn now fails fast with "at least one resolver is required", matching the existing domain/key validation, instead of writing an empty resolvers file.

Not changed (with rationale):

  • GODEBUG=asyncpreemptoff=1 'process-wide' (Greptile P2): it is not process-wide — it's set in a per-subprocess env map passed to processes.start(commands, env), so it only affects the MasterDnsVPN sidecar's own Go runtime, not the NekoBox app or other sidecars. The comment explains the SIGSEGV-under-VpnService rationale; the sidecar is I/O-bound so scheduling latency is unaffected.
  • Collision-safe temp files (earlier finding): already addressed — the sidecar artifacts use ${port}_${elapsedRealtime()} (port is unique per chain entry), see the comment at BoxInstance.kt:252.
  • awaitExternalProcessesReady 1s floor: intentional minimum readiness window; the sidecar opens its SOCKS listener well within it in practice, and the wait is condition-based (socket-connect) with the floor only as a lower bound.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

@hawkff: Thanks for the thorough update — the three fixes all look well-targeted:

  • actions/cache SHA-pinning across all 20 occurrences is the right supply-chain hardening approach, and doing it consistently across all four workflows is cleaner than a partial fix.
  • MDVPN_COMMIT-based fetch (git init + fetch <sha> + checkout FETCH_HEAD) correctly handles the fact that git clone --branch rejects bare SHAs — and wiring it into the cache key means the cache properly invalidates on commit change.
  • Empty resolver validation matching the existing domain/key fail-fast pattern keeps the error surface consistent.

The rationale on the non-changes is well-documented — GODEBUG being per-subprocess env, the port-namespaced temp files, and the intentional 1s readiness floor are all clear. Kicking off a full review now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt (1)

19-26: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add a non-destructive 8→9 migration before shipping.

Bumping to version = 9 without an 8 -> 9 migration means upgrades from v8 will hit fallbackToDestructiveMigration() (Line 42), wiping user data (proxy_entities, groups, rules). Please add a guarded manual migration for masterDnsVpnBean (check column existence via PRAGMA table_info before ALTER TABLE) and register it in the builder.

Suggested direction
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
...
 abstract class SagerDatabase : RoomDatabase() {
     companion object {
+        private val MIGRATION_8_9 = object : Migration(8, 9) {
+            override fun migrate(database: SupportSQLiteDatabase) {
+                var hasColumn = false
+                database.query("PRAGMA table_info(`proxy_entities`)").use { c ->
+                    val nameIdx = c.getColumnIndex("name")
+                    while (c.moveToNext()) {
+                        if (c.getString(nameIdx) == "masterDnsVpnBean") {
+                            hasColumn = true
+                            break
+                        }
+                    }
+                }
+                if (!hasColumn) {
+                    database.execSQL(
+                        "ALTER TABLE `proxy_entities` ADD COLUMN `masterDnsVpnBean` BLOB"
+                    )
+                }
+            }
+        }
...
             Room.databaseBuilder(SagerNet.application, SagerDatabase::class.java, Key.DB_PROFILE)
+                .addMigrations(MIGRATION_8_9)
                 .setJournalMode(JournalMode.TRUNCATE)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt` around
lines 19 - 26, The database version has been bumped to 9 without a corresponding
migration from version 8, which will trigger a destructive migration and wipe
user data. Add a new AutoMigration entry in the autoMigrations array for version
8 to 9, or implement a manual migration that safely adds the new
masterDnsVpnBean column. The migration should use PRAGMA table_info to check if
the masterDnsVpnBean column already exists before attempting to execute the
ALTER TABLE statement, ensuring the migration is non-destructive and handles
edge cases gracefully.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Line 34: The cache steps at lines 34, 57, 95, 104, and 120 using actions/cache
can write caches during pull_request events, which creates a security
vulnerability where untrusted PR content can poison caches used by trusted runs.
For each actions/cache usage, split the operation into two separate steps: use
actions/cache/restore to restore the cache on all events, and use
actions/cache/save to save the cache only on trusted events (add `if:
github.event_name == 'push'` condition to the save step). This prevents pull
requests from writing to the cache while still allowing cache restoration during
PR runs.

---

Outside diff comments:
In `@app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt`:
- Around line 19-26: The database version has been bumped to 9 without a
corresponding migration from version 8, which will trigger a destructive
migration and wipe user data. Add a new AutoMigration entry in the
autoMigrations array for version 8 to 9, or implement a manual migration that
safely adds the new masterDnsVpnBean column. The migration should use PRAGMA
table_info to check if the masterDnsVpnBean column already exists before
attempting to execute the ALTER TABLE statement, ensuring the migration is
non-destructive and handles edge cases gracefully.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8fc55d61-65fd-4e4e-a1f3-38652b1bbe2c

📥 Commits

Reviewing files that changed from the base of the PR and between 31f3a0c and e40bd18.

📒 Files selected for processing (8)
  • .github/workflows/build.yml
  • .github/workflows/ci.yml
  • .github/workflows/preview.yml
  • .github/workflows/release.yml
  • app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/9.json
  • app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnFmt.kt
  • buildScript/lib/masterdnsvpn.sh
✅ Files skipped from review due to trivial changes (1)
  • app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/9.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/io/nekohasekai/sagernet/fmt/masterdnsvpn/MasterDnsVpnFmt.kt
  • buildScript/lib/masterdnsvpn.sh

Comment thread .github/workflows/ci.yml
- name: LibCore Cache
id: cache
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden cache steps against PR-based cache poisoning.

These actions/cache steps can write caches during pull_request runs, which can let untrusted PR content influence caches later restored by trusted runs. Split restore/save and gate save to trusted events only.

Suggested pattern (apply to each cache target)
-      - name: Sidecars Cache
-        id: cache
-        uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+      - name: Sidecars Cache (restore)
+        id: cache
+        uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
         with:
           path: |
             app/executableSo
           key: ${{ hashFiles('.github/workflows/*', 'sidecars_status') }}-${{ env.MIERU_VERSION }}-${{ env.HYSTERIA_VERSION }}-${{ env.MDVPN_COMMIT }}-sidecars-ci
+
+      - name: Sidecars Cache (save)
+        if: github.event_name != 'pull_request' && steps.cache.outputs.cache-hit != 'true'
+        uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+        with:
+          path: |
+            app/executableSo
+          key: ${{ steps.cache.outputs.cache-primary-key }}

Also applies to: 57-57, 95-95, 104-104, 120-120

🧰 Tools
🪛 zizmor (1.25.2)

[error] 34-34: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default

(cache-poisoning)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 34, The cache steps at lines 34, 57, 95,
104, and 120 using actions/cache can write caches during pull_request events,
which creates a security vulnerability where untrusted PR content can poison
caches used by trusted runs. For each actions/cache usage, split the operation
into two separate steps: use actions/cache/restore to restore the cache on all
events, and use actions/cache/save to save the cache only on trusted events (add
`if: github.event_name == 'push'` condition to the save step). This prevents
pull requests from writing to the cache while still allowing cache restoration
during PR runs.

Source: Linters/SAST tools

@hawkff

hawkff commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

Re: cache-poisoning hardening (split actions/cache into gated restore/save) — valid finding, acknowledged. Deferring it as a separate follow-up rather than folding it into this PR, because:

  1. It's a cross-cutting CI security change that affects every cache step in all workflows (build/ci/preview/release) and exists on main too — it's not specific to the MasterDnsVPN sidecar work this PR is about.
  2. Restructuring every cache step (restore + conditionally-gated save) changes caching behavior repo-wide and deserves its own focused PR + CI validation rather than riding along here.

This PR's scope was the MasterDnsVPN sidecar; the cache pinning (immutable SHA) is done. The restore/save split will be tracked separately so it can be applied uniformly across all workflows and branches in one reviewed change.

hawkff added 2 commits June 18, 2026 01:11
Picks up the protected-UDP-resolver context propagation fix on the
MasterDnsVPN fork branch, so the sidecar build uses the cancellation-aware
dial path.
# Conflicts:
#	.github/workflows/build.yml
#	.github/workflows/ci.yml
#	.github/workflows/preview.yml
#	.github/workflows/release.yml
#	app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/9.json
#	app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
#	app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt
#	app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java
@hawkff

hawkff commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 17-18: The `contents: write` permission is currently set at the
workflow level, which grants write access to all jobs unnecessarily. Remove the
`permissions: contents: write` section from the workflow-level configuration and
instead add it only to the specific job that handles release publication. This
ensures only the job that actually publishes the release has write access to
repository contents, following the principle of least privilege.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49441cfd-c130-48b6-952a-50c3a13b821d

📥 Commits

Reviewing files that changed from the base of the PR and between e40bd18 and 06d00f8.

📒 Files selected for processing (15)
  • .github/workflows/build.yml
  • .github/workflows/ci.yml
  • .github/workflows/preview.yml
  • .github/workflows/release.yml
  • app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/9.json
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
  • app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
  • app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  • app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java
  • app/src/main/res/menu/add_profile_menu.xml
  • app/src/main/res/values/strings.xml
  • buildScript/lib/masterdnsvpn.sh
🚧 Files skipped from review as they are similar to previous changes (9)
  • app/src/main/res/menu/add_profile_menu.xml
  • app/src/main/java/io/nekohasekai/sagernet/plugin/PluginManager.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt
  • app/src/main/res/values/strings.xml
  • app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java
  • app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt
  • buildScript/lib/masterdnsvpn.sh

Comment thread .github/workflows/release.yml Outdated
Workflow-level contents:write granted a write-capable token to every job.
Default to read-only and grant write only to the publish job that uploads the
release. (CodeRabbit)
@hawkff

hawkff commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

Addressed the workflow-permissions finding in f702e80: release.yml now defaults to contents: read at the workflow level, with contents: write scoped to the publish job only.

On the cache-poisoning hardening (split actions/cache into gated restore/save): deferring to a dedicated follow-up PR rather than bundling it here. It's a cross-cutting change across all 20 cache steps in build/ci/preview/release, and these caches feed if: steps.cache.outputs.cache-hit guards on subsequent build steps — restructuring them needs its own focused PR + CI validation so it can be applied uniformly without breaking the cache-hit gating. This PR's scope is the MasterDnsVPN sidecar + the main-merge conflict resolution. Tracking the cache hardening separately.

@hawkff hawkff merged commit 3f82b8e into main Jun 18, 2026
4 of 5 checks passed
hawkff added a commit that referenced this pull request Jun 18, 2026
awaitExternalProcessesReady previously threw IOException (hard VPN-start
failure) for any sidecar that didn't bind within maxOf(1s, connectionTestTimeout).
MasterDnsVPN genuinely needs that gate (it crashed if dialed before binding),
but Mieru/Naive/TrojanGo/Hysteria were historically fire-and-forget with
first-dial retry. Now only MasterDnsVPN throws on timeout; the others log a
warning and continue. (Greptile, PR #18 follow-up)
@hawkff hawkff deleted the feature/masterdnsvpn-sidecar branch June 23, 2026 00:45
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