feat: Hysteria2 Gecko obfs via bundled official-binary sidecar#17
Conversation
Adds Hysteria2 'Gecko' obfuscation by bundling the official apernet/hysteria Android client as a sidecar, instead of re-platforming the sing-box core. - HysteriaBean: add hysteria2ObfsType (none/salamander/gecko) + gecko min/max packet size; kryo version 7->8 with backward-compatible deserialize (old HY2 profiles with a password derive as Salamander). obfuscation stays the password. - HysteriaFmt: canUseSingBox() returns false for HY2+Gecko (forces the sidecar); native HY2 outbound still emits Salamander; URI parse/export handle obfs type + gecko packet sizes; new buildHysteria2SidecarConfig() emits official hysteria client JSON with obfs.gecko + quic.sockopts.fdControlUnixSocket (reuses libcore's existing protect_path server) + loopback SOCKS5. - BoxInstance: init/launch branches run the bundled libhysteria2.so (hysteria client --config <file>) for HY2+Gecko; Salamander/HY1 unchanged. - buildScript/lib/hysteria2.sh: download official hysteria-android-* (v2.9.2) as app/executableSo/<abi>/libhysteria2.so (all 4 ABIs). - Executable: add libhysteria2.so to the kill-set. - UI: obfs type selector + gecko min/max packet-size fields (HY2 only). - Routing reuses the existing external-proxy machinery (pluginId hysteria2-plugin). No sing-box core change; starifly core + SSR/Snell/Juicity untouched.
Combine the per-workflow Mieru native-build job into a 'sidecars' job that builds both libmieru.so (cross-compiled) and libhysteria2.so (downloaded from apernet/hysteria v2.9.2) into app/executableSo, cached as one unit and restored in the build job with a verify-all-ABIs-and-both-binaries guard. Adds HYSTERIA_VERSION env. Applied across ci/build/preview/release workflows.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
📝 WalkthroughSummary by CodeRabbitRelease Notes
WalkthroughAdds Hysteria2 Gecko obfuscation support to NekoBox for Android via a bundled ChangesApp: Hysteria2 Gecko Obfuscation Sidecar
CI/Build Pipeline: Sidecars Job
Sequence Diagram(s)sequenceDiagram
participant User
participant HysteriaSettingsActivity
participant HysteriaBean
participant BoxInstance
participant buildHysteria2SidecarConfig
participant hysteria2_plugin
rect rgba(70, 130, 180, 0.5)
note over User, HysteriaSettingsActivity: Profile Configuration
User->>HysteriaSettingsActivity: select Gecko obfs type + min/max packet
HysteriaSettingsActivity->>HysteriaBean: serialize fields (hysteria2ObfsType, geckoMin/MaxPacketSize)
end
rect rgba(34, 139, 34, 0.5)
note over BoxInstance, hysteria2_plugin: Connection Launch
BoxInstance->>HysteriaBean: protocolVersion==2 && OBFS_GECKO?
HysteriaBean-->>BoxInstance: true
BoxInstance->>buildHysteria2SidecarConfig: port, protectPath
buildHysteria2SidecarConfig-->>BoxInstance: JSON config (TLS, Gecko obfs, SOCKS5 listener)
BoxInstance->>hysteria2_plugin: write hysteria2_*.json to cache dir
BoxInstance->>hysteria2_plugin: exec --config --log-level --disable-update-check mode=client
hysteria2_plugin-->>BoxInstance: process started
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
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 full review Please focus on: Hysteria2 obfs-type backward compatibility (kryo v7->8 deserialize for old Salamander/no-obfs profiles), the sidecar config correctness (obfs.gecko + fdControlUnixSocket protect path), routing (canUseSingBox false for Gecko -> external socks outbound), and binary integrity (SHA256 verification against hashes.txt). Note on a likely finding: the obfs password field is shown for BOTH Salamander AND Gecko (type != OBFS_NONE). This is correct — Gecko obfs REQUIRES a password (apernet/hysteria clientConfigObfsGecko.Password / WrapPacketConnGecko uses it), same as Salamander. The gecko min/max packet-size fields already use EditTextPreferenceModifiers.Number (the codebase's numeric-input pattern), consistent with the other numeric prefs on this screen. |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt (1)
356-362: ⚡ Quick winRestrict native HY2 obfs emission to explicit Salamander type.
This block always writes
type = "salamander", but the guard currently allows any non-NONEtype. Tightening the condition keeps this method self-consistent and avoids silent mis-encoding if routing conditions change.Suggested patch
- if (bean.obfuscation.isNotBlank() && bean.hysteria2ObfsType != HysteriaBean.OBFS_NONE) { + if (bean.obfuscation.isNotBlank() && bean.hysteria2ObfsType == HysteriaBean.OBFS_SALAMANDER) { // Native sing-box core only supports Salamander; Gecko goes through the // bundled hysteria binary sidecar (canUseSingBox() == false for Gecko). obfs = SingBoxOptions.Hysteria2Obfs().apply { type = "salamander" password = bean.obfuscation } }🤖 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/fmt/hysteria/HysteriaFmt.kt` around lines 356 - 362, The guard condition for the obfuscation block checks only that bean.hysteria2ObfsType is not OBFS_NONE, but the code inside always sets type to "salamander". Tighten the condition to explicitly check that bean.hysteria2ObfsType equals the Salamander constant (e.g., HysteriaBean.OBFS_SALAMANDER or equivalent) instead of just checking for non-NONE values. This ensures the native sing-box core only emits Salamander obfuscation when that specific type is configured, avoiding silent mis-encoding if other obfs types are set.
🤖 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:
- Around line 48-53: Pin the mutable version tags for GitHub Actions to
immutable full commit SHAs to improve supply-chain security across all workflow
files. In `.github/workflows/ci.yml` at lines 48-53, replace
`actions/checkout@v5` with its full commit SHA and `actions/cache@v5` with its
full commit SHA. Apply the same changes in `.github/workflows/build.yml` at
lines 43-49 for both `actions/checkout@v5` and `actions/cache@v5`. Similarly,
update both action references in `.github/workflows/preview.yml` at lines 39-44
to use full commit SHAs instead of version tags. Finally, make the same
replacements for both actions in `.github/workflows/release.yml` at lines 45-50.
Use the format `uses: owner/repo@<full-40-character-commit-sha>` for each
action.
- Around line 43-48: The sidecars jobs in multiple workflow files lack
least-privilege security controls. In `.github/workflows/ci.yml` lines 43-48,
`.github/workflows/build.yml` lines 38-43, `.github/workflows/preview.yml` lines
34-39, and `.github/workflows/release.yml` lines 40-45, add a `permissions`
block at the job level set to `{ contents: read }` (or tighter scope if
applicable), and modify the `actions/checkout` step to include
`persist-credentials: false`. This prevents overly permissive token handling
when running scripts that follow the checkout action.
In `@app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt`:
- Around line 106-111: Add validation checks for the gecko packet-size
parameters across all three affected locations in HysteriaFmt.kt. After parsing
obfs-min-packet-size into geckoMinPacketSize and obfs-max-packet-size into
geckoMaxPacketSize (lines 106-111), verify that both values are positive
(greater than zero) and that the minimum does not exceed the maximum; if
validation fails, either skip invalid values or log an error to prevent sidecar
config failures. Apply identical validation logic at lines 178-179 and lines
432-439 where similar packet-size parameters are parsed and assigned.
In `@buildScript/lib/hysteria2.sh`:
- Around line 20-21: The curl commands used to download the hashes file and
binary assets lack resilience to transient network issues. Add retry and timeout
controls to both curl invocations: first, enhance the curl command on line 20
that fetches the hashes.txt file by adding the flags --retry 3
--retry-all-errors --connect-timeout 15 and --max-time to set appropriate
timeouts; second, apply the same retry and timeout flags to the curl command on
line 31 that downloads the binary assets to ensure both external downloads are
protected against flaky network conditions.
In `@docs/maintenance/hysteria2-gecko-sidecar.md`:
- Around line 68-76: The CI documentation section currently describes adding a
separate "Native Build (Hysteria2)" job, but according to the PR summary,
Hysteria2 should share the combined sidecars workflow with libmieru.so instead
of having its own pipeline. Update the CI bullet point to reflect this actual
workflow structure by replacing the reference to a separate "Native Build
(Hysteria2)" job with wording that indicates Hysteria2 is integrated into the
combined sidecars workflow alongside libmieru.so.
- Around line 78-82: Update the launch() section in the BoxInstance wiring
documentation to reflect the actual command-line flags used by the
hysteria2-plugin sidecar. Replace the example command that shows
`libhysteria2.so client -c <file>` with the correct invocation that includes the
actual sidecar flags: `--disable-update-check --config <file> --log-level ...
client`. This ensures the documentation accurately represents the implementation
rather than the official CLI contract, preventing readers from copying incorrect
flag syntax.
---
Nitpick comments:
In `@app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt`:
- Around line 356-362: The guard condition for the obfuscation block checks only
that bean.hysteria2ObfsType is not OBFS_NONE, but the code inside always sets
type to "salamander". Tighten the condition to explicitly check that
bean.hysteria2ObfsType equals the Salamander constant (e.g.,
HysteriaBean.OBFS_SALAMANDER or equivalent) instead of just checking for
non-NONE values. This ensures the native sing-box core only emits Salamander
obfuscation when that specific type is configured, avoiding silent mis-encoding
if other obfs types are set.
🪄 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: 1b327831-862f-4c0a-8862-eff62824bccf
📒 Files selected for processing (16)
.github/workflows/build.yml.github/workflows/ci.yml.github/workflows/preview.yml.github/workflows/release.ymlapp/src/main/java/io/nekohasekai/sagernet/Constants.ktapp/src/main/java/io/nekohasekai/sagernet/bg/Executable.ktapp/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.ktapp/src/main/java/io/nekohasekai/sagernet/database/DataStore.ktapp/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.javaapp/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.ktapp/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.ktapp/src/main/res/values/arrays.xmlapp/src/main/res/values/strings.xmlapp/src/main/res/xml/hysteria_preferences.xmlbuildScript/lib/hysteria2.shdocs/maintenance/hysteria2-gecko-sidecar.md
- buildHysteria2SidecarConfig: clamp gecko min/max to hysteria's accepted bounds (1 <= min <= max <= 2048) to avoid emitting an invalid config. - Document why the obfs password field is shown for both Salamander and Gecko (Gecko requires a password too).
|
@coderabbitai review Addressed review feedback:
Declined (with rationale): the suggestion to hide the obfs password for Gecko is incorrect — Gecko obfs REQUIRES a password (apernet/hysteria clientConfigObfsGecko.Password is mandatory; WrapPacketConnGecko uses it), same as Salamander. The gecko min/max fields already use the codebase's EditTextPreferenceModifiers.Number numeric-input pattern (consistent with the screen's other numeric prefs), so a separate android:inputType in XML is redundant. Local coderabbit CLI review is now clean (0 findings). Builds (debug+release) pass with libhysteria2.so packaged for all 4 ABIs. |
|
Triggering a fresh review pass now. ✅ Action performedReview finished.
|
…c accuracy - Add top-level permissions blocks: contents:read for ci/build/preview, contents:write for release (it publishes via ghr). Caps the default GITHUB_TOKEN scope for the jobs that run ./run sidecar scripts. - hysteria2.sh: add curl --retry/--max-time to the hashes.txt fetch and binary downloads. - Fix the design doc to describe the combined 'sidecars' CI job and the actual sidecar launch flags (--disable-update-check --config <file> --log-level <level> client).
Summary
Adds Hysteria2 Gecko obfuscation by bundling the official
apernet/hysteriaAndroid client binary as a sidecar — instead of re-platforming the sing-box core.
Keeps the starifly core (and SSR/Snell/Juicity) fully intact. Days-scale vs the
2-8 week core migration. (See
docs/maintenance/hysteria2-gecko-sidecar.md.)How it works
core (1.12.x) can't do it. So for HY2 + Gecko profiles,
canUseSingBox()returnsfalse and the existing external-proxy machinery routes a sing-box
socksoutbound to abundled
libhysteria2.sosidecar. Non-obfs and Salamander HY2 stay on the nativesing-box outbound (unchanged).
quic.sockopts.fdControlUnixSocket, pointed at libcore's existingprotect_serversocket (protect_path). No new bridge, no upstream patch.Changes
hysteria2ObfsType(none/salamander/gecko) + gecko min/max packetsize; kryo serialize version 7->8 with backward-compatible deserialize (old HY2 profiles
with a password derive as Salamander;
obfuscationremains the password). No Room schema change.canUseSingBox()false for HY2+Gecko; native HY2 outbound still emitsSalamander; URI parse/export handle
obfs=gecko+obfs-min/max-packet-size; newbuildHysteria2SidecarConfig()emits official hysteria client JSON.libhysteria2.so client --config <file>for HY2+Gecko.hysteria-android-*v2.9.2 asapp/executableSo/<abi>/libhysteria2.so(all 4 ABIs), verified against the officialhashes.txt SHA256.
sidecarsjob builds+caches both libmieru.so and libhysteria2.so;build job verifies all ABIs/both binaries.
Verification (AWS Linux builder)
./run lib hysteria2downloads + SHA256-verifies all 4 ABI binaries.assembleOssDebug+assembleOssRelease(R8) BUILD SUCCESSFUL;libhysteria2.soconfirmed packaged in the APK for all ABIs. No
app/schemaschange.Not yet done (follow-up)
quota now approved).
Scope
No sing-box core / Go change. starifly core + SSR/Snell/Juicity untouched.