feat: AmneziaWG outbound#25
Conversation
Add AmneziaWG as a native sing-box outbound: an obfuscated WireGuard
variant that evades DPI via junk packets (Jc/Jmin/Jmax), randomized
packet-size prefixes (S1-S4), and magic-header obfuscation (H1-H4) plus
optional CPS signatures (I1-I5). With all params zero it behaves like
plain WireGuard.
App wiring (mirrors the existing WireGuard type, TYPE_AWG=26):
- AmneziaWGBean + AmneziaWGFmt (buildSingBoxOutboundAmneziaWGBean)
- Outbound_AmneziaWGOptions in SingBoxOptions
- ProxyEntity / TypeMap ("awg") / KryoConverters / ConfigBuilder
- AmneziaWGSettingsActivity + amneziawg_preferences.xml + manifest entry
- profile-create menu entry + strings
- RawUpdater: parseAmneziaWG + .conf auto-detection for QR/clipboard/file
- Room schema bump v8 -> v9 (new awgBean column)
- libcore box_include.go registers the amneziawg outbound
Also fixes an infinite-recursion StackOverflow in SingBoxOptionSerializer
(delegate adapter re-entered the same type-hierarchy serializer), which
surfaced as a 'stack size NNNNKB' connection failure on new outbound
types; serialize via a plain Gson tree and merge hacks recursively.
Verified end-to-end on-device from a censored test network: handshake
completes and egress IP is the AmneziaWG server.
Requires the matching sing-box fork branch (hawkff/sing-box
feature/amneziawg: transport/amneziawg + protocol/amneziawg + options).
📝 WalkthroughWalkthroughAmneziaWG (an obfuscated WireGuard variant) is added as proxy type 26 across the full stack: a new ChangesAmneziaWG Proxy Type
Sequence Diagram(s)sequenceDiagram
actor User
participant ConfigurationFragment
participant AmneziaWGSettingsActivity
participant ProxyEntity
participant RawUpdater
participant ConfigBuilder
participant SingBoxOutbound
User->>ConfigurationFragment: select action_new_awg
ConfigurationFragment->>AmneziaWGSettingsActivity: startActivity
AmneziaWGSettingsActivity->>ProxyEntity: createEntity() → AmneziaWGBean
User->>AmneziaWGSettingsActivity: configure obfuscation params
AmneziaWGSettingsActivity->>ProxyEntity: serialize bean to awgBean
ProxyEntity->>ProxyEntity: store in database (schema v9)
Note over RawUpdater: On import from file
RawUpdater->>RawUpdater: isAmneziaWGConf?
RawUpdater->>RawUpdater: parseAmneziaWG → List<AmneziaWGBean>
Note over ConfigBuilder: On VPN start
ConfigBuilder->>ConfigBuilder: AmneziaWGBean dispatch
ConfigBuilder->>SingBoxOutbound: buildSingBoxOutboundAmneziaWGBean
SingBoxOutbound-->>ConfigBuilder: Outbound_AmneziaWGOptions{jc,jmin,jmax,s1–s4,h1–h4,i1–i5}
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
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 (2)
app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt (1)
427-448:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winClear
snellBeanin theputBeanreset block.On Lines 428-448, all protocol beans are nulled before assignment except
snellBean. That leaves stale Snell credentials persisted after type switches.Suggested fix
fun putBean(bean: AbstractBean): ProxyEntity { socksBean = null httpBean = null ssBean = null ssrBean = null vmessBean = null trojanBean = null trojanGoBean = null mieruBean = null naiveBean = null hysteriaBean = null sshBean = null wgBean = null awgBean = null tuicBean = null juicityBean = null shadowTLSBean = null anyTLSBean = null chainBean = null + snellBean = null configBean = null nekoBean = null🤖 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 427 - 448, The putBean method in ProxyEntity is missing the reset for snellBean when clearing all protocol beans before assignment. Add snellBean = null to the list of bean nullifications in the putBean function along with the other protocol beans (socksBean, httpBean, ssBean, ssrBean, vmessBean, trojanBean, trojanGoBean, mieruBean, naiveBean, hysteriaBean, sshBean, wgBean, awgBean, tuicBean, juicityBean, shadowTLSBean, anyTLSBean, chainBean, configBean, nekoBean). This will ensure stale Snell credentials are not retained when switching protocol types.app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt (1)
19-26:⚠️ Potential issue | 🟠 MajorAdd a non-destructive Room migration path for v8 → v9.
Line 19 bumps to v9, but Lines 20-26 don't include
AutoMigration(from = 8, to = 9). With Line 42fallbackToDestructiveMigration(), upgrades from v8 will trigger destructive migration, wiping saved profiles.Suggested fix
autoMigrations = [ AutoMigration(from = 3, to = 4), AutoMigration(from = 4, to = 5), AutoMigration(from = 5, to = 6), AutoMigration(from = 6, to = 7), - AutoMigration(from = 7, to = 8) + 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/SagerDatabase.kt` around lines 19 - 26, The database version is incremented to 9 but the autoMigrations array is missing the migration path from version 8 to 9. This causes the fallbackToDestructiveMigration() fallback to trigger during upgrades from v8, destroying user data. Add AutoMigration(from = 8, to = 9) to the autoMigrations array in the Database annotation to provide a non-destructive migration path for this version bump.
🤖 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/group/RawUpdater.kt`:
- Around line 988-989: The AmneziaWG auto-detection logic in RawUpdater.kt at
the listOf statement is missing the CPS signature keys I1 through I5, which
causes profiles using only those fields to be incorrectly parsed as plain
WireGuard. Add the strings I1, I2, I3, I4, and I5 to the listOf collection that
currently contains Jc, Jmin, Jmax, S1, S2, S3, S4, H1, H2, H3, H4 so that the
.any check properly detects all AmneziaWG profile variations.
---
Outside diff comments:
In `@app/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.kt`:
- Around line 427-448: The putBean method in ProxyEntity is missing the reset
for snellBean when clearing all protocol beans before assignment. Add snellBean
= null to the list of bean nullifications in the putBean function along with the
other protocol beans (socksBean, httpBean, ssBean, ssrBean, vmessBean,
trojanBean, trojanGoBean, mieruBean, naiveBean, hysteriaBean, sshBean, wgBean,
awgBean, tuicBean, juicityBean, shadowTLSBean, anyTLSBean, chainBean,
configBean, nekoBean). This will ensure stale Snell credentials are not retained
when switching protocol types.
In `@app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt`:
- Around line 19-26: The database version is incremented to 9 but the
autoMigrations array is missing the migration path from version 8 to 9. This
causes the fallbackToDestructiveMigration() fallback to trigger during upgrades
from v8, destroying user data. Add AutoMigration(from = 8, to = 9) to the
autoMigrations array in the Database annotation to provide a non-destructive
migration path for this version bump.
🪄 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: 2beef3fa-5c8b-4693-9795-a9a3dd22500d
⛔ Files ignored due to path filters (1)
libcore/go.sumis excluded by!**/*.sum
📒 Files selected for processing (18)
app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/9.jsonapp/src/main/AndroidManifest.xmlapp/src/main/java/io/nekohasekai/sagernet/database/ProxyEntity.ktapp/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.ktapp/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.ktapp/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.javaapp/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.ktapp/src/main/java/io/nekohasekai/sagernet/fmt/amneziawg/AmneziaWGBean.javaapp/src/main/java/io/nekohasekai/sagernet/fmt/amneziawg/AmneziaWGFmt.ktapp/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.ktapp/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.ktapp/src/main/java/io/nekohasekai/sagernet/ui/profile/AmneziaWGSettingsActivity.ktapp/src/main/java/moe/matsuri/nb4a/SingBoxOptions.javaapp/src/main/res/menu/add_profile_menu.xmlapp/src/main/res/values/strings.xmlapp/src/main/res/xml/amneziawg_preferences.xmllibcore/box_include.golibcore/go.mod
- isAmneziaWGConf now also checks the CPS signature keys I1-I5, so a .conf carrying only those AmneziaWG fields is no longer misparsed as plain WireGuard (dropping the AWG params). (CodeRabbit, Greptile) - parseAmneziaWG error messages: 'selection' -> 'section'. (Greptile)
|
Thanks for the reviews — pushed 151c67a addressing the I1–I5 detection gap (CodeRabbit Major / Greptile P2) and the 'selection' → 'section' wording. On TYPE_AWG = 26 skipping 25: this is intentional. The device-stack files and the |
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)
|
@coderabbitai review Pushed:
The WireGuard-derived device-stack files and the existing |
|
✏️ Learnings added
✅ Action performedReview finished.
|
CI cloned starifly/sing-box and checked out a commit without the new protocol/amneziawg + transport/amneziawg packages, so the Native Build (LibCore) job failed resolving github.com/sagernet/sing-box/protocol/amneziawg. Point get_source.sh at the hawkff fork and pin COMMIT_SING_BOX to the AmneziaWG commit (ed45ea9) so the native build resolves the new packages. The libcore go.mod replace (=> ../../sing-box) consumes this sibling checkout.
Summary
Adds AmneziaWG as a native sing-box outbound — an obfuscated WireGuard variant that evades DPI via junk packets (Jc/Jmin/Jmax), randomized packet-size prefixes (S1-S4), magic-header obfuscation (H1-H4), and optional CPS signature packets (I1-I5). With all obfuscation params zero, it behaves like plain WireGuard.
Verified end-to-end on-device: handshake completes, egress IP is the AmneziaWG server, confirmed via ipleak.net.
App changes (mirror the WireGuard type;
TYPE_AWG = 26)AmneziaWGBean+AmneziaWGFmt(buildSingBoxOutboundAmneziaWGBean)Outbound_AmneziaWGOptionsinSingBoxOptionsProxyEntity/TypeMap("awg") /KryoConverters/ConfigBuilderAmneziaWGSettingsActivity+amneziawg_preferences.xml+ manifest entryRawUpdater.parseAmneziaWG+.confauto-detection (QR / clipboard / file import)awgBeancolumn)libcore/box_include.goregisters the amneziawg outboundSerializer fix
Also fixes an infinite-recursion
StackOverflowErrorinSingBoxOptionSerializer(the delegate adapter re-entered the same type-hierarchy serializer), which surfaced as astack size NNNNKBconnection failure whenever a newly-added outbound type was serialized. Now serializes via a plain Gson tree and merges_hack_config_map/_hack_custom_configrecursively.Native dependency
Requires the matching sing-box fork branch
hawkff/sing-box@feature/amneziawg: addstransport/amneziawg(drives upstreamamnezia-vpn/amneziawg-goas a pristine dependency),protocol/amneziawg, andoption.AmneziaWGOutboundOptions.Test plan
Greptile Summary
This PR adds AmneziaWG (an obfuscated WireGuard variant) as a native sing-box outbound (
type = "amneziawg",TYPE_AWG = 26), and fixes an infinite-recursionStackOverflowErrorinSingBoxOptionSerializerthat surfaced whenever a newly-added outbound type was serialized.AmneziaWGBean+AmneziaWGFmt,Outbound_AmneziaWGOptions, settings activity, preferences XML,RawUpdater.parseAmneziaWG,.confauto-detection (including all I1–I5 keys),TypeMap/KryoConvertersregistration, andamneziawg.RegisterOutboundin the Go layer; Room schema bumped to v9 with a newawgBeancolumn.SingBoxOptionSerializernow callsgsonSingboxPlain.toJsonTree(src)(bypassing the hierarchy adapter) instead of delegating through the Gson context, which re-entered the same adapter for subtype instances and caused a stack overflow;applyHackConfigis then applied recursively to merge_hack_config_map/_hack_custom_config.libcore/go.moduses a localreplacedirective pointing to thehawkff/sing-boxfork;buildScript/lib/core/get_source.shclones that fork at a pinned commit so the CI build resolves correctly when the script runs first.Confidence Score: 4/5
Safe to merge for new installs; existing v8 users will lose all saved proxies on upgrade due to a missing database migration that was flagged in a prior review pass.
The app-side wiring is thorough and self-consistent: every dispatch site (putByteArray, requireBean, displayType, ConfigBuilder, TypeMap, KryoConverters) handles TYPE_AWG, and the serializer fix correctly breaks the stack-overflow cycle. The unresolved issue is SagerDatabase.kt: the autoMigrations list stops at 7→8 with no AutoMigration(from=8, to=9) entry, so the Room framework falls back to destructive migration and silently wipes all proxy data for anyone upgrading from v8.
app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt — the missing AutoMigration(from=8, to=9) entry is the only blocking concern.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["User: import .conf / QR / clipboard"] --> B{"isAmneziaWGConf?\n(any of Jc/Jmin/Jmax,\nS1-S4, H1-H4, I1-I5 present)"} B -- Yes --> C["parseAmneziaWG(conf)\n→ List<AmneziaWGBean>"] B -- No --> D["parseWireGuard(conf)\n→ List<WireGuardBean>"] C --> E["ProxyEntity(TYPE_AWG=26,\nawgBean=AmneziaWGBean)"] E --> F["Room DB v9\n(awgBean BLOB column)"] E --> G["ConfigBuilder\nbuildSingBoxOutboundAmneziaWGBean"] G --> H["Outbound_AmneziaWGOptions\ntype='amneziawg'"] H --> I["SingBoxOptionSerializer\ngsonSingboxPlain.toJsonTree(src)\n+ applyHackConfig"] I --> J["sing-box JSON config"] J --> K["libcore: amneziawg.RegisterOutbound\n(hawkff/sing-box fork)"] K --> L["amnezia-vpn/amneziawg-go v1.0.4\nhandshake + DPI evasion"] M["AmneziaWGSettingsActivity\n(manual profile creation)"] --> E%%{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"}}}%% flowchart TD A["User: import .conf / QR / clipboard"] --> B{"isAmneziaWGConf?\n(any of Jc/Jmin/Jmax,\nS1-S4, H1-H4, I1-I5 present)"} B -- Yes --> C["parseAmneziaWG(conf)\n→ List<AmneziaWGBean>"] B -- No --> D["parseWireGuard(conf)\n→ List<WireGuardBean>"] C --> E["ProxyEntity(TYPE_AWG=26,\nawgBean=AmneziaWGBean)"] E --> F["Room DB v9\n(awgBean BLOB column)"] E --> G["ConfigBuilder\nbuildSingBoxOutboundAmneziaWGBean"] G --> H["Outbound_AmneziaWGOptions\ntype='amneziawg'"] H --> I["SingBoxOptionSerializer\ngsonSingboxPlain.toJsonTree(src)\n+ applyHackConfig"] I --> J["sing-box JSON config"] J --> K["libcore: amneziawg.RegisterOutbound\n(hawkff/sing-box fork)"] K --> L["amnezia-vpn/amneziawg-go v1.0.4\nhandshake + DPI evasion"] M["AmneziaWGSettingsActivity\n(manual profile creation)"] --> EComments Outside Diff (1)
app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt, line 17-27 (link)The database version was bumped to 9, but the
autoMigrationslist stops atfrom = 7, to = 8with noAutoMigration(from = 8, to = 9)entry. Both8.jsonand9.jsonschema files are present, so Room could generate this migration automatically. As written, any user upgrading from v8 will hitfallbackToDestructiveMigration(), silently erasing all of their saved proxy configurations.Reviews (4): Last reviewed commit: "ci: build libcore against hawkff/sing-bo..." | Re-trigger Greptile