Skip to content

Add opt-in Hysteria 2 ECH profiles - #152

Merged
hawkff merged 3 commits into
mainfrom
feat/hysteria2-ech
Jul 13, 2026
Merged

hawkff merged 3 commits into
mainfrom
feat/hysteria2-ech

Conversation

@hawkff

@hawkff hawkff commented Jul 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • add an opt-in ECH section to Hysteria 2 profiles
  • import and export the official tls.ech JSON field and ech URI parameter
  • validate ECHConfigList framing before saving or generating a core configuration
  • convert raw base64 or ECH CONFIGS PEM input to the static PEM form expected by sing-box
  • keep existing profiles and Hysteria 1 behavior unchanged

Validation

  • Namespace CI run 29260118847: all jobs passed
  • CodeRabbit CLI review: no findings
  • Android device validation pending device availability

Greptile Summary

This PR adds opt-in ECH support for Hysteria 2 profiles. The main changes are:

  • New ECH fields in Hysteria profile storage.
  • URI and JSON import/export for Hysteria 2 ECH config.
  • Validation and canonicalization for ECHConfigList input.
  • UI preferences for enabling ECH and entering the config.
  • sing-box outbound generation for static ECH config.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt Adds ECH parsing, validation, URI export, JSON import, and sing-box config output.
app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java Adds persisted ECH fields with defaults for existing profiles.
app/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.kt Adds Hysteria 2 ECH UI state, visibility handling, and save-time validation.
app/src/main/res/xml/hysteria_preferences.xml Adds the Hysteria 2 ECH preference section.

Reviews (3): Last reviewed commit: "fix(hysteria): validate missing ECH payl..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Hysteria2 ECH support adds persisted profile fields, version 9 serialization, canonical configuration parsing, settings UI controls and validation, URI/JSON conversion, sing-box outbound generation, localized strings, and compatibility tests.

Changes

Hysteria2 ECH

Layer / File(s) Summary
ECH profile persistence and serialization
app/src/main/java/io/nekohasekai/sagernet/Constants.kt, app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt, app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java
Adds ECH storage keys and profile properties, initializes disabled defaults, and serializes ECH fields using format version 9 with legacy version 8 defaults.
ECH parsing and outbound conversion
app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt
Canonicalizes raw or PEM ECH configurations, preserves URI + characters, parses URI and JSON ECH values, emits URI parameters, and configures sing-box Hysteria2 TLS ECH.
ECH settings and validation
app/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.kt, app/src/main/res/xml/hysteria_preferences.xml, app/src/main/res/values/strings.xml, app/src/main/res/values-zh-rCN/strings.xml
Adds ECH preferences and localized text, synchronizes values with DataStore, updates visibility by protocol and enablement, and blocks saving invalid enabled configurations.
ECH behavior and compatibility tests
app/src/test/java/io/nekohasekai/sagernet/fmt/ConfigBuilderGoldenTest.kt, app/src/test/java/io/nekohasekai/sagernet/fmt/HysteriaBeanSerializationTest.kt, app/src/test/java/io/nekohasekai/sagernet/fmt/HysteriaFmtTest.kt
Tests default and legacy serialization behavior, URI/JSON conversion, canonicalization, outbound generation, invalid inputs, disabled ECH, and Hysteria1 behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant HysteriaSettingsActivity
  participant HysteriaBean
  participant HysteriaFmt
  participant SingBoxOutbound
  User->>HysteriaSettingsActivity: Enable ECH and enter config
  HysteriaSettingsActivity->>HysteriaFmt: Validate and canonicalize config
  HysteriaSettingsActivity->>HysteriaBean: Persist ECH fields
  HysteriaBean->>HysteriaFmt: Provide Hysteria2 ECH settings
  HysteriaFmt->>SingBoxOutbound: Build enabled TLS ECH config
Loading

Possibly related PRs

Poem

A bunny hops through ECH config bright,
Base64 wrapped in PEM moonlight.
Old profiles rest, safely unchanged,
New Hysteria paths are neatly arranged.
Tests thump their paws: “All clear!” 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding opt-in Hysteria 2 ECH profile support.
Description check ✅ Passed The description matches the changeset and clearly describes the added Hysteria 2 ECH import, export, validation, and UI support.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@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 (2)
app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt (1)

27-54: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Nullable echConfig vs. non-null String parameter.

canonicalHysteria2ECHConfig(config: String) takes a non-null Kotlin String, but HysteriaBean.echConfig (Java field) is nullable. Every current call site pairs enableECH = true with an already-validated, non-null echConfig, so this isn't reachable today — but if a future code path sets enableECH = true directly on a bean without routing through parseHysteria2/parseHysteria2Json/the validated settings-save flow, bean.echConfig could still be null, and this call would throw a raw NullPointerException (Kotlin's intrinsic null-check) instead of the intended IllegalArgumentException with a descriptive message.

Consider accepting String? and failing with the same descriptive IllegalArgumentException for a null config, for a more robust contract.

Also applies to: 623-628

🤖 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 27 - 54, Update canonicalHysteria2ECHConfig to accept a nullable String
and explicitly reject null with the existing descriptive
IllegalArgumentException used for missing ECH configuration. Preserve the
current trimming, validation, decoding, and canonicalization behavior for
non-null values, including the HysteriaBean.echConfig call path.
app/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.kt (1)

49-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Validation swallows the specific failure reason.

runCatching { canonicalHysteria2ECHConfig(...) }.isSuccess discards the exception's message, so the user always gets the same generic hysteria2_ech_config_invalid toast regardless of why the config was rejected (bad base64, incomplete PEM, malformed ECHConfigList, etc.), making it harder to fix the input.

Proposed improvement to surface the failure reason
     override suspend fun saveAndExit() {
         if (DataStore.protocolVersion == 2 && DataStore.serverHy2EchEnabled) {
-            val isValid = runCatching {
+            val result = runCatching {
                 canonicalHysteria2ECHConfig(DataStore.serverHy2EchConfig)
-            }.isSuccess
-            if (!isValid) {
+            }
+            if (result.isFailure) {
                 onMainDispatcher {
                     Toast.makeText(
                         this@HysteriaSettingsActivity,
-                        R.string.hysteria2_ech_config_invalid,
+                        result.exceptionOrNull()?.message ?: getString(R.string.hysteria2_ech_config_invalid),
                         Toast.LENGTH_LONG,
                     ).show()
                 }
                 return
             }
         }
         super.saveAndExit()
     }
🤖 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/ui/profile/HysteriaSettingsActivity.kt`
around lines 49 - 66, Update saveAndExit in HysteriaSettingsActivity to retain
the exception from canonicalHysteria2ECHConfig instead of reducing validation to
isSuccess. Include the captured failure reason in the invalid-configuration
Toast while preserving the existing early return and successful
super.saveAndExit flow.
🤖 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/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt`:
- Around line 27-54: Update canonicalHysteria2ECHConfig to accept a nullable
String and explicitly reject null with the existing descriptive
IllegalArgumentException used for missing ECH configuration. Preserve the
current trimming, validation, decoding, and canonicalization behavior for
non-null values, including the HysteriaBean.echConfig call path.

In
`@app/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.kt`:
- Around line 49-66: Update saveAndExit in HysteriaSettingsActivity to retain
the exception from canonicalHysteria2ECHConfig instead of reducing validation to
isSuccess. Include the captured failure reason in the invalid-configuration
Toast while preserving the existing early return and successful
super.saveAndExit flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d44190c0-acec-48c7-8efd-7828d2c5834b

📥 Commits

Reviewing files that changed from the base of the PR and between 890c492 and 510328d.

📒 Files selected for processing (11)
  • app/src/main/java/io/nekohasekai/sagernet/Constants.kt
  • app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
  • app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java
  • app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/profile/HysteriaSettingsActivity.kt
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/xml/hysteria_preferences.xml
  • app/src/test/java/io/nekohasekai/sagernet/fmt/ConfigBuilderGoldenTest.kt
  • app/src/test/java/io/nekohasekai/sagernet/fmt/HysteriaBeanSerializationTest.kt
  • app/src/test/java/io/nekohasekai/sagernet/fmt/HysteriaFmtTest.kt

@hawkff

hawkff commented Jul 13, 2026

Copy link
Copy Markdown
Owner Author

Validation completed:

  • Namespace CI run 29262164924 passed repository guards, Android lint, formatting, unit tests, migration instrumentation, native dependency handling, and APK packaging.
  • The signed arm64 APK installed as an update on Android. Existing app data remained available, rotation stayed disabled, and validation produced no fatal app exceptions.
  • ECH stayed disabled by default. The settings screen rejected an enabled blank config, persisted a valid config, and preserved it through standard URI export and import.
  • An isolated sing-box 1.13.x integration test completed a Hysteria 2 connection with static ECH and rejected a stale ECH config.

The temporary validation profiles were removed after the checks.

@hawkff
hawkff merged commit dea3468 into main Jul 13, 2026
9 checks passed
@hawkff
hawkff deleted the feat/hysteria2-ech branch July 13, 2026 19:51
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