feat(nodes): hysteria2 inbound kind — creds in DB + config generation - #123
Merged
Conversation
A node inbound can now be kind "hysteria2" (alongside panel-managed "vless"), with its client creds stored in subgen and rendered as a plain mihomo hysteria2 node. This backs the RU1 China dual-stack (Design A): hysteria2 has no 3x-ui inbound (Xray-core can't host it), so subgen sources it from stored creds instead of the panel. Access is still granted via user_connections, identically to vless. - migration 0005: node_inbounds + kind + settings (JSON) - entity: Inbound.Kind + Hysteria2Settings; Connection.Kind - nodes repo: persist/load kind + creds (create/update/get/list) - fleet: render hysteria2 proxies from user_connections + stored creds (DB, not panel) - render: proxyToMap emits type: hysteria2 - provisioning: skip hysteria2 inbounds on panels (no client; avoids a same-port VLESS mismatch in panelLookup) - API: node_save accepts kind + creds (password required); nodes_get returns kind + non-secret params (password write-only) - admin UI: kind selector + cred fields in the node form Tests: repo round-trip, fleet-build generation, handler mapping, render — unit + integration all green. Follow-up: on re-edit the hysteria2 password must be re-entered (write-only; empty -> "password required", a safe failure, not silent loss). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Postlog
commented
Aug 11, 2026
| // Protocol selects the mihomo proxy type. Empty (the default) and "vless" render a | ||
| // VLESS proxy; "hysteria2" renders the QUIC outer transport (see the hysteria2 fields | ||
| // below). Kept optional so every existing (panel-sourced) proxy stays VLESS untouched. | ||
| Protocol string |
Owner
Author
There was a problem hiding this comment.
Empty (the default)
Никаких дефолтов. Если нужна миграция в бд - надо ее добавить
| // DialerProxy chains this proxy THROUGH another proxy/group (mihomo `dialer-proxy`). | ||
| // Used by the inner VLESS hop to ride the outer hysteria2 (Design C), so the inner | ||
| // carries the per-user UUID into in-12466 while hysteria2 crosses the GFW. | ||
| DialerProxy string |
Owner
Author
There was a problem hiding this comment.
Зачем нам DialerProxy - мы же отказались от vless over hy2?
| ID int64 // node_inbounds.id (0 until persisted); referenced by user_connections | ||
| Name string // ASCII letters/digits/-, unique within the node (e.g. "force") | ||
| Port int | ||
| Kind string // "" / "vless" (panel-managed) | "hysteria2" (static; Hysteria2 set) |
Owner
Author
There was a problem hiding this comment.
То же самое что и выше - пустое значение (дефолтное) недопустимо
| for _, in := range n.Inbounds { | ||
| inbounds = append(inbounds, oas.NodesGetOKNodesItemInboundsItem{ID: in.ID, Name: in.Name, Port: in.Port}) | ||
| item := oas.NodesGetOKNodesItemInboundsItem{ID: in.ID, Name: in.Name, Port: in.Port} | ||
| if in.Kind != "" { |
Owner
Author
There was a problem hiding this comment.
пустые недопустимы, не нужно проверять на пустоту
| } | ||
| // Return the non-secret hysteria2 params so the edit form prefills them; the | ||
| // password is write-only and deliberately never sent back. | ||
| if in.IsHysteria2() && in.Hysteria2 != nil { |
Owner
Author
There was a problem hiding this comment.
Достаточно проверять in.Hysteria2 != nil
| // vless) and the JSON `settings` blob (the hysteria2 creds; empty for vless). | ||
| func inboundKindSettings(in entity.Inbound) (kind, settings string) { | ||
| kind = in.Kind | ||
| if kind == "" { |
Owner
Author
There was a problem hiding this comment.
пустое значение недопустимо
| -- settings — no behaviour change. A hysteria2 inbound is NOT provisioned on 3x-ui (Xray has | ||
| -- no hysteria2): the fleet renders it from `settings`, and provisioning skips it. | ||
| ALTER TABLE node_inbounds ADD COLUMN kind TEXT NOT NULL DEFAULT 'vless'; | ||
| ALTER TABLE node_inbounds ADD COLUMN settings TEXT NOT NULL DEFAULT ''; |
Owner
Author
There was a problem hiding this comment.
почему settings not null? Должны быть nullable, для vless нет сеттингов
- entity.Proxy: split into discriminated VLESSProxy / Hysteria2Proxy variants (no fat union struct), dropping the Protocol string and the dead DialerProxy field (a leftover from the abandoned vless-over-hysteria2 design) - Inbound.Kind is always explicit (vless|hysteria2): the handler defaults to vless, no empty-string default in the repo; migration backfills existing rows - entity.Hysteria2Settings: drop json tags — (un)marshalled via a repo-local DTO at the persistence boundary - migration 0005: settings is NULLABLE (NULL for vless, JSON for hysteria2) - nodes_get: emit kind unconditionally; simplify the guard to in.Hysteria2 != nil - tests updated (render / build / repo / handlers) — unit + integration green Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
postlog-vibecoder
previously approved these changes
Aug 11, 2026
- gosec G101/G117: nolint on the hysteria2 "requires a password" validation message (a user-facing string) and on the settings-blob Marshal (the password is intentionally persisted) - wsl_v5: blank line before the two range loops in syncPanels — the `if … continue` first statement no longer shares the just-declared map, so the cuddle isn't allowed Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
postlog-vibecoder
previously approved these changes
Aug 11, 2026
Kind is now required and validated on every inbound (vless | hysteria2): - vless inbounds must carry no hysteria2 settings; - hysteria2 inbounds are validated in full — password required; obfs (when set) must be "salamander" and obfs + obfs-password are all-or-nothing (Salamander needs a password); up/down (when set) must be a bandwidth; SNI (when set) must be a valid host. Adds five ErrValidation* sentinels + their node_save messages/mapErr cases, and TestValidateNode / TestIsBandwidth coverage. Existing fixtures now set Kind explicitly since an empty kind is rejected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
postlog-vibecoder
approved these changes
Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A node inbound can now be kind
hysteria2(alongside panel-managedvless), with itsclient creds stored in subgen and rendered as a plain mihomo
type: hysteria2node. Thisbacks the RU1 China dual-stack (Design A): hysteria2 has no 3x-ui inbound (Xray-core can't
host it), so subgen sources it from stored creds instead of the panel. Access is granted
via
user_connections, identically to vless — both are full inbounds.What's in it
node_inbounds+kind+settings(JSON)Inbound.Kind+Hysteria2Settings;Connection.Kinduser_connections+ stored creds (DB, not panel)proxyToMapemitstype: hysteria2mismatch in
panelLookup)node_saveacceptskind+ creds (password required);nodes_getreturnskind+ non-secret params (password is write-only)Testing
Repo round-trip, fleet-build generation, handler mapping, and render — unit + integration
all green;
gofmtclean;node --checkonapp.jsOK.Known follow-up
On re-edit the hysteria2 password must be re-entered (write-only; empty → "password
required", a safe failure, not silent loss). A "empty keeps existing" preserve in the repo
Update would smooth this.
Server side (Design A on RU1) is already deployed + validated and lives in the
vpn-toolchain repo (
hysteria2/,docs/hysteria2.md).🤖 Generated with Claude Code